轉自(http://www.cnblogs.com/gq-ouyang/archive/2013/02/26/2933431.html)
不撞南牆不回頭——樹規總結
焦作一中信息學oy
之所以這樣命名樹規,是因爲樹規的這一特殊性:沒有環,dfs是不會重複,而且具有明顯而又嚴格的層數關係。利用這一特性,我們可以很清晰地根據題目寫出一個在樹(型結構)上的記憶化搜索的程序。而深搜的特點,就是“不撞南牆不回頭”。這一點在之後的文章中會詳細的介紹。
首先是掃盲,介紹幾條名詞的專業解釋以顯示我的高端(大部分人可以略過,因爲學習到樹規的人一下應該都懂……):
動態規劃:
問題可以分解成若干相互聯繫的階段,在每一個階段都要做出決策,全部過程的決策是一個決策序列。要使整個活動的總體效果達到最優的問題,稱爲多階段決策問題。動態規劃就是解決多階段決策最優化問題的一種思想方法。
階段:
將所給問題的過程,按時間或空間(樹歸中是空間,即層數)特徵分解成若干相互聯繫的階段,以便按次序去求每階段的解。
狀態:
各階段開始時的客觀條件叫做狀態。
決策:
當各段的狀態取定以後,就可以做出不同的決定,從而確定下一階段的狀態,這種決定稱爲決策。 (即孩子節點和父親節點的關係)
策略:
由開始到終點的全過程中,由每段決策組成的決策序列稱爲全過程策略,簡稱策略。
狀態轉移方程:
前一階段的終點就是後一階段的起點,前一階段的決策選擇導出了後一階段的狀態,這種關係描述了由k階段到k+1階段(在樹中是孩子節點和父親節點)狀態的演變規律,稱爲狀態轉移方程。
目標函數與最優化概念:
目標函數是衡量多階段決策過程優劣的準則。最優化概念是在一定條件下找到一個途徑,經過按題目具體性質所確定的運算以後,使全過程的總效益達到最優。
樹的特點與性質:
1、 有n個點,n-1條邊的無向圖,任意兩頂點間可達
2、 無向圖中任意兩個點間有且只有一條路
3、 一個點至多有一個前趨,但可以有多個後繼
4、 無向圖中沒有環;
廢話說完了,下面是正文:
拿到一道樹規題,我們有以下3個步驟需要執行:
- 判斷是否是一道樹規題:即判斷數據結構是否是一棵樹,然後是否符合動態規劃的要求。如果是,那麼執行以下步驟,如果不是,那麼換臺。
- 建樹:通過數據量和題目要求,選擇合適的樹的存儲方式。如果節點數小於5000,那麼我們可以用鄰接矩陣存儲,如果更大可以用鄰接表來存儲(注意邊要開到2*n,因爲是雙向的。這是血與淚的教訓)。如果是二叉樹或者是需要多叉轉二叉,那麼我們可以用兩個一維數組brother[],child[]來存儲(這一點下面會仔細數的)。
- 寫出樹規方程:通過觀察孩子和父親之間的關係建立方程。我們通常認爲,樹規的寫法有兩種:
a.根到葉子: 不過這種動態規劃在實際的問題中運用的不多。本文只有最後一題提到。
b.葉子到根: 既根的子節點傳遞有用的信息給根,完後根得出最優解的過程。這類的習題比較的多。
注意:這兩種寫法一般情況下是不能相互轉化的。但是有時可以同時使用具體往後看。
以下即將分析的題目的目錄及題目特點:
1、加分二叉樹:區間動規+樹的遍歷;
2、二叉蘋果樹:二叉樹上的動規;
3、最大利潤:多叉樹上的動規;
4、選課:多叉樹轉二叉;
5、選課(輸出方案):多叉轉二叉+記錄路徑;
6、軟件安裝:判斷環+縮點+多叉轉二叉;
【4、5、6屬於依賴問題的變形】
基本的知識掌握和步驟了,我們就通過習題來感受一下樹規的魅力,先來看這樣一道題:
1、加分二叉樹
【問題描述】
設一個n個節點的二叉樹tree的中序遍歷爲(l,2,3,…,n),其中數字1,2,3,…,n爲節點編號。每個節點都有一個分數(均爲正整數),記第i個節點的分數爲di,tree及它的每個子樹都有一個加分,任一棵子樹subtree(也包含tree本身)的加分計算方法如下:
subtree的左子樹的加分× subtree的右子樹的加分+subtree的根的分數
若某個子樹爲空,規定其加分爲1,葉子的加分就是葉節點本身的分數。不考慮它的空子樹。
試求一棵符合中序遍歷爲(1,2,3,…,n)且加分最高的二叉樹tree。要求輸出;
(1)tree的最高加分
(2)tree的前序遍歷
【輸入格式】
第1行:一個整數n(n<30),爲節點個數。
第2行:n個用空格隔開的整數,爲每個節點的分數(分數<100)。
【輸出格式】
第1行:一個整數,爲最高加分(結果不會超過4,000,000,000)。
第2行:n個用空格隔開的整數,爲該樹的前序遍歷。
【算法&思路】:
看到這個問題,我們首先應該想到的是這道題是否屬於動態規劃,而這裏我們發現,結合問題,如果整棵樹的權值最大,必然有左子樹的權值最大,右子樹的權值也最大,符合最優性原理。所以是動態規劃。而卻不是一道樹規的題目。因爲我們可以用區間動規的模型解決掉:直接定義一個f[i][j]表示從i到j的最大值,則f[i][j]=max(f[i][k-1]*f[k+1][j]+a[k]),枚舉k即可。接下來是如何建樹的問題,只有把樹建好了,才能輸出其前序遍歷。於是,我們看到了兩個關鍵詞:二叉樹,中序遍歷。有了這兩個關鍵詞,加上區間動規,這棵樹就能建起來了。根據二叉樹的特性來建樹(這裏不再具體討論樹的詳細的構造了,中序遍歷和前序遍歷不懂得自己百度)。所以這顆樹的前序遍歷,只需要邊動規邊記錄下root[i][j]=k表示i到j的根爲k即可確定樹的構造。
【代碼】:
1 #include<iostream> 2 #include<iomanip> 3 #include<cstring> 4 #include<cstdio> 5 #include<cmath> 6 #include<memory> 7 #include<algorithm> 8 #include<string> 9 #include<climits> 10 #include<queue> 11 #include<vector> 12 #include<cstdlib> 13 #include<map> 14 using namespace std; 15 16 const int ee=50,e=-999999999; 17 int n; 18 int a[ee]={0},f[ee][ee],root[ee][ee]={0};//f(i,j)中序遍歷爲i,i+1,…,j的二叉樹的最大加分 19 20 //**若根節點的下表是k,則左端點的是k-1,右端點是k+1; 21 void front(int x,int y) 22 { 23 if(root[x][y]!=0) 24 cout<<root[x][y]<<' '; 25 if(root[x][root[x][y]-1]!=0) front(x,root[x][y]-1); 26 if(root[root[x][y]+1][y]!=0) front(root[x][y]+1,y); 27 } 28 29 int main() 30 { 31 //freopen("in.in","r",stdin); 32 33 34 //memset 賦初值不能爲1 memset(f,1,sizeof(f)); 35 cin>>n; 36 37 for(int i=0;i<=n;i++) 38 { 39 for(int j=0;j<=n;j++) 40 f[i][j]=1; 41 } 42 for(int i=1;i<=n;i++) 43 { 44 cin>>a[i]; 45 f[i][i]=a[i]; 46 root[i][i]=i; 47 } 48 //區間長度 49 for(int len=1;len<=n;len++) 50 { 51 //區間起點 52 for(int i=1;i<=n;i++) 53 { 54 //終點 55 int j=i+len; 56 if(j<=n) 57 { 58 int temp=e; 59 //因爲是中序排列 60 for(int k=i;k<=j;k++) 61 { 62 if(temp < (f[i][k-1]*f[k+1][j]+a[k])) 63 { 64 temp=f[i][k-1]*f[k+1][j]+a[k]; 65 root[i][j]=k; 66 } 67 } 68 f[i][j]=temp; 69 } 70 } 71 } 72 cout<<f[1][n]; 73 74 //前序遍歷 75 cout<<endl; 76 front(1,n); 77 78 79 fclose(stdin);fclose(stdout); 80 return 0; 81 }
【小結】:拿到一道題目,首先我們要做的是看清題目,判斷這是一道考察什麼算法的題目。只有建立在正確思路基礎下的算法,纔是有意義的,正確的算法,也是事半功倍的算法。而此題是批着 樹形 外觀的 非樹形動態規劃題。而真正的樹形動態規劃是在樹上做動態規劃。
真正的樹規來了。
2、二叉蘋果樹
【題目描述】:
有一棵蘋果樹,如果樹枝有分叉,一定是分2叉(就是說沒有隻有1個兒子的結點)這棵樹共有N個結點(葉子點或者樹枝分叉點),編號爲1-N,樹根編號一定是1。我們用一根樹枝兩端連接的結點的編號來描述一根樹枝的位置。現在這顆樹枝條太多了,需要剪枝。但是一些樹枝上長有蘋果。
給定需要保留的樹枝數量,求出最多能留住多少蘋果。
【輸入格式】:
第1行2個數,N和Q(1<=Q<= N,1<N<=100)。
N表示樹的結點數,Q表示要保留的樹枝數量。接下來N-1行描述樹枝的信息。
每行3個整數,前兩個是它連接的結點的編號。第3個數是這根樹枝上蘋果的數量。
每根樹枝上的蘋果不超過30000個。
【輸出格式】:
剩餘蘋果的最大數量。
input
5 2
1 3 1
1 4 10
2 3 20
3 5 20
output
21
【算法&思路】:首先,可以肯定的是,這是一道有關樹規的題目,父節點和子節點存在着相互關聯的階段關係。
第一步完成。再執行第二步:我們觀察到題目數據量不大,所以有兩種選擇:鄰接矩陣和鄰接表。因爲鄰接矩陣的代碼簡單,思路清晰,所以建議能寫鄰接矩陣的時候就不要寫鄰接表了。我們設ma[x][y]爲邊的值,因爲樹是雙向的,所以要再記錄ma[y][x]。
設tree[v,1]爲節點v的左子樹,tree[v,2]爲節點v的右子樹,然後我們再遞歸建樹(因爲樹是遞歸定義的,所以很多時候建樹都要考慮遞歸)。
建樹的問題解決的了,我們就要列狀態轉移方程了。根據求什麼設什麼的原則,我們定義f[i][j]表示以i爲節點的根保留k條邊的最大值,那麼f[v][k]=max(f[v][k],(f[tree[v][1]][i]+f[tree[v][2]][k-i-1]+num[v])),我們枚舉i就可以了。正如我開頭提到的。因爲樹是遞歸定義的所以我們可以用記憶化搜索的形式(dfs)來具體實現。而樹本身嚴格分層,而且沒有環。所以是不會重複的。
F[1][Q+1]就是答案。因爲題目中給的是邊的權值,而我們在處理時將每條邊的權值全賦給其所連的父節點和子節點中的子節點(將關於邊的問題轉化爲關於點的問題),所以最後是Q+1,表示點的數目。
【代碼】:
1 #include<iostream> 2 #include<iomanip> 3 #include<cstring> 4 #include<cstdio> 5 #include<cmath> 6 #include<memory> 7 #include<algorithm> 8 #include<string> 9 #include<climits> 10 #include<queue> 11 #include<vector> 12 #include<cstdlib> 13 #include<map> 14 using namespace std; 15 16 const int ee=105; 17 int n,q; 18 int tree[ee][5]={0},ma[ee][ee]={0},num[ee]={0},f[ee][ee]={0}; 19 20 void preproccess() 21 { 22 for(int i=0;i<=n;i++) 23 for(int j=0;j<=n;j++) 24 { 25 ma[i][j]=-1; 26 ma[j][i]=-1; 27 } 28 } 29 30 void maketree(int v); 31 32 void build(int x,int y,int lor)//lor means left or right 33 { 34 num[y]=ma[x][y]; 35 tree[x][lor]=y; 36 ma[x][y]=-1;ma[y][x]=-1; 37 maketree(y); 38 } 39 40 void maketree(int v) 41 { 42 int lr=0; 43 for(int i=0;i<=n;i++) 44 if(ma[v][i]>=0)//如果分叉了,那麼記錄 45 { 46 lr++;//1 or 2 表示左支還是右支; 47 build(v,i,lr);//存入並遞歸 48 if(lr==2) return; 49 } 50 } 51 52 void dfs(int v,int k) 53 { 54 if(k==0) f[v][k]=0; 55 else if(tree[v][1]==0 && tree[v][2]==0) f[v][k]=num[v]; 56 else 57 { 58 f[v][k]=0; 59 for(int i=0;i<k;i++) 60 { 61 if(f[tree[v][1]][i]==0) dfs(tree[v][1],i); 62 if(f[tree[v][2]][k-i-1]==0) dfs(tree[v][2],k-i-1); 63 f[v][k]=max(f[v][k],(f[tree[v][1]][i]+f[tree[v][2]][k-i-1]+num[v])); 64 } 65 } 66 } 67 68 int main() 69 { 70 //freopen("in.txt","r",stdin); 71 72 cin>>n>>q; 73 preproccess(); 74 75 for(int i=0;i<n;i++) 76 { 77 int x,y,xy; 78 scanf("%d%d%d",&x,&y,&xy); 79 ma[x][y]=xy; 80 ma[y][x]=xy; 81 } 82 83 //建樹; 84 maketree(1); 85 86 dfs(1,q+1); 87 88 cout<<f[1][q+1]; 89 90 fclose(stdin);fclose(stdout); 91 return 0; 92 }
【小結】:在樹的存儲結構上,我們一般選的都是二叉樹,因爲二叉樹可以用靜態數組來存儲,並且狀態轉移也很好寫(根節點只和左子節點和右子節點有關係)。
可如果是多叉怎麼辦? 往下看。
3、最大利潤
【題目描述】
政府邀請了你在火車站開飯店,但不允許同時在兩個相連接的火車站開。任意兩個火車站有且只有一條路徑,每個火車站最多有50個和它相連接的火車站。
告訴你每個火車站的利潤,問你可以獲得的最大利潤爲多少。
最佳投資方案是在1,2,5,6這4個火車站開飯店可以獲得利潤爲90
【輸入格式】
第一行輸入整數N(<=100000),表示有N個火車站,分別用1,2。。。,N來編號。接下來N行,每行一個整數表示每個站點的利潤,接下來N-1行描述火車站網絡,每行兩個整數,表示相連接的兩個站點。
【輸出格式】
輸出一個整數表示可以獲得的最大利潤。
【樣例輸入】
6 10
20
25
40
30
30
4 5
1 3
3 4
2 3
6 4
【樣例輸出】
90
【算法&思路】:
按照上一題的步驟,我們再來分析一遍:一、是否是動態規劃。這時可能很多人已經吐槽了:閉着眼都知道是動態規劃,不然你粘出來幹什麼??呵呵,沒錯,確實是。但是爲什麼是呢??首先,這是棵樹,是一棵多叉樹。其次,當我們嘗試着把他向動態規劃上靠時,我們發現當前節點只與其孩子節點的孩子節點(這裏沒打錯,因爲隔一個火車站)有關係。所以綜上所述,是動規,還是一個樹規,一個不折不扣的樹規!
接下來,第二步建樹。看範圍和題目發現,這是一個有着n(<100000)的多叉樹,所以只能用鄰接表存儲了。沒有根,我們一般通常指定1爲根。
第三步:F[i]表示i這條根要,G[i]表示不要(也可以用f[i][1,0]來表示)。然後以此枚舉i的孩子:如果i要了那麼i的孩子就不能要,如果i不要i的孩子就可要可不要(取最大值)即可。最後輸出max(f[1],g[1]);
【代碼】:
1 #include<iostream> 2 #include<iomanip> 3 #include<cstring> 4 #include<cstdio> 5 #include<cmath> 6 #include<memory> 7 #include<algorithm> 8 #include<string> 9 #include<climits> 10 #include<queue> 11 #include<vector> 12 #include<cstdlib> 13 #include<map> 14 using namespace std; 15 16 const int e=100020; 17 int f[e]={0},g[e]={0},a[e]={0},link[e]={0};//f[i]表示這條根要,g【i】表示不要; 18 int n,t=0; 19 20 struct qq 21 { 22 int y,next; 23 }ee[2*e]; 24 25 void insert(int startt,int endd) //臨界表存儲樹 26 { 27 ee[++t].y=endd; ee[t].next=link[startt]; 28 link[startt]=t; 29 } 30 31 void dfs(int root,int father) 32 { 33 int tempf=0,tempg=0; 34 for(int i=link[root];i;i=ee[i].next) 35 { 36 if(ee[i].y!=father) 37 { 38 dfs(ee[i].y,root); 39 f[root]+=g[ee[i].y]; 40 g[root]+=max(f[ee[i].y],g[ee[i].y]); 41 } 42 } 43 f[root]+=a[root]; 44 } 45 46 int main() 47 { 48 //freopen("in.txt","r",stdin); 49 cin>>n; 50 for(int i=1;i<=n;i++) 51 scanf("%d",&a[i]); 52 53 for(int i=1;i<n;i++) 54 { 55 int xx,yy; 56 scanf("%d%d",&xx,&yy); 57 insert(xx,yy); 58 insert(yy,xx); 59 } 60 61 dfs(1,1); 62 63 cout<<max(f[1],g[1]); 64 65 fclose(stdin);fclose(stdout); 66 return 0; 67 }
【小結】:無論是多叉樹還是二叉樹,只要我們把樹以正確的形式建立起來,那麼我們再根據建樹的形式和題目要求,找出孩子和父親之間的關係,那麼狀態轉移方程很容易就求解出來了。多叉其實也不是很難。對麼?呵呵,那麼再看下面一道題:
4、選課
【題目描述】
學校實行學分制。每門的必修課都有固定的學分,同時還必須獲得相應的選修課程學分。學校開設了N(N<300)門的選修課程,每個學生可選課程的數量M是給定的。學生選修了這M門課並考覈通過就能獲得相應的學分。
在選修課程中,有些課程可以直接選修,有些課程需要一定的基礎知識,必須在選了其它的一些課程的基礎上才能選修。例如《Frontpage》必須在選修了《Windows操作基礎》之後才能選修。我們稱《Windows操作基礎》是《Frontpage》的先修課。每門課的直接先修課最多隻有一門。兩門課也可能存在相同的先修課。每門課都有一個課號,依次爲1,2,3,…。
你的任務是爲自己確定一個選課方案,使得你能得到的學分最多,並且必須滿足先修課優先的原則。假定課程之間不存在時間上的衝突。
【輸入格式 】Input Format |
|
|||
|
輸入文件的第一行包括兩個整數N、M(中間用一個空格隔開),其中1≤N≤300,1≤M≤N。 |
|||
|
|
|
||
|
|
|
||
|
【輸出格式】 Output Format |
|
||
|
只有一個數:實際所選課程的學分總數。 |
【算法&思路】:
繼續照着三步的方法判斷:一,題目大致一看,有點像有依賴的揹包問題,於是你扭頭就走,關掉了我的《樹規》,打開了崔神犇的《揹包九講》。然後你哭了,因爲有依賴的揹包問題只限定於一個物品只依賴於一個物品,而沒有間接的依賴關係。有依賴的揹包問題的模型,根本解決不了。崔神告訴你,這屬於樹規的問題,不屬於他揹包的範圍了。好了,回過來,我們接着分析。發現這是一棵樹,還是一棵多叉樹,嗯,很好,確定是樹規了。
然後第二步,建樹,一看數據範圍鄰接矩陣;
第三步動規方程:f[i][j]表示以i爲節點的根的選j門課的最大值,然後有兩種情況: i不修,則i的孩子一定不修,所以爲0;i修,則i的孩子們可修可不修(在這裏其實可以將其轉化爲將j-1個對i的孩子們進行資源分配的問題,也屬於揹包問題);答案是f[1][m]。問題圓滿解決,一氣呵成。
但……
身爲追求完美的苦*程序猿的我們,不可以將它更簡單一點呢?
多叉轉二叉。
因爲之前我們說過“在樹的存儲結構上,我們一般選的都是二叉樹,因爲二叉樹可以用靜態數組來存儲,並且狀態轉移也很好寫(根節點只和左子節點和右子節點有關係)。”所以轉換成二叉樹無疑是一種不錯的選擇。
我們開兩個一維數組,b[i](brother)&c[i](child)分別表示節點i的孩子和兄弟,以左孩子和右兄弟的二叉樹的形式存儲這樣,根節點之和兩個節點有關係了,狀態轉移的關係少了,代碼自然也就好寫了。
我們依舊f[i][j]表示以i爲節點的根的選j門課的最大值,那麼兩種情況:1.根節點不選修則f[i][j]=f[b[i]][j];2.根節點選修f[i][j]=f[c[i]][k]+f[b[i]][j-k-1]+a[i](k表示左孩子學了k種課程);取二者的最大值即可。
【代碼】:
1 #include<iostream> 2 #include<iomanip> 3 #include<cstring> 4 #include<cstdio> 5 #include<cmath> 6 #include<memory> 7 #include<algorithm> 8 #include<string> 9 #include<climits> 10 #include<queue> 11 #include<vector> 12 #include<cstdlib> 13 #include<map> 14 using namespace std; 15 16 const int e=320; 17 int n,m; 18 int c[e]={0},b[e]={0},s[e]={0},f[e][e]={0};//c[]:means child ; b[]:means brother 19 20 void maketree()//多叉轉二叉 21 { 22 cin>>n>>m; 23 for(int i=1;i<=n;i++) 24 { 25 int ta,tb; 26 scanf("%d%d",&ta,&tb); 27 s[i]=tb; 28 if(ta==0) ta=n+1; 29 b[i]=c[ta]; 30 c[ta]=i; 31 } 32 } 33 34 void dfs(int x,int y) 35 { 36 if(f[x][y]>=0) return; 37 if(x==0 || y==0) { f[x][y]=0;return;} 38 dfs(b[x],y); 39 //max()f[b[x]][y]; 40 for(int k=0;k<y;k++) 41 { 42 dfs(b[x],k); //不取根節點 43 dfs(c[x],y-k-1);//取根節點 44 f[x][y]=max(f[x][y] , max(f[b[x]][y] , f[b[x]][k]+f[c[x]][y-k-1]+s[x])); 45 } 46 //cout<<x<<' '<<y<<' '<<f[x][y]<<endl; 47 return; 48 } 49 50 int main() 51 { 52 //freopen("ni.txt","r",stdin); 53 54 memset(f,-1,sizeof(f)); 55 56 maketree(); 57 dfs(c[n+1],m); 58 59 cout<<f[c[n+1]][m]<<endl; 60 61 fclose(stdin);fclose(stdout); 62 return 0; 63 }
【小結】:當題目中的數據結構是多叉樹的時候,我們有兩種選擇:直接在多叉樹上動規,或者轉化爲二叉樹後動規。毫無疑問,二叉樹上的動規是簡潔的。但是,並不是說所有的多叉樹都需要轉化,一般情況下,當根節點與孩子節點有着必然的關係時纔會轉化。這需要我們多做題目,增加對樹規的感覺才能遊刃有餘。
我們繼續擴展:如果上一道題目繼續提問,要求輸出所選的方案呢?
5、選課(輸出方案)
【題目描述】同上。
【輸入格式】同上。
【輸出格式】 Output Format |
|
|
|
第一行只有一個數,即實際所選課程的學分總數。 |
|
【算法&思路】:拿到這道題目,首先我們必然要和上一道題目做一下對比。對比後我們發現,這道題目和上第一道題目完全一樣,除了問題比上一題多一問:輸出方案。所以,我們可以把這道題目分成兩部分:求總數和輸出方案。而求總數的問題我們在上一題中已經很好的解決了,所以這道題目重點是考察的是樹的路徑記錄的問題。
既然數是遞歸定義的,所以我們依舊使用遞歸的形式來記錄路徑:使用一個bool數組ans來進行遞歸,分兩種情況:取(1)和不取(0)。然後,我們繼續利用已經求得的f[i][j]的值來思考如何找到路徑:首先定義一個path()函數。如果f[i][j]=f[b[i]][j],那麼節點i必然沒有取,讓ans[i]=0;否則,節點i一定取到了。(爲什麼呢?其實,這是依照第一問的dfs來思考的,第一問的dfs是這樣定義的,所以我們就這樣考慮了。)然後依照上一問,if(f[x][y]==f[b[x]][k-1]+f[c[x]][y-k]+s[x]),那麼我們在i節點後選的一定是以上的方案,在這時讓ans[i]=1,繼續深搜path()即可。最後從1到n依次輸出取到的點即可。
1 #include<iostream> 2 #include<iomanip> 3 #include<cmath> 4 #include<algorithm> 5 #include<cstring> 6 #include<string> 7 #include<cstdio> 8 #include<cstdlib> 9 #include<map> 10 #include<vector> 11 #include<queue> 12 #include<memory> 13 #include<climits> 14 using namespace std; 15 16 const int e=520; 17 int c[e]={0},b[e]={0},f[e][e]={0},s[e]={0}; 18 bool ans[e]={0}; 19 int n,m; 20 21 void maketree() 22 { 23 cin>>n>>m; 24 for(int i=1;i<=n;i++) 25 { 26 int ta,tb; 27 scanf("%d%d",&ta,&tb); 28 s[i]=tb; 29 b[i]=c[ta]; 30 c[ta]=i; 31 } 32 } 33 34 void dfs(int x,int y)//以x爲節點,取y個 35 { 36 if(f[x][y]>=0) return; 37 if(x==0 || y==0) 38 { 39 f[x][y]=0; 40 return; 41 } 42 else dfs(b[x],y); 43 int tm=f[b[x]][y],tp=0; 44 45 for(int k=1;k<=y;k++) 46 { 47 dfs(b[x],k-1); 48 dfs(c[x],y-k); 49 tp=max(tp,f[b[x]][k-1]+f[c[x]][y-k]+s[x]); 50 } 51 52 if(tp>tm) f[x][y]=tp; 53 else f[x][y]=tm; 54 55 return; 56 } 57 58 void path(int x,int y) 59 { 60 if(x==0 || y==0) 61 return; 62 if(f[x][y]==f[b[x]][y]) path(b[x],y); 63 else 64 { 65 for(int k=1;k<=y;k++) 66 { 67 if(f[x][y]==f[b[x]][k-1]+f[c[x]][y-k]+s[x]) 68 { 69 path(b[x],k-1); 70 path(c[x],y-k); 71 ans[x]=1; 72 return; 73 } 74 } 75 } 76 } 77 78 int main() 79 { 80 //freopen("in.txt","r",stdin); 81 82 memset(f,-1,sizeof(f)); 83 84 maketree(); 85 dfs(c[0],m); 86 87 cout<<f[c[0]][m]<<endl; 88 path(c[0],m); 89 90 for(int i=1;i<=n;i++) 91 if(ans[i]) cout<<i<<endl; 92 93 return 0; 94 }
【小結】:路徑輸出的問題,在不同的題目中有不同的解法,比如說邊求值邊記錄等等,而在樹規中,利用動規和樹的特點,求解完後再原路返回找一遍,是一種比較容易想到且容易操作的性價比比較高的算法。
6、軟件安裝
【題目描述】:
現在我們的手頭有N個軟件,對於一個軟件i,它要佔用Wi的磁盤空間,它的價值爲Vi。我們希望從中選擇一些軟件安裝到一臺磁盤容量爲M的計算機上,使得這些軟件的價值儘可能大(即Vi的和最大)。
但是現在有個問題:軟件之間存在依賴關係,即軟件i只有在安裝了軟件j(包括軟件j的直接或間接依賴)的情況下才能正確工作(軟件i依賴軟件j)。幸運的是,一個軟件最多依賴另外一個軟件。如果一個軟件不能正常工作,那麼他能夠發揮的作用爲0。
我們現在知道了軟件之間的依賴關係:軟件i依賴Di。現在請你設計出一種方案,安裝價值儘量大的軟件。一個軟件只能被安裝一次,如果一個軟件沒有依賴則Di=0,這是隻要這個軟件安裝了,它就能正常工作。
【輸入格式】:
第1行:N,M (0<=N<=100,0<=M<=500)
第2行:W1,W2, … Wi, … ,Wn
第3行:V1,V2, … Vi, … ,Vn
第4行:D1,D2, … Di, … ,Dn
【輸出格式】:
一個整數,代表最大價值。
【樣例】
3 10
5 5 6
2 3 4
0 1 1
5
【算法&思路】:同樣,這道題目類似與第4題,是一個依賴的問題,毫無疑問是一道動態規劃,但是它確實是樹規麼?我們來想這樣一組數據,1依賴2,2依賴3,3依賴1。這樣符合題目要求,但有形成了環,所以不是一棵樹了。但是根據題目,這樣特殊的情況,要麼全要,要麼全就不要。所以,事實上我們可以將這個環看成一個點再來動規,即縮點。如何判斷是否是一個環呢,依照數據範圍,我們想到了floyed(弗洛裏德),這是在這種數據範圍內性價比最高的方式。最後樹規。於是一個比較清晰的步驟就出來了:判環,縮點,樹規。
接下來是細節:首先存樹,毫無疑問,是鄰接矩陣。
做floyed。如果兩點之間mapp[i][j]中有另一條路徑相連,即mapp[i][k]=1 && mapp[k][j]=1(1表示兩點是通的);那麼mapp[i][j]也是通的且是環。
縮點。這個是最麻煩的,麻煩在於我們要把縮的點當成一個新點來判斷,而且要判斷某個點是否在某個環裏。我們用染色法來判斷,用所佔的空間w控制顏色的對應,有以下三種情況:1、點i所在的環之前沒有判斷過,是新環。那麼,我們將這個新環放到數組最後,即新加一個點,然後讓這兩個點的空間標記爲負值tmpw,且tmpw+tmpn(新點的下標)等於原來的點數,這樣,我們就可以通過某個點的空間迅速找到他所在的新點。像鑰匙一樣一一對應;2、點i所在的環之前已經判斷過了,是舊環(已合成新點),且i是環的一部分。那麼我們就把i也加到這個新點裏面,即體積,價值相加即可;3、點j所在的環是舊環,但是i不是環的一部分(例如1依賴2,2依賴3,3依賴1。4也依賴1,那麼,4所在的是個環,但4不屬於環的一部分)。那麼,把j的父親轉到新點上d[j]= n-w[d[j]]。
以上縮點的工作做完之後,剩下的就是一棵樹。就可以在這上面動規了:先將其轉換成一棵左孩子右兄弟的二叉樹,之後記憶化。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]。
【代碼】:
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++)//弗洛裏德判斷是否有環; 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()//合點 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)//如果是新環; 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永遠等於最開始的n 43 } 44 45 //如果j依賴的點被合併(是舊環),且j在環裏 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依賴的點在環裏,但是j不在環裏 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 //多叉轉二叉 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 }
【小結】:依賴問題的變化很多,比如基本樹規,記錄路徑,有環等等。但是他們都有一些共同的特點,比如說記憶化的方程差不多。根據題目,我們應該會判斷給的數據是否是圖,是樹,能想出特殊情況。樹畢竟是圖的一種特殊形式,而二叉樹又是樹的一種特殊形式。如果能將一個問題由複雜向簡單轉換,那麼我們不僅思路會清晰很多,代碼量也會少很多。下面一道題目,就是我們所說的很少見的根節點向葉子節點動規的問題。
【總結】:樹規是動態規劃的一種,它將樹和動態規劃很巧妙地結合在了一起。做樹規題目,不僅僅鍛鍊了我們的代碼能力,而且加深了我們對動態規劃的理解。再次強調,樹的遞歸定義使樹規多以記憶化的形式來寫,而由於樹的嚴格分層,使動規的階段自然就清晰了起來,多找一找父節點與子節點的關係,就是很可能是兩個階段之間的聯繫。