树规总结
之所以這樣命名樹規(guī),是因?yàn)闃湟?guī)的這一特殊性:沒有環(huán),dfs是不會重復(fù),而且具有明顯而又嚴(yán)格的層數(shù)關(guān)系。利用這一特性,我們可以很清晰地根據(jù)題目寫出一個(gè)在樹(型結(jié)構(gòu))上的記憶化搜索的程序。而深搜的特點(diǎn),就是“不撞南墻不回頭”。這一點(diǎn)在之后的文章中會詳細(xì)的介紹。
? ? ? ?首先是掃盲,介紹幾條名詞的專業(yè)解釋以顯示我的高端(大部分人可以略過,因?yàn)閷W(xué)習(xí)到樹規(guī)的人一下應(yīng)該都懂……):
動態(tài)規(guī)劃:?
問題可以分解成若干相互聯(lián)系的階段,在每一個(gè)階段都要做出決策,全部過程的決策是一個(gè)決策序列。要使整個(gè)活動的總體效果達(dá)到最優(yōu)的問題,稱為多階段決策問題。動態(tài)規(guī)劃就是解決多階段決策最優(yōu)化問題的一種思想方法。
階段:
將所給問題的過程,按時(shí)間或空間(樹歸中是空間,即層數(shù))特征分解成若干相互聯(lián)系的階段,以便按次序去求每階段的解。
狀態(tài):
各階段開始時(shí)的客觀條件叫做狀態(tài)。
決策:
當(dāng)各段的狀態(tài)取定以后,就可以做出不同的決定,從而確定下一階段的狀態(tài),這種決定稱為決策。 (即孩子節(jié)點(diǎn)和父親節(jié)點(diǎn)的關(guān)系)
?
策略:
由開始到終點(diǎn)的全過程中,由每段決策組成的決策序列稱為全過程策略,簡稱策略。
狀態(tài)轉(zhuǎn)移方程:
前一階段的終點(diǎn)就是后一階段的起點(diǎn),前一階段的決策選擇導(dǎo)出了后一階段的狀態(tài),這種關(guān)系描述了由k階段到k+1階段(在樹中是孩子節(jié)點(diǎn)和父親節(jié)點(diǎn))狀態(tài)的演變規(guī)律,稱為狀態(tài)轉(zhuǎn)移方程。
?
目標(biāo)函數(shù)與最優(yōu)化概念:
目標(biāo)函數(shù)是衡量多階段決策過程優(yōu)劣的準(zhǔn)則。最優(yōu)化概念是在一定條件下找到一個(gè)途徑,經(jīng)過按題目具體性質(zhì)所確定的運(yùn)算以后,使全過程的總效益達(dá)到最優(yōu)。
樹的特點(diǎn)與性質(zhì):
1、 有n個(gè)點(diǎn),n-1條邊的無向圖,任意兩頂點(diǎn)間可達(dá)
2、 無向圖中任意兩個(gè)點(diǎn)間有且只有一條路
3、 一個(gè)點(diǎn)至多有一個(gè)前趨,但可以有多個(gè)后繼
4、 無向圖中沒有環(huán);
?
廢話說完了,下面是正文:
? ? ? ?拿到一道樹規(guī)題,我們有以下3個(gè)步驟需要執(zhí)行:
判斷是否是一道樹規(guī)題:即判斷數(shù)據(jù)結(jié)構(gòu)是否是一棵樹,然后是否符合動態(tài)規(guī)劃的要求。如果是,那么執(zhí)行以下步驟,如果不是,那么換臺。
建樹:通過數(shù)據(jù)量和題目要求,選擇合適的樹的存儲方式。如果節(jié)點(diǎn)數(shù)小于5000,那么我們可以用鄰接矩陣存儲,如果更大可以用鄰接表來存儲(注意邊要開到2*n,因?yàn)槭请p向的。這是血與淚的教訓(xùn))。如果是二叉樹或者是需要多叉轉(zhuǎn)二叉,那么我們可以用兩個(gè)一維數(shù)組brother[],child[]來存儲(這一點(diǎn)下面會仔細(xì)數(shù)的)。
寫出樹規(guī)方程:通過觀察孩子和父親之間的關(guān)系建立方程。我們通常認(rèn)為,樹規(guī)的寫法有兩種:
a.根到葉子: 不過這種動態(tài)規(guī)劃在實(shí)際的問題中運(yùn)用的不多。本文只有最后一題提到。
b.葉子到根: 既根的子節(jié)點(diǎn)傳遞有用的信息給根,完后根得出最優(yōu)解的過程。這類的習(xí)題比較的多。
注意:這兩種寫法一般情況下是不能相互轉(zhuǎn)化的。但是有時(shí)可以同時(shí)使用具體往后看。
?
以下即將分析的題目的目錄及題目特點(diǎn):
1、加分二叉樹:區(qū)間動規(guī)+樹的遍歷;
2、二叉蘋果樹:二叉樹上的動規(guī);
3、最大利潤:多叉樹上的動規(guī);
4、選課:多叉樹轉(zhuǎn)二叉;
5、選課(輸出方案):多叉轉(zhuǎn)二叉+記錄路徑;
6、軟件安裝:判斷環(huán)+縮點(diǎn)+多叉轉(zhuǎn)二叉;
【4、5、6屬于依賴問題的變形】
?
基本的知識掌握和步驟了,我們就通過習(xí)題來感受一下樹規(guī)的魅力,先來看這樣一道題:
1、加分二叉樹
【問題描述】
? ? 設(shè)一個(gè)n個(gè)節(jié)點(diǎn)的二叉樹tree的中序遍歷為(l,2,3,…,n),其中數(shù)字1,2,3,…,n為節(jié)點(diǎn)編號。每個(gè)節(jié)點(diǎn)都有一個(gè)分?jǐn)?shù)(均為正整數(shù)),記第i個(gè)節(jié)點(diǎn)的分?jǐn)?shù)為di,tree及它的每個(gè)子樹都有一個(gè)加分,任一棵子樹subtree(也包含tree本身)的加分計(jì)算方法如下:
? ? subtree的左子樹的加分× subtree的右子樹的加分+subtree的根的分?jǐn)?shù)
? ? 若某個(gè)子樹為空,規(guī)定其加分為1,葉子的加分就是葉節(jié)點(diǎn)本身的分?jǐn)?shù)。不考慮它的空子樹。
? ? 試求一棵符合中序遍歷為(1,2,3,…,n)且加分最高的二叉樹tree。要求輸出;
? ? (1)tree的最高加分
? ? (2)tree的前序遍歷
【輸入格式】
? ? 第1行:一個(gè)整數(shù)n(n<30),為節(jié)點(diǎn)個(gè)數(shù)。
? ? 第2行:n個(gè)用空格隔開的整數(shù),為每個(gè)節(jié)點(diǎn)的分?jǐn)?shù)(分?jǐn)?shù)<100)。
【輸出格式】
? ? 第1行:一個(gè)整數(shù),為最高加分(結(jié)果不會超過4,000,000,000)。
第2行:n個(gè)用空格隔開的整數(shù),為該樹的前序遍歷。
?
【算法&思路】:
看到這個(gè)問題,我們首先應(yīng)該想到的是這道題是否屬于動態(tài)規(guī)劃,而這里我們發(fā)現(xiàn),結(jié)合問題,如果整棵樹的權(quán)值最大,必然有左子樹的權(quán)值最大,右子樹的權(quán)值也最大,符合最優(yōu)性原理。所以是動態(tài)規(guī)劃。而卻不是一道樹規(guī)的題目。因?yàn)槲覀兛梢杂脜^(qū)間動規(guī)的模型解決掉:直接定義一個(gè)f[i][j]表示從i到j(luò)的最大值,則f[i][j]=max(f[i][k-1]*f[k+1][j]+a[k]),枚舉k即可。接下來是如何建樹的問題,只有把樹建好了,才能輸出其前序遍歷。于是,我們看到了兩個(gè)關(guān)鍵詞:二叉樹,中序遍歷。有了這兩個(gè)關(guān)鍵詞,加上區(qū)間動規(guī),這棵樹就能建起來了。根據(jù)二叉樹的特性來建樹(這里不再具體討論樹的詳細(xì)的構(gòu)造了,中序遍歷和前序遍歷不懂得自己百度)。所以這顆樹的前序遍歷,只需要邊動規(guī)邊記錄下root[i][j]=k表示i到j(luò)的根為k即可確定樹的構(gòu)造。
【代碼】:
View Code
?
【小結(jié)】:拿到一道題目,首先我們要做的是看清題目,判斷這是一道考察什么算法的題目。只有建立在正確思路基礎(chǔ)下的算法,才是有意義的,正確的算法,也是事半功倍的算法。而此題是批著 樹形 外觀的 非樹形動態(tài)規(guī)劃題。而真正的樹形動態(tài)規(guī)劃是在樹上做動態(tài)規(guī)劃。
?
真正的樹規(guī)來了。
?
2、二叉蘋果樹
【題目描述】:
有一棵蘋果樹,如果樹枝有分叉,一定是分2叉(就是說沒有只有1個(gè)兒子的結(jié)點(diǎn))這棵樹共有N個(gè)結(jié)點(diǎn)(葉子點(diǎn)或者樹枝分叉點(diǎn)),編號為1-N,樹根編號一定是1。我們用一根樹枝兩端連接的結(jié)點(diǎn)的編號來描述一根樹枝的位置。現(xiàn)在這顆樹枝條太多了,需要剪枝。但是一些樹枝上長有蘋果。
給定需要保留的樹枝數(shù)量,求出最多能留住多少蘋果。
【輸入格式】:
第1行2個(gè)數(shù),N和Q(1<=Q<= N,1<N<=100)。
N表示樹的結(jié)點(diǎn)數(shù),Q表示要保留的樹枝數(shù)量。接下來N-1行描述樹枝的信息。
每行3個(gè)整數(shù),前兩個(gè)是它連接的結(jié)點(diǎn)的編號。第3個(gè)數(shù)是這根樹枝上蘋果的數(shù)量。
每根樹枝上的蘋果不超過30000個(gè)。
【輸出格式】:
剩余蘋果的最大數(shù)量。
input
5 2
1 3 1
1 4 10
2 3 20
3 5 20
output
21
?
【算法&思路】:首先,可以肯定的是,這是一道有關(guān)樹規(guī)的題目,父節(jié)點(diǎn)和子節(jié)點(diǎn)存在著相互關(guān)聯(lián)的階段關(guān)系。
第一步完成。再執(zhí)行第二步:我們觀察到題目數(shù)據(jù)量不大,所以有兩種選擇:鄰接矩陣和鄰接表。因?yàn)猷徑泳仃嚨拇a簡單,思路清晰,所以建議能寫鄰接矩陣的時(shí)候就不要寫鄰接表了。我們設(shè)ma[x][y]為邊的值,因?yàn)闃涫请p向的,所以要再記錄ma[y][x]。
設(shè)tree[v,1]為節(jié)點(diǎn)v的左子樹,tree[v,2]為節(jié)點(diǎn)v的右子樹,然后我們再遞歸建樹(因?yàn)闃涫沁f歸定義的,所以很多時(shí)候建樹都要考慮遞歸)。
建樹的問題解決的了,我們就要列狀態(tài)轉(zhuǎn)移方程了。根據(jù)求什么設(shè)什么的原則,我們定義f[i][j]表示以i為節(jié)點(diǎn)的根保留k條邊的最大值,那么f[v][k]=max(f[v][k],(f[tree[v][1]][i]+f[tree[v][2]][k-i-1]+num[v])),我們枚舉i就可以了。正如我開頭提到的。因?yàn)闃涫沁f歸定義的所以我們可以用記憶化搜索的形式(dfs)來具體實(shí)現(xiàn)。而樹本身嚴(yán)格分層,而且沒有環(huán)。所以是不會重復(fù)的。
F[1][Q+1]就是答案。因?yàn)轭}目中給的是邊的權(quán)值,而我們在處理時(shí)將每條邊的權(quán)值全賦給其所連的父節(jié)點(diǎn)和子節(jié)點(diǎn)中的子節(jié)點(diǎn)(將關(guān)于邊的問題轉(zhuǎn)化為關(guān)于點(diǎn)的問題),所以最后是Q+1,表示點(diǎn)的數(shù)目。
?【代碼】:
View Code
?
【小結(jié)】:在樹的存儲結(jié)構(gòu)上,我們一般選的都是二叉樹,因?yàn)槎鏄淇梢杂渺o態(tài)數(shù)組來存儲,并且狀態(tài)轉(zhuǎn)移也很好寫(根節(jié)點(diǎn)只和左子節(jié)點(diǎn)和右子節(jié)點(diǎn)有關(guān)系)。
可如果是多叉怎么辦? ? ? ? 往下看。
?
3、最大利潤
【題目描述】
政府邀請了你在火車站開飯店,但不允許同時(shí)在兩個(gè)相連接的火車站開。任意兩個(gè)火車站有且只有一條路徑,每個(gè)火車站最多有50個(gè)和它相連接的火車站。
告訴你每個(gè)火車站的利潤,問你可以獲得的最大利潤為多少。
最佳投資方案是在1,2,5,6這4個(gè)火車站開飯店可以獲得利潤為90
【輸入格式】
第一行輸入整數(shù)N(<=100000),表示有N個(gè)火車站,分別用1,2。。。,N來編號。接下來N行,每行一個(gè)整數(shù)表示每個(gè)站點(diǎn)的利潤,接下來N-1行描述火車站網(wǎng)絡(luò),每行兩個(gè)整數(shù),表示相連接的兩個(gè)站點(diǎn)。
【輸出格式】
輸出一個(gè)整數(shù)表示可以獲得的最大利潤。
【樣例輸入】
6 10
?20
25
40
30
30
4 5
1 3
3 4
2 3
6 4
【樣例輸出】
90
?
【算法&思路】:
按照上一題的步驟,我們再來分析一遍:一、是否是動態(tài)規(guī)劃。這時(shí)可能很多人已經(jīng)吐槽了:閉著眼都知道是動態(tài)規(guī)劃,不然你粘出來干什么??呵呵,沒錯(cuò),確實(shí)是。但是為什么是呢??首先,這是棵樹,是一棵多叉樹。其次,當(dāng)我們嘗試著把他向動態(tài)規(guī)劃上靠時(shí),我們發(fā)現(xiàn)當(dāng)前節(jié)點(diǎn)只與其孩子節(jié)點(diǎn)的孩子節(jié)點(diǎn)(這里沒打錯(cuò),因?yàn)楦粢粋€(gè)火車站)有關(guān)系。所以綜上所述,是動規(guī),還是一個(gè)樹規(guī),一個(gè)不折不扣的樹規(guī)!
接下來,第二步建樹。看范圍和題目發(fā)現(xiàn),這是一個(gè)有著n(<100000)的多叉樹,所以只能用鄰接表存儲了。沒有根,我們一般通常指定1為根。
第三步:F[i]表示i這條根要,G[i]表示不要(也可以用f[i][1,0]來表示)。然后以此枚舉i的孩子:如果i要了那么i的孩子就不能要,如果i不要i的孩子就可要可不要(取最大值)即可。最后輸出max(f[1],g[1]);
?【代碼】:
View Code
?
【小結(jié)】:無論是多叉樹還是二叉樹,只要我們把樹以正確的形式建立起來,那么我們再根據(jù)建樹的形式和題目要求,找出孩子和父親之間的關(guān)系,那么狀態(tài)轉(zhuǎn)移方程很容易就求解出來了。多叉其實(shí)也不是很難。對么?呵呵,那么再看下面一道題:
?
4、選課
【題目描述】
學(xué)校實(shí)行學(xué)分制。每門的必修課都有固定的學(xué)分,同時(shí)還必須獲得相應(yīng)的選修課程學(xué)分。學(xué)校開設(shè)了N(N<300)門的選修課程,每個(gè)學(xué)生可選課程的數(shù)量M是給定的。學(xué)生選修了這M門課并考核通過就能獲得相應(yīng)的學(xué)分。
在選修課程中,有些課程可以直接選修,有些課程需要一定的基礎(chǔ)知識,必須在選了其它的一些課程的基礎(chǔ)上才能選修。例如《Frontpage》必須在選修了《Windows操作基礎(chǔ)》之后才能選修。我們稱《Windows操作基礎(chǔ)》是《Frontpage》的先修課。每門課的直接先修課最多只有一門。兩門課也可能存在相同的先修課。每門課都有一個(gè)課號,依次為1,2,3,…。
你的任務(wù)是為自己確定一個(gè)選課方案,使得你能得到的學(xué)分最多,并且必須滿足先修課優(yōu)先的原則。假定課程之間不存在時(shí)間上的沖突。
?
【輸入格式 】Input Format
?
?
輸入文件的第一行包括兩個(gè)整數(shù)N、M(中間用一個(gè)空格隔開),其中1≤N≤300,1≤M≤N。
以下N行每行代表一門課。課號依次為1,2,…,N。每行有兩個(gè)數(shù)(用一個(gè)空格隔開),第一個(gè)數(shù)為這門課先修課的課號(若不存在先修課則該項(xiàng)為0),第二個(gè)數(shù)為這門課的學(xué)分。學(xué)分是不超過10的正整數(shù)。
?
?
?
?
?
?
?
【輸出格式】 Output Format
?
?
只有一個(gè)數(shù):實(shí)際所選課程的學(xué)分總數(shù)。
?
【算法&思路】:
繼續(xù)照著三步的方法判斷:一,題目大致一看,有點(diǎn)像有依賴的背包問題,于是你扭頭就走,關(guān)掉了我的《樹規(guī)》,打開了崔神犇的《背包九講》。然后你哭了,因?yàn)橛幸蕾嚨谋嘲鼏栴}只限定于一個(gè)物品只依賴于一個(gè)物品,而沒有間接的依賴關(guān)系。有依賴的背包問題的模型,根本解決不了。崔神告訴你,這屬于樹規(guī)的問題,不屬于他背包的范圍了。好了,回過來,我們接著分析。發(fā)現(xiàn)這是一棵樹,還是一棵多叉樹,嗯,很好,確定是樹規(guī)了。
然后第二步,建樹,一看數(shù)據(jù)范圍鄰接矩陣;
第三步動規(guī)方程:f[i][j]表示以i為節(jié)點(diǎn)的根的選j門課的最大值,然后有兩種情況: i不修,則i的孩子一定不修,所以為0;i修,則i的孩子們可修可不修(在這里其實(shí)可以將其轉(zhuǎn)化為將j-1個(gè)對i的孩子們進(jìn)行資源分配的問題,也屬于背包問題);答案是f[1][m]。問題圓滿解決,一氣呵成。
但……
身為追求完美的苦*程序猿的我們,不可以將它更簡單一點(diǎn)呢?
多叉轉(zhuǎn)二叉。
因?yàn)橹拔覀冋f過“在樹的存儲結(jié)構(gòu)上,我們一般選的都是二叉樹,因?yàn)槎鏄淇梢杂渺o態(tài)數(shù)組來存儲,并且狀態(tài)轉(zhuǎn)移也很好寫(根節(jié)點(diǎn)只和左子節(jié)點(diǎn)和右子節(jié)點(diǎn)有關(guān)系)。”所以轉(zhuǎn)換成二叉樹無疑是一種不錯(cuò)的選擇。
我們開兩個(gè)一維數(shù)組,b[i](brother)&c[i](child)分別表示節(jié)點(diǎn)i的孩子和兄弟,以左孩子和右兄弟的二叉樹的形式存儲這樣,根節(jié)點(diǎn)之和兩個(gè)節(jié)點(diǎn)有關(guān)系了,狀態(tài)轉(zhuǎn)移的關(guān)系少了,代碼自然也就好寫了。
我們依舊f[i][j]表示以i為節(jié)點(diǎn)的根的選j門課的最大值,那么兩種情況:1.根節(jié)點(diǎn)不選修則f[i][j]=f[b[i]][j];2.根節(jié)點(diǎn)選修f[i][j]=f[c[i]][k]+f[b[i]][j-k-1]+a[i](k表示左孩子學(xué)了k種課程);取二者的最大值即可。
【代碼】:
View Code
?
【小結(jié)】:當(dāng)題目中的數(shù)據(jù)結(jié)構(gòu)是多叉樹的時(shí)候,我們有兩種選擇:直接在多叉樹上動規(guī),或者轉(zhuǎn)化為二叉樹后動規(guī)。毫無疑問,二叉樹上的動規(guī)是簡潔的。但是,并不是說所有的多叉樹都需要轉(zhuǎn)化,一般情況下,當(dāng)根節(jié)點(diǎn)與孩子節(jié)點(diǎn)有著必然的關(guān)系時(shí)才會轉(zhuǎn)化。這需要我們多做題目,增加對樹規(guī)的感覺才能游刃有余。
?
我們繼續(xù)擴(kuò)展:如果上一道題目繼續(xù)提問,要求輸出所選的方案呢?
?
5、選課(輸出方案)
【題目描述】同上。
【輸入格式】同上。
【輸出格式】 Output Format
?
?
第一行只有一個(gè)數(shù),即實(shí)際所選課程的學(xué)分總數(shù)。
以下N行每行有一個(gè)數(shù),表示學(xué)生所選課程的課號。
n行學(xué)生選課的課號按從小到大的順序輸出。
? ?
?
【算法&思路】:拿到這道題目,首先我們必然要和上一道題目做一下對比。對比后我們發(fā)現(xiàn),這道題目和上第一道題目完全一樣,除了問題比上一題多一問:輸出方案。所以,我們可以把這道題目分成兩部分:求總數(shù)和輸出方案。而求總數(shù)的問題我們在上一題中已經(jīng)很好的解決了,所以這道題目重點(diǎn)是考察的是樹的路徑記錄的問題。
既然數(shù)是遞歸定義的,所以我們依舊使用遞歸的形式來記錄路徑:使用一個(gè)bool數(shù)組ans來進(jìn)行遞歸,分兩種情況:取(1)和不取(0)。然后,我們繼續(xù)利用已經(jīng)求得的f[i][j]的值來思考如何找到路徑:首先定義一個(gè)path()函數(shù)。如果f[i][j]=f[b[i]][j],那么節(jié)點(diǎn)i必然沒有取,讓ans[i]=0;否則,節(jié)點(diǎn)i一定取到了。(為什么呢?其實(shí),這是依照第一問的dfs來思考的,第一問的dfs是這樣定義的,所以我們就這樣考慮了。)然后依照上一問,if(f[x][y]==f[b[x]][k-1]+f[c[x]][y-k]+s[x]),那么我們在i節(jié)點(diǎn)后選的一定是以上的方案,在這時(shí)讓ans[i]=1,繼續(xù)深搜path()即可。最后從1到n依次輸出取到的點(diǎn)即可。
?
View Code
?
【小結(jié)】:路徑輸出的問題,在不同的題目中有不同的解法,比如說邊求值邊記錄等等,而在樹規(guī)中,利用動規(guī)和樹的特點(diǎn),求解完后再原路返回找一遍,是一種比較容易想到且容易操作的性價(jià)比比較高的算法。
?
6、軟件安裝
【題目描述】:
現(xiàn)在我們的手頭有N個(gè)軟件,對于一個(gè)軟件i,它要占用Wi的磁盤空間,它的價(jià)值為Vi。我們希望從中選擇一些軟件安裝到一臺磁盤容量為M的計(jì)算機(jī)上,使得這些軟件的價(jià)值盡可能大(即Vi的和最大)。
但是現(xiàn)在有個(gè)問題:軟件之間存在依賴關(guān)系,即軟件i只有在安裝了軟件j(包括軟件j的直接或間接依賴)的情況下才能正確工作(軟件i依賴軟件j)。幸運(yùn)的是,一個(gè)軟件最多依賴另外一個(gè)軟件。如果一個(gè)軟件不能正常工作,那么他能夠發(fā)揮的作用為0。
我們現(xiàn)在知道了軟件之間的依賴關(guān)系:軟件i依賴Di。現(xiàn)在請你設(shè)計(jì)出一種方案,安裝價(jià)值盡量大的軟件。一個(gè)軟件只能被安裝一次,如果一個(gè)軟件沒有依賴則Di=0,這是只要這個(gè)軟件安裝了,它就能正常工作。
?
【輸入格式】:
第1行:N,M (0<=N<=100,0<=M<=500)
第2行:W1,W2, … Wi, … ,Wn
第3行:V1,V2, … Vi, … ,Vn
第4行:D1,D2, … Di, … ,Dn
【輸出格式】:
一個(gè)整數(shù),代表最大價(jià)值。
【樣例】
3 10
5 5 6
2 3 4
0 1 1
?
5
?
【算法&思路】:同樣,這道題目類似與第4題,是一個(gè)依賴的問題,毫無疑問是一道動態(tài)規(guī)劃,但是它確實(shí)是樹規(guī)么?我們來想這樣一組數(shù)據(jù),1依賴2,2依賴3,3依賴1。這樣符合題目要求,但有形成了環(huán),所以不是一棵樹了。但是根據(jù)題目,這樣特殊的情況,要么全要,要么全就不要。所以,事實(shí)上我們可以將這個(gè)環(huán)看成一個(gè)點(diǎn)再來動規(guī),即縮點(diǎn)。如何判斷是否是一個(gè)環(huán)呢,依照數(shù)據(jù)范圍,我們想到了floyed(弗洛里德),這是在這種數(shù)據(jù)范圍內(nèi)性價(jià)比最高的方式。最后樹規(guī)。于是一個(gè)比較清晰的步驟就出來了:判環(huán),縮點(diǎn),樹規(guī)。
接下來是細(xì)節(jié):首先存樹,毫無疑問,是鄰接矩陣。
做floyed。如果兩點(diǎn)之間mapp[i][j]中有另一條路徑相連,即mapp[i][k]=1 && mapp[k][j]=1(1表示兩點(diǎn)是通的);那么mapp[i][j]也是通的且是環(huán)。
縮點(diǎn)。這個(gè)是最麻煩的,麻煩在于我們要把縮的點(diǎn)當(dāng)成一個(gè)新點(diǎn)來判斷,而且要判斷某個(gè)點(diǎn)是否在某個(gè)環(huán)里。我們用染色法來判斷,用所占的空間w控制顏色的對應(yīng),有以下三種情況:1、點(diǎn)i所在的環(huán)之前沒有判斷過,是新環(huán)。那么,我們將這個(gè)新環(huán)放到數(shù)組最后,即新加一個(gè)點(diǎn),然后讓這兩個(gè)點(diǎn)的空間標(biāo)記為負(fù)值tmpw,且tmpw+tmpn(新點(diǎn)的下標(biāo))等于原來的點(diǎn)數(shù),這樣,我們就可以通過某個(gè)點(diǎn)的空間迅速找到他所在的新點(diǎn)。像鑰匙一樣一一對應(yīng);2、點(diǎn)i所在的環(huán)之前已經(jīng)判斷過了,是舊環(huán)(已合成新點(diǎn)),且i是環(huán)的一部分。那么我們就把i也加到這個(gè)新點(diǎn)里面,即體積,價(jià)值相加即可;3、點(diǎn)j所在的環(huán)是舊環(huán),但是i不是環(huán)的一部分(例如1依賴2,2依賴3,3依賴1。4也依賴1,那么,4所在的是個(gè)環(huán),但4不屬于環(huán)的一部分)。那么,把j的父親轉(zhuǎn)到新點(diǎn)上d[j]= n-w[d[j]]。
以上縮點(diǎn)的工作做完之后,剩下的就是一棵樹。就可以在這上面動規(guī)了:先將其轉(zhuǎn)換成一棵左孩子右兄弟的二叉樹,之后記憶化。i的孩子不取f[b[x]][k]=dfs(b[x],k);還是取:f[c[x]][y-i]=dfs(c[x],y-i); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?f[b[x]][i]=dfs(b[x],i); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?f[x][k]=max(f[x][k],v[x]+f[c[x]][y-i]+f[b[x]][i]);
最后答案是f[c[0]][m]。
【代碼】:
復(fù)制代碼
? 1 #include<iostream>
? 2 #include<iomanip>
? 3 #include<cstring>
? 4 #include<cmath>
? 5 #include<cstdio>
? 6 #include<cstdlib>
? 7 #include<string>
? 8 #include<memory>
? 9 #include<climits>
?10 #include<vector>
?11 #include<map>
?12 #include<queue>
?13 #include<algorithm>
?14 using namespace std;
?15?
?16 const int e=505;
?17 int n,m,tmpw=0,tmpn;
?18 int w[e]={0},v[e]={0},b[e]={0},c[e]={0},f[e][5*e]={0},d[e]={0};
?19 bool mapp[e][e]={0};
?20?
?21 void floride()
?22 {
?23 ? ? for(int i=1;i<=n;i++)//弗洛里德判斷是否有環(huán);
?24 ? ? ? ? for(int j=1;j<=n;j++)
?25 ? ? ? ? ? ? for(int k=1;k<=n;k++)
?26 ? ? ? ? ? ? ? ? if(mapp[k][i]==1 && mapp[i][j]==1)
?27 ? ? ? ? ? ? ? ? ? ? mapp[k][j]=1;
?28 }
?29?
?30?
?31 void merge()//合點(diǎn)
?32 {
?33 ? ? tmpn=n;
?34 ? ? for(int i=1;i<=tmpn;i++)
?35 ? ? ? ? for(int j=1;j<=tmpn;j++)
?36 ? ? ? ? {
?37 ? ? ? ? ? ? if(mapp[i][j]==1 && mapp[j][i]==1 && i!=j && w[i]>0 && w[j]>0)//如果是新環(huán);
?38 ? ? ? ? ? ? {
?39 ? ? ? ? ? ? ? ? tmpn++;
?40 ? ? ? ? ? ? ? ? v[tmpn]=v[i]+v[j];
?41 ? ? ? ? ? ? ? ? w[tmpn]=w[i]+w[j];
?42 ? ? ? ? ? ? ? ? tmpw--; ? ?w[i]=tmpw; ? ?w[j]=tmpw; ? ?//tmpw+tmpn永遠(yuǎn)等于最開始的n
?43 ? ? ? ? ? ? }
?44 ? ? ? ? ? ??
?45 ? ? ? ? ? ? //如果j依賴的點(diǎn)被合并(是舊環(huán)),且j在環(huán)里
?46 ? ? ? ? ? ? if(w[d[j]]<0 && w[j]>0 && mapp[j][d[j]]==1 && mapp[j][d[j]]==1) ? ?
?47 ? ? ? ? ? ? {
?48 ? ? ? ? ? ? ? ? w[n-w[d[j]]]+=w[j];
?49 ? ? ? ? ? ? ? ? v[n-w[d[j]]]+=v[j];
?50 ? ? ? ? ? ? ? ? w[j]=w[d[j]];
?51 ? ? ? ? ? ? }
?52 ? ? ? ? ? ??
?53 ? ? ? ? ? ? //如果j依賴的點(diǎn)在環(huán)里,但是j不在環(huán)里
?54 ? ? ? ? ? ? if(w[d[j]]<0 && w[j]>0)
?55 ? ? ? ? ? ? ? ? if((mapp[j][d[j]]==1 && mapp[d[j]][j]==0) || (mapp[j][d[j]]==0 && mapp[d[j]][j]==1))
?56 ? ? ? ? ? ? ? ? ? ? d[j]=n-w[d[j]];
?57 ? ? ? ? }
?58 }
?59?
?60 int ?dfs(int x,int k)
?61 {
?62 ? ? if(f[x][k]>0) ? ?return(f[x][k]);
?63 ? ? if(x==0 || k<=0) ? ?return(0);
?64 ? ? //不取x
?65 ? ? f[b[x]][k]=dfs(b[x],k);
?66 ? ? f[x][k]=f[b[x]][k];
?67 ? ? ? ? int y=k-w[x];
?68 ? ? ? ? for(int i=0;i<=y;i++)
?69 ? ? ? ? {
?70 ? ? ? ? ? ? f[c[x]][y-i]=dfs(c[x],y-i);
?71 ? ? ? ? ? ? f[b[x]][i]=dfs(b[x],i);
?72 ? ? ? ? ? ? f[x][k]=max(f[x][k],v[x]+f[c[x]][y-i]+f[b[x]][i]);
?73 ? ? ? ? }
?74 ? ? return(f[x][k]);
?75 }
?76?
?77?
?78?
?79 int main()
?80 {
?81 ? ? //freopen("in.in","r",stdin);
?82 ? ? cin>>n>>m;
?83 ? ? for(int i=1;i<=n;i++)
?84 ? ? ? ? scanf("%d",&w[i]);
?85 ? ? for(int i=1;i<=n;i++)
?86 ? ? ? ? scanf("%d",&v[i]);
?87 ? ? for(int i=1;i<=n;i++)
?88 ? ? {
?89 ? ? ? ? int a;
?90 ? ? ? ? scanf("%d",&a);
?91 ? ? ? ? d[i]=a;
?92 ? ? ? ? mapp[a][i]=1;
?93 ? ? }
?94 ? ??
?95 ? ? floride();
?96 ? ? merge();
?97 ? ??
?98 ? ? //多叉轉(zhuǎn)二叉
?99 ? ? for(int i=1;i<=tmpn;i++)
100 ? ? ? ? if(w[i]>0)
101 ? ? ? ? {
102 ? ? ? ? ? ? b[i]=c[d[i]];
103 ? ? ? ? ? ? c[d[i]]=i;
104 ? ? ? ? }
105 ? ? cout<<dfs(c[0],m);
106?
107 ? ??
108 ? ? fclose(stdin); ? ?fclose(stdout);
109 ? ? return 0;
110 }
復(fù)制代碼
?
【小結(jié)】:依賴問題的變化很多,比如基本樹規(guī),記錄路徑,有環(huán)等等。但是他們都有一些共同的特點(diǎn),比如說記憶化的方程差不多。根據(jù)題目,我們應(yīng)該會判斷給的數(shù)據(jù)是否是圖,是樹,能想出特殊情況。樹畢竟是圖的一種特殊形式,而二叉樹又是樹的一種特殊形式。如果能將一個(gè)問題由復(fù)雜向簡單轉(zhuǎn)換,那么我們不僅思路會清晰很多,代碼量也會少很多。下面一道題目,就是我們所說的很少見的根節(jié)點(diǎn)向葉子節(jié)點(diǎn)動規(guī)的問題。
?
【總結(jié)】:樹規(guī)是動態(tài)規(guī)劃的一種,它將樹和動態(tài)規(guī)劃很巧妙地結(jié)合在了一起。做樹規(guī)題目,不僅僅鍛煉了我們的代碼能力,而且加深了我們對動態(tài)規(guī)劃的理解。再次強(qiáng)調(diào),樹的遞歸定義使樹規(guī)多以記憶化的形式來寫,而由于樹的嚴(yán)格分層,使動規(guī)的階段自然就清晰了起來,多找一找父節(jié)點(diǎn)與子節(jié)點(diǎn)的關(guān)系,就是很可能是兩個(gè)階段之間的聯(lián)系。
? ? ? ?首先是掃盲,介紹幾條名詞的專業(yè)解釋以顯示我的高端(大部分人可以略過,因?yàn)閷W(xué)習(xí)到樹規(guī)的人一下應(yīng)該都懂……):
動態(tài)規(guī)劃:?
問題可以分解成若干相互聯(lián)系的階段,在每一個(gè)階段都要做出決策,全部過程的決策是一個(gè)決策序列。要使整個(gè)活動的總體效果達(dá)到最優(yōu)的問題,稱為多階段決策問題。動態(tài)規(guī)劃就是解決多階段決策最優(yōu)化問題的一種思想方法。
階段:
將所給問題的過程,按時(shí)間或空間(樹歸中是空間,即層數(shù))特征分解成若干相互聯(lián)系的階段,以便按次序去求每階段的解。
狀態(tài):
各階段開始時(shí)的客觀條件叫做狀態(tài)。
決策:
當(dāng)各段的狀態(tài)取定以后,就可以做出不同的決定,從而確定下一階段的狀態(tài),這種決定稱為決策。 (即孩子節(jié)點(diǎn)和父親節(jié)點(diǎn)的關(guān)系)
?
策略:
由開始到終點(diǎn)的全過程中,由每段決策組成的決策序列稱為全過程策略,簡稱策略。
狀態(tài)轉(zhuǎn)移方程:
前一階段的終點(diǎn)就是后一階段的起點(diǎn),前一階段的決策選擇導(dǎo)出了后一階段的狀態(tài),這種關(guān)系描述了由k階段到k+1階段(在樹中是孩子節(jié)點(diǎn)和父親節(jié)點(diǎn))狀態(tài)的演變規(guī)律,稱為狀態(tài)轉(zhuǎn)移方程。
?
目標(biāo)函數(shù)與最優(yōu)化概念:
目標(biāo)函數(shù)是衡量多階段決策過程優(yōu)劣的準(zhǔn)則。最優(yōu)化概念是在一定條件下找到一個(gè)途徑,經(jīng)過按題目具體性質(zhì)所確定的運(yùn)算以后,使全過程的總效益達(dá)到最優(yōu)。
樹的特點(diǎn)與性質(zhì):
1、 有n個(gè)點(diǎn),n-1條邊的無向圖,任意兩頂點(diǎn)間可達(dá)
2、 無向圖中任意兩個(gè)點(diǎn)間有且只有一條路
3、 一個(gè)點(diǎn)至多有一個(gè)前趨,但可以有多個(gè)后繼
4、 無向圖中沒有環(huán);
?
廢話說完了,下面是正文:
? ? ? ?拿到一道樹規(guī)題,我們有以下3個(gè)步驟需要執(zhí)行:
判斷是否是一道樹規(guī)題:即判斷數(shù)據(jù)結(jié)構(gòu)是否是一棵樹,然后是否符合動態(tài)規(guī)劃的要求。如果是,那么執(zhí)行以下步驟,如果不是,那么換臺。
建樹:通過數(shù)據(jù)量和題目要求,選擇合適的樹的存儲方式。如果節(jié)點(diǎn)數(shù)小于5000,那么我們可以用鄰接矩陣存儲,如果更大可以用鄰接表來存儲(注意邊要開到2*n,因?yàn)槭请p向的。這是血與淚的教訓(xùn))。如果是二叉樹或者是需要多叉轉(zhuǎn)二叉,那么我們可以用兩個(gè)一維數(shù)組brother[],child[]來存儲(這一點(diǎn)下面會仔細(xì)數(shù)的)。
寫出樹規(guī)方程:通過觀察孩子和父親之間的關(guān)系建立方程。我們通常認(rèn)為,樹規(guī)的寫法有兩種:
a.根到葉子: 不過這種動態(tài)規(guī)劃在實(shí)際的問題中運(yùn)用的不多。本文只有最后一題提到。
b.葉子到根: 既根的子節(jié)點(diǎn)傳遞有用的信息給根,完后根得出最優(yōu)解的過程。這類的習(xí)題比較的多。
注意:這兩種寫法一般情況下是不能相互轉(zhuǎn)化的。但是有時(shí)可以同時(shí)使用具體往后看。
?
以下即將分析的題目的目錄及題目特點(diǎn):
1、加分二叉樹:區(qū)間動規(guī)+樹的遍歷;
2、二叉蘋果樹:二叉樹上的動規(guī);
3、最大利潤:多叉樹上的動規(guī);
4、選課:多叉樹轉(zhuǎn)二叉;
5、選課(輸出方案):多叉轉(zhuǎn)二叉+記錄路徑;
6、軟件安裝:判斷環(huán)+縮點(diǎn)+多叉轉(zhuǎn)二叉;
【4、5、6屬于依賴問題的變形】
?
基本的知識掌握和步驟了,我們就通過習(xí)題來感受一下樹規(guī)的魅力,先來看這樣一道題:
1、加分二叉樹
【問題描述】
? ? 設(shè)一個(gè)n個(gè)節(jié)點(diǎn)的二叉樹tree的中序遍歷為(l,2,3,…,n),其中數(shù)字1,2,3,…,n為節(jié)點(diǎn)編號。每個(gè)節(jié)點(diǎn)都有一個(gè)分?jǐn)?shù)(均為正整數(shù)),記第i個(gè)節(jié)點(diǎn)的分?jǐn)?shù)為di,tree及它的每個(gè)子樹都有一個(gè)加分,任一棵子樹subtree(也包含tree本身)的加分計(jì)算方法如下:
? ? subtree的左子樹的加分× subtree的右子樹的加分+subtree的根的分?jǐn)?shù)
? ? 若某個(gè)子樹為空,規(guī)定其加分為1,葉子的加分就是葉節(jié)點(diǎn)本身的分?jǐn)?shù)。不考慮它的空子樹。
? ? 試求一棵符合中序遍歷為(1,2,3,…,n)且加分最高的二叉樹tree。要求輸出;
? ? (1)tree的最高加分
? ? (2)tree的前序遍歷
【輸入格式】
? ? 第1行:一個(gè)整數(shù)n(n<30),為節(jié)點(diǎn)個(gè)數(shù)。
? ? 第2行:n個(gè)用空格隔開的整數(shù),為每個(gè)節(jié)點(diǎn)的分?jǐn)?shù)(分?jǐn)?shù)<100)。
【輸出格式】
? ? 第1行:一個(gè)整數(shù),為最高加分(結(jié)果不會超過4,000,000,000)。
第2行:n個(gè)用空格隔開的整數(shù),為該樹的前序遍歷。
?
【算法&思路】:
看到這個(gè)問題,我們首先應(yīng)該想到的是這道題是否屬于動態(tài)規(guī)劃,而這里我們發(fā)現(xiàn),結(jié)合問題,如果整棵樹的權(quán)值最大,必然有左子樹的權(quán)值最大,右子樹的權(quán)值也最大,符合最優(yōu)性原理。所以是動態(tài)規(guī)劃。而卻不是一道樹規(guī)的題目。因?yàn)槲覀兛梢杂脜^(qū)間動規(guī)的模型解決掉:直接定義一個(gè)f[i][j]表示從i到j(luò)的最大值,則f[i][j]=max(f[i][k-1]*f[k+1][j]+a[k]),枚舉k即可。接下來是如何建樹的問題,只有把樹建好了,才能輸出其前序遍歷。于是,我們看到了兩個(gè)關(guān)鍵詞:二叉樹,中序遍歷。有了這兩個(gè)關(guān)鍵詞,加上區(qū)間動規(guī),這棵樹就能建起來了。根據(jù)二叉樹的特性來建樹(這里不再具體討論樹的詳細(xì)的構(gòu)造了,中序遍歷和前序遍歷不懂得自己百度)。所以這顆樹的前序遍歷,只需要邊動規(guī)邊記錄下root[i][j]=k表示i到j(luò)的根為k即可確定樹的構(gòu)造。
【代碼】:
View Code
?
【小結(jié)】:拿到一道題目,首先我們要做的是看清題目,判斷這是一道考察什么算法的題目。只有建立在正確思路基礎(chǔ)下的算法,才是有意義的,正確的算法,也是事半功倍的算法。而此題是批著 樹形 外觀的 非樹形動態(tài)規(guī)劃題。而真正的樹形動態(tài)規(guī)劃是在樹上做動態(tài)規(guī)劃。
?
真正的樹規(guī)來了。
?
2、二叉蘋果樹
【題目描述】:
有一棵蘋果樹,如果樹枝有分叉,一定是分2叉(就是說沒有只有1個(gè)兒子的結(jié)點(diǎn))這棵樹共有N個(gè)結(jié)點(diǎn)(葉子點(diǎn)或者樹枝分叉點(diǎn)),編號為1-N,樹根編號一定是1。我們用一根樹枝兩端連接的結(jié)點(diǎn)的編號來描述一根樹枝的位置。現(xiàn)在這顆樹枝條太多了,需要剪枝。但是一些樹枝上長有蘋果。
給定需要保留的樹枝數(shù)量,求出最多能留住多少蘋果。
【輸入格式】:
第1行2個(gè)數(shù),N和Q(1<=Q<= N,1<N<=100)。
N表示樹的結(jié)點(diǎn)數(shù),Q表示要保留的樹枝數(shù)量。接下來N-1行描述樹枝的信息。
每行3個(gè)整數(shù),前兩個(gè)是它連接的結(jié)點(diǎn)的編號。第3個(gè)數(shù)是這根樹枝上蘋果的數(shù)量。
每根樹枝上的蘋果不超過30000個(gè)。
【輸出格式】:
剩余蘋果的最大數(shù)量。
input
5 2
1 3 1
1 4 10
2 3 20
3 5 20
output
21
?
【算法&思路】:首先,可以肯定的是,這是一道有關(guān)樹規(guī)的題目,父節(jié)點(diǎn)和子節(jié)點(diǎn)存在著相互關(guān)聯(lián)的階段關(guān)系。
第一步完成。再執(zhí)行第二步:我們觀察到題目數(shù)據(jù)量不大,所以有兩種選擇:鄰接矩陣和鄰接表。因?yàn)猷徑泳仃嚨拇a簡單,思路清晰,所以建議能寫鄰接矩陣的時(shí)候就不要寫鄰接表了。我們設(shè)ma[x][y]為邊的值,因?yàn)闃涫请p向的,所以要再記錄ma[y][x]。
設(shè)tree[v,1]為節(jié)點(diǎn)v的左子樹,tree[v,2]為節(jié)點(diǎn)v的右子樹,然后我們再遞歸建樹(因?yàn)闃涫沁f歸定義的,所以很多時(shí)候建樹都要考慮遞歸)。
建樹的問題解決的了,我們就要列狀態(tài)轉(zhuǎn)移方程了。根據(jù)求什么設(shè)什么的原則,我們定義f[i][j]表示以i為節(jié)點(diǎn)的根保留k條邊的最大值,那么f[v][k]=max(f[v][k],(f[tree[v][1]][i]+f[tree[v][2]][k-i-1]+num[v])),我們枚舉i就可以了。正如我開頭提到的。因?yàn)闃涫沁f歸定義的所以我們可以用記憶化搜索的形式(dfs)來具體實(shí)現(xiàn)。而樹本身嚴(yán)格分層,而且沒有環(huán)。所以是不會重復(fù)的。
F[1][Q+1]就是答案。因?yàn)轭}目中給的是邊的權(quán)值,而我們在處理時(shí)將每條邊的權(quán)值全賦給其所連的父節(jié)點(diǎn)和子節(jié)點(diǎn)中的子節(jié)點(diǎn)(將關(guān)于邊的問題轉(zhuǎn)化為關(guān)于點(diǎn)的問題),所以最后是Q+1,表示點(diǎn)的數(shù)目。
?【代碼】:
View Code
?
【小結(jié)】:在樹的存儲結(jié)構(gòu)上,我們一般選的都是二叉樹,因?yàn)槎鏄淇梢杂渺o態(tài)數(shù)組來存儲,并且狀態(tài)轉(zhuǎn)移也很好寫(根節(jié)點(diǎn)只和左子節(jié)點(diǎn)和右子節(jié)點(diǎn)有關(guān)系)。
可如果是多叉怎么辦? ? ? ? 往下看。
?
3、最大利潤
【題目描述】
政府邀請了你在火車站開飯店,但不允許同時(shí)在兩個(gè)相連接的火車站開。任意兩個(gè)火車站有且只有一條路徑,每個(gè)火車站最多有50個(gè)和它相連接的火車站。
告訴你每個(gè)火車站的利潤,問你可以獲得的最大利潤為多少。
最佳投資方案是在1,2,5,6這4個(gè)火車站開飯店可以獲得利潤為90
【輸入格式】
第一行輸入整數(shù)N(<=100000),表示有N個(gè)火車站,分別用1,2。。。,N來編號。接下來N行,每行一個(gè)整數(shù)表示每個(gè)站點(diǎn)的利潤,接下來N-1行描述火車站網(wǎng)絡(luò),每行兩個(gè)整數(shù),表示相連接的兩個(gè)站點(diǎn)。
【輸出格式】
輸出一個(gè)整數(shù)表示可以獲得的最大利潤。
【樣例輸入】
6 10
?20
25
40
30
30
4 5
1 3
3 4
2 3
6 4
【樣例輸出】
90
?
【算法&思路】:
按照上一題的步驟,我們再來分析一遍:一、是否是動態(tài)規(guī)劃。這時(shí)可能很多人已經(jīng)吐槽了:閉著眼都知道是動態(tài)規(guī)劃,不然你粘出來干什么??呵呵,沒錯(cuò),確實(shí)是。但是為什么是呢??首先,這是棵樹,是一棵多叉樹。其次,當(dāng)我們嘗試著把他向動態(tài)規(guī)劃上靠時(shí),我們發(fā)現(xiàn)當(dāng)前節(jié)點(diǎn)只與其孩子節(jié)點(diǎn)的孩子節(jié)點(diǎn)(這里沒打錯(cuò),因?yàn)楦粢粋€(gè)火車站)有關(guān)系。所以綜上所述,是動規(guī),還是一個(gè)樹規(guī),一個(gè)不折不扣的樹規(guī)!
接下來,第二步建樹。看范圍和題目發(fā)現(xiàn),這是一個(gè)有著n(<100000)的多叉樹,所以只能用鄰接表存儲了。沒有根,我們一般通常指定1為根。
第三步:F[i]表示i這條根要,G[i]表示不要(也可以用f[i][1,0]來表示)。然后以此枚舉i的孩子:如果i要了那么i的孩子就不能要,如果i不要i的孩子就可要可不要(取最大值)即可。最后輸出max(f[1],g[1]);
?【代碼】:
View Code
?
【小結(jié)】:無論是多叉樹還是二叉樹,只要我們把樹以正確的形式建立起來,那么我們再根據(jù)建樹的形式和題目要求,找出孩子和父親之間的關(guān)系,那么狀態(tài)轉(zhuǎn)移方程很容易就求解出來了。多叉其實(shí)也不是很難。對么?呵呵,那么再看下面一道題:
?
4、選課
【題目描述】
學(xué)校實(shí)行學(xué)分制。每門的必修課都有固定的學(xué)分,同時(shí)還必須獲得相應(yīng)的選修課程學(xué)分。學(xué)校開設(shè)了N(N<300)門的選修課程,每個(gè)學(xué)生可選課程的數(shù)量M是給定的。學(xué)生選修了這M門課并考核通過就能獲得相應(yīng)的學(xué)分。
在選修課程中,有些課程可以直接選修,有些課程需要一定的基礎(chǔ)知識,必須在選了其它的一些課程的基礎(chǔ)上才能選修。例如《Frontpage》必須在選修了《Windows操作基礎(chǔ)》之后才能選修。我們稱《Windows操作基礎(chǔ)》是《Frontpage》的先修課。每門課的直接先修課最多只有一門。兩門課也可能存在相同的先修課。每門課都有一個(gè)課號,依次為1,2,3,…。
你的任務(wù)是為自己確定一個(gè)選課方案,使得你能得到的學(xué)分最多,并且必須滿足先修課優(yōu)先的原則。假定課程之間不存在時(shí)間上的沖突。
?
【輸入格式 】Input Format
?
?
輸入文件的第一行包括兩個(gè)整數(shù)N、M(中間用一個(gè)空格隔開),其中1≤N≤300,1≤M≤N。
以下N行每行代表一門課。課號依次為1,2,…,N。每行有兩個(gè)數(shù)(用一個(gè)空格隔開),第一個(gè)數(shù)為這門課先修課的課號(若不存在先修課則該項(xiàng)為0),第二個(gè)數(shù)為這門課的學(xué)分。學(xué)分是不超過10的正整數(shù)。
?
?
?
?
?
?
?
【輸出格式】 Output Format
?
?
只有一個(gè)數(shù):實(shí)際所選課程的學(xué)分總數(shù)。
?
【算法&思路】:
繼續(xù)照著三步的方法判斷:一,題目大致一看,有點(diǎn)像有依賴的背包問題,于是你扭頭就走,關(guān)掉了我的《樹規(guī)》,打開了崔神犇的《背包九講》。然后你哭了,因?yàn)橛幸蕾嚨谋嘲鼏栴}只限定于一個(gè)物品只依賴于一個(gè)物品,而沒有間接的依賴關(guān)系。有依賴的背包問題的模型,根本解決不了。崔神告訴你,這屬于樹規(guī)的問題,不屬于他背包的范圍了。好了,回過來,我們接著分析。發(fā)現(xiàn)這是一棵樹,還是一棵多叉樹,嗯,很好,確定是樹規(guī)了。
然后第二步,建樹,一看數(shù)據(jù)范圍鄰接矩陣;
第三步動規(guī)方程:f[i][j]表示以i為節(jié)點(diǎn)的根的選j門課的最大值,然后有兩種情況: i不修,則i的孩子一定不修,所以為0;i修,則i的孩子們可修可不修(在這里其實(shí)可以將其轉(zhuǎn)化為將j-1個(gè)對i的孩子們進(jìn)行資源分配的問題,也屬于背包問題);答案是f[1][m]。問題圓滿解決,一氣呵成。
但……
身為追求完美的苦*程序猿的我們,不可以將它更簡單一點(diǎn)呢?
多叉轉(zhuǎn)二叉。
因?yàn)橹拔覀冋f過“在樹的存儲結(jié)構(gòu)上,我們一般選的都是二叉樹,因?yàn)槎鏄淇梢杂渺o態(tài)數(shù)組來存儲,并且狀態(tài)轉(zhuǎn)移也很好寫(根節(jié)點(diǎn)只和左子節(jié)點(diǎn)和右子節(jié)點(diǎn)有關(guān)系)。”所以轉(zhuǎn)換成二叉樹無疑是一種不錯(cuò)的選擇。
我們開兩個(gè)一維數(shù)組,b[i](brother)&c[i](child)分別表示節(jié)點(diǎn)i的孩子和兄弟,以左孩子和右兄弟的二叉樹的形式存儲這樣,根節(jié)點(diǎn)之和兩個(gè)節(jié)點(diǎn)有關(guān)系了,狀態(tài)轉(zhuǎn)移的關(guān)系少了,代碼自然也就好寫了。
我們依舊f[i][j]表示以i為節(jié)點(diǎn)的根的選j門課的最大值,那么兩種情況:1.根節(jié)點(diǎn)不選修則f[i][j]=f[b[i]][j];2.根節(jié)點(diǎn)選修f[i][j]=f[c[i]][k]+f[b[i]][j-k-1]+a[i](k表示左孩子學(xué)了k種課程);取二者的最大值即可。
【代碼】:
View Code
?
【小結(jié)】:當(dāng)題目中的數(shù)據(jù)結(jié)構(gòu)是多叉樹的時(shí)候,我們有兩種選擇:直接在多叉樹上動規(guī),或者轉(zhuǎn)化為二叉樹后動規(guī)。毫無疑問,二叉樹上的動規(guī)是簡潔的。但是,并不是說所有的多叉樹都需要轉(zhuǎn)化,一般情況下,當(dāng)根節(jié)點(diǎn)與孩子節(jié)點(diǎn)有著必然的關(guān)系時(shí)才會轉(zhuǎn)化。這需要我們多做題目,增加對樹規(guī)的感覺才能游刃有余。
?
我們繼續(xù)擴(kuò)展:如果上一道題目繼續(xù)提問,要求輸出所選的方案呢?
?
5、選課(輸出方案)
【題目描述】同上。
【輸入格式】同上。
【輸出格式】 Output Format
?
?
第一行只有一個(gè)數(shù),即實(shí)際所選課程的學(xué)分總數(shù)。
以下N行每行有一個(gè)數(shù),表示學(xué)生所選課程的課號。
n行學(xué)生選課的課號按從小到大的順序輸出。
? ?
?
【算法&思路】:拿到這道題目,首先我們必然要和上一道題目做一下對比。對比后我們發(fā)現(xiàn),這道題目和上第一道題目完全一樣,除了問題比上一題多一問:輸出方案。所以,我們可以把這道題目分成兩部分:求總數(shù)和輸出方案。而求總數(shù)的問題我們在上一題中已經(jīng)很好的解決了,所以這道題目重點(diǎn)是考察的是樹的路徑記錄的問題。
既然數(shù)是遞歸定義的,所以我們依舊使用遞歸的形式來記錄路徑:使用一個(gè)bool數(shù)組ans來進(jìn)行遞歸,分兩種情況:取(1)和不取(0)。然后,我們繼續(xù)利用已經(jīng)求得的f[i][j]的值來思考如何找到路徑:首先定義一個(gè)path()函數(shù)。如果f[i][j]=f[b[i]][j],那么節(jié)點(diǎn)i必然沒有取,讓ans[i]=0;否則,節(jié)點(diǎn)i一定取到了。(為什么呢?其實(shí),這是依照第一問的dfs來思考的,第一問的dfs是這樣定義的,所以我們就這樣考慮了。)然后依照上一問,if(f[x][y]==f[b[x]][k-1]+f[c[x]][y-k]+s[x]),那么我們在i節(jié)點(diǎn)后選的一定是以上的方案,在這時(shí)讓ans[i]=1,繼續(xù)深搜path()即可。最后從1到n依次輸出取到的點(diǎn)即可。
?
View Code
?
【小結(jié)】:路徑輸出的問題,在不同的題目中有不同的解法,比如說邊求值邊記錄等等,而在樹規(guī)中,利用動規(guī)和樹的特點(diǎn),求解完后再原路返回找一遍,是一種比較容易想到且容易操作的性價(jià)比比較高的算法。
?
6、軟件安裝
【題目描述】:
現(xiàn)在我們的手頭有N個(gè)軟件,對于一個(gè)軟件i,它要占用Wi的磁盤空間,它的價(jià)值為Vi。我們希望從中選擇一些軟件安裝到一臺磁盤容量為M的計(jì)算機(jī)上,使得這些軟件的價(jià)值盡可能大(即Vi的和最大)。
但是現(xiàn)在有個(gè)問題:軟件之間存在依賴關(guān)系,即軟件i只有在安裝了軟件j(包括軟件j的直接或間接依賴)的情況下才能正確工作(軟件i依賴軟件j)。幸運(yùn)的是,一個(gè)軟件最多依賴另外一個(gè)軟件。如果一個(gè)軟件不能正常工作,那么他能夠發(fā)揮的作用為0。
我們現(xiàn)在知道了軟件之間的依賴關(guān)系:軟件i依賴Di。現(xiàn)在請你設(shè)計(jì)出一種方案,安裝價(jià)值盡量大的軟件。一個(gè)軟件只能被安裝一次,如果一個(gè)軟件沒有依賴則Di=0,這是只要這個(gè)軟件安裝了,它就能正常工作。
?
【輸入格式】:
第1行:N,M (0<=N<=100,0<=M<=500)
第2行:W1,W2, … Wi, … ,Wn
第3行:V1,V2, … Vi, … ,Vn
第4行:D1,D2, … Di, … ,Dn
【輸出格式】:
一個(gè)整數(shù),代表最大價(jià)值。
【樣例】
3 10
5 5 6
2 3 4
0 1 1
?
5
?
【算法&思路】:同樣,這道題目類似與第4題,是一個(gè)依賴的問題,毫無疑問是一道動態(tài)規(guī)劃,但是它確實(shí)是樹規(guī)么?我們來想這樣一組數(shù)據(jù),1依賴2,2依賴3,3依賴1。這樣符合題目要求,但有形成了環(huán),所以不是一棵樹了。但是根據(jù)題目,這樣特殊的情況,要么全要,要么全就不要。所以,事實(shí)上我們可以將這個(gè)環(huán)看成一個(gè)點(diǎn)再來動規(guī),即縮點(diǎn)。如何判斷是否是一個(gè)環(huán)呢,依照數(shù)據(jù)范圍,我們想到了floyed(弗洛里德),這是在這種數(shù)據(jù)范圍內(nèi)性價(jià)比最高的方式。最后樹規(guī)。于是一個(gè)比較清晰的步驟就出來了:判環(huán),縮點(diǎn),樹規(guī)。
接下來是細(xì)節(jié):首先存樹,毫無疑問,是鄰接矩陣。
做floyed。如果兩點(diǎn)之間mapp[i][j]中有另一條路徑相連,即mapp[i][k]=1 && mapp[k][j]=1(1表示兩點(diǎn)是通的);那么mapp[i][j]也是通的且是環(huán)。
縮點(diǎn)。這個(gè)是最麻煩的,麻煩在于我們要把縮的點(diǎn)當(dāng)成一個(gè)新點(diǎn)來判斷,而且要判斷某個(gè)點(diǎn)是否在某個(gè)環(huán)里。我們用染色法來判斷,用所占的空間w控制顏色的對應(yīng),有以下三種情況:1、點(diǎn)i所在的環(huán)之前沒有判斷過,是新環(huán)。那么,我們將這個(gè)新環(huán)放到數(shù)組最后,即新加一個(gè)點(diǎn),然后讓這兩個(gè)點(diǎn)的空間標(biāo)記為負(fù)值tmpw,且tmpw+tmpn(新點(diǎn)的下標(biāo))等于原來的點(diǎn)數(shù),這樣,我們就可以通過某個(gè)點(diǎn)的空間迅速找到他所在的新點(diǎn)。像鑰匙一樣一一對應(yīng);2、點(diǎn)i所在的環(huán)之前已經(jīng)判斷過了,是舊環(huán)(已合成新點(diǎn)),且i是環(huán)的一部分。那么我們就把i也加到這個(gè)新點(diǎn)里面,即體積,價(jià)值相加即可;3、點(diǎn)j所在的環(huán)是舊環(huán),但是i不是環(huán)的一部分(例如1依賴2,2依賴3,3依賴1。4也依賴1,那么,4所在的是個(gè)環(huán),但4不屬于環(huán)的一部分)。那么,把j的父親轉(zhuǎn)到新點(diǎn)上d[j]= n-w[d[j]]。
以上縮點(diǎn)的工作做完之后,剩下的就是一棵樹。就可以在這上面動規(guī)了:先將其轉(zhuǎn)換成一棵左孩子右兄弟的二叉樹,之后記憶化。i的孩子不取f[b[x]][k]=dfs(b[x],k);還是取:f[c[x]][y-i]=dfs(c[x],y-i); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?f[b[x]][i]=dfs(b[x],i); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?f[x][k]=max(f[x][k],v[x]+f[c[x]][y-i]+f[b[x]][i]);
最后答案是f[c[0]][m]。
【代碼】:
復(fù)制代碼
? 1 #include<iostream>
? 2 #include<iomanip>
? 3 #include<cstring>
? 4 #include<cmath>
? 5 #include<cstdio>
? 6 #include<cstdlib>
? 7 #include<string>
? 8 #include<memory>
? 9 #include<climits>
?10 #include<vector>
?11 #include<map>
?12 #include<queue>
?13 #include<algorithm>
?14 using namespace std;
?15?
?16 const int e=505;
?17 int n,m,tmpw=0,tmpn;
?18 int w[e]={0},v[e]={0},b[e]={0},c[e]={0},f[e][5*e]={0},d[e]={0};
?19 bool mapp[e][e]={0};
?20?
?21 void floride()
?22 {
?23 ? ? for(int i=1;i<=n;i++)//弗洛里德判斷是否有環(huán);
?24 ? ? ? ? for(int j=1;j<=n;j++)
?25 ? ? ? ? ? ? for(int k=1;k<=n;k++)
?26 ? ? ? ? ? ? ? ? if(mapp[k][i]==1 && mapp[i][j]==1)
?27 ? ? ? ? ? ? ? ? ? ? mapp[k][j]=1;
?28 }
?29?
?30?
?31 void merge()//合點(diǎn)
?32 {
?33 ? ? tmpn=n;
?34 ? ? for(int i=1;i<=tmpn;i++)
?35 ? ? ? ? for(int j=1;j<=tmpn;j++)
?36 ? ? ? ? {
?37 ? ? ? ? ? ? if(mapp[i][j]==1 && mapp[j][i]==1 && i!=j && w[i]>0 && w[j]>0)//如果是新環(huán);
?38 ? ? ? ? ? ? {
?39 ? ? ? ? ? ? ? ? tmpn++;
?40 ? ? ? ? ? ? ? ? v[tmpn]=v[i]+v[j];
?41 ? ? ? ? ? ? ? ? w[tmpn]=w[i]+w[j];
?42 ? ? ? ? ? ? ? ? tmpw--; ? ?w[i]=tmpw; ? ?w[j]=tmpw; ? ?//tmpw+tmpn永遠(yuǎn)等于最開始的n
?43 ? ? ? ? ? ? }
?44 ? ? ? ? ? ??
?45 ? ? ? ? ? ? //如果j依賴的點(diǎn)被合并(是舊環(huán)),且j在環(huán)里
?46 ? ? ? ? ? ? if(w[d[j]]<0 && w[j]>0 && mapp[j][d[j]]==1 && mapp[j][d[j]]==1) ? ?
?47 ? ? ? ? ? ? {
?48 ? ? ? ? ? ? ? ? w[n-w[d[j]]]+=w[j];
?49 ? ? ? ? ? ? ? ? v[n-w[d[j]]]+=v[j];
?50 ? ? ? ? ? ? ? ? w[j]=w[d[j]];
?51 ? ? ? ? ? ? }
?52 ? ? ? ? ? ??
?53 ? ? ? ? ? ? //如果j依賴的點(diǎn)在環(huán)里,但是j不在環(huán)里
?54 ? ? ? ? ? ? if(w[d[j]]<0 && w[j]>0)
?55 ? ? ? ? ? ? ? ? if((mapp[j][d[j]]==1 && mapp[d[j]][j]==0) || (mapp[j][d[j]]==0 && mapp[d[j]][j]==1))
?56 ? ? ? ? ? ? ? ? ? ? d[j]=n-w[d[j]];
?57 ? ? ? ? }
?58 }
?59?
?60 int ?dfs(int x,int k)
?61 {
?62 ? ? if(f[x][k]>0) ? ?return(f[x][k]);
?63 ? ? if(x==0 || k<=0) ? ?return(0);
?64 ? ? //不取x
?65 ? ? f[b[x]][k]=dfs(b[x],k);
?66 ? ? f[x][k]=f[b[x]][k];
?67 ? ? ? ? int y=k-w[x];
?68 ? ? ? ? for(int i=0;i<=y;i++)
?69 ? ? ? ? {
?70 ? ? ? ? ? ? f[c[x]][y-i]=dfs(c[x],y-i);
?71 ? ? ? ? ? ? f[b[x]][i]=dfs(b[x],i);
?72 ? ? ? ? ? ? f[x][k]=max(f[x][k],v[x]+f[c[x]][y-i]+f[b[x]][i]);
?73 ? ? ? ? }
?74 ? ? return(f[x][k]);
?75 }
?76?
?77?
?78?
?79 int main()
?80 {
?81 ? ? //freopen("in.in","r",stdin);
?82 ? ? cin>>n>>m;
?83 ? ? for(int i=1;i<=n;i++)
?84 ? ? ? ? scanf("%d",&w[i]);
?85 ? ? for(int i=1;i<=n;i++)
?86 ? ? ? ? scanf("%d",&v[i]);
?87 ? ? for(int i=1;i<=n;i++)
?88 ? ? {
?89 ? ? ? ? int a;
?90 ? ? ? ? scanf("%d",&a);
?91 ? ? ? ? d[i]=a;
?92 ? ? ? ? mapp[a][i]=1;
?93 ? ? }
?94 ? ??
?95 ? ? floride();
?96 ? ? merge();
?97 ? ??
?98 ? ? //多叉轉(zhuǎn)二叉
?99 ? ? for(int i=1;i<=tmpn;i++)
100 ? ? ? ? if(w[i]>0)
101 ? ? ? ? {
102 ? ? ? ? ? ? b[i]=c[d[i]];
103 ? ? ? ? ? ? c[d[i]]=i;
104 ? ? ? ? }
105 ? ? cout<<dfs(c[0],m);
106?
107 ? ??
108 ? ? fclose(stdin); ? ?fclose(stdout);
109 ? ? return 0;
110 }
復(fù)制代碼
?
【小結(jié)】:依賴問題的變化很多,比如基本樹規(guī),記錄路徑,有環(huán)等等。但是他們都有一些共同的特點(diǎn),比如說記憶化的方程差不多。根據(jù)題目,我們應(yīng)該會判斷給的數(shù)據(jù)是否是圖,是樹,能想出特殊情況。樹畢竟是圖的一種特殊形式,而二叉樹又是樹的一種特殊形式。如果能將一個(gè)問題由復(fù)雜向簡單轉(zhuǎn)換,那么我們不僅思路會清晰很多,代碼量也會少很多。下面一道題目,就是我們所說的很少見的根節(jié)點(diǎn)向葉子節(jié)點(diǎn)動規(guī)的問題。
?
【總結(jié)】:樹規(guī)是動態(tài)規(guī)劃的一種,它將樹和動態(tài)規(guī)劃很巧妙地結(jié)合在了一起。做樹規(guī)題目,不僅僅鍛煉了我們的代碼能力,而且加深了我們對動態(tài)規(guī)劃的理解。再次強(qiáng)調(diào),樹的遞歸定義使樹規(guī)多以記憶化的形式來寫,而由于樹的嚴(yán)格分層,使動規(guī)的階段自然就清晰了起來,多找一找父節(jié)點(diǎn)與子節(jié)點(diǎn)的關(guān)系,就是很可能是兩個(gè)階段之間的聯(lián)系。
總結(jié)
- 上一篇: 【树型DP】BZOJ1564 二叉查找树
- 下一篇: 最长不下降子序列的O(n^2)算法和O(