樹形dp 入門
今天學了樹形dp,發現樹形dp就是入門難一些,於是好心的我便立誌要發一篇樹形dp入門的博客了。
樹形dp的概念什麽的,相信大家都已經明白,這裏就不再多說。直接上例題。
一、常規樹形DP
P1352 沒有上司的舞會
題目描述
某大學有N個職員,編號為1~N。他們之間有從屬關系,也就是說他們的關系就像一棵以校長為根的樹,父結點就是子結點的直接上司。現在有個周年慶宴會,宴會每邀請來一個職員都會增加一定的快樂指數Ri,但是呢,如果某個職員的上司來參加舞會了,那麽這個職員就無論如何也不肯來參加舞會了。所以,請你編程計算,邀請哪些職員可以使快樂指數最大,求最大的快樂指數。
這是一道非常經典的樹形dp,首先分析題目,如果上司去了,那麽他的所有下司都不能去;如果上司不去,那麽他的所有下司去不去無所謂,但要取最大值。接下來就是狀態轉移方程:
我們設f[i][0]為i不去的情況,f[i][1]為i去的情況,那麽
f[i][0]+=max(f[i的下司][1],f[i的下司][0]);
f[i][1]+=f[i的下司]][0];
初始化,如果i去,那麽顯然f[i][1]=i的快樂指數,如果i不去那麽f[i][1]=0;
最後貼上代碼
1 #include<iostream> 2 #include<cstring> 3 #include<cmath> 4 #include<string> 5 #include<cstdio> 6 using namespace std;7 struct edge 8 { 9 int num,child[6001]; 10 }g[6001]; 11 int n; 12 int a[10000]; 13 int tree[10000]; 14 int f[6001][2]; 15 int aa,bb,root; 16 void dp(int t) 17 { 18 f[t][1]=a[t];//初始化 19 f[t][0]=0;//初始化 20 for(int i=1;i<=g[t].num;i++) 21 { 22 dp(g[t].child[i]); 23 f[t][0]+=max(f[g[t].child[i]][1],f[g[t].child[i]][0]);//狀態轉移 24 f[t][1]+=f[g[t].child[i]][0];//狀態轉移 25 } 26 } 27 int main() 28 { 29 cin>>n; 30 for(int i=1;i<=n;i++) 31 { 32 scanf("%d",&a[i]); 33 } 34 while(scanf("%d%d",&aa,&bb)) 35 { 36 if(aa==0&&bb==0) break; 37 g[bb].num++;//記錄孩子結點的個數 38 g[bb].child[g[bb].num]=aa;//儲存孩子結點 39 tree[aa]=bb;//記錄父親結點 40 } 41 root=1; 42 while(tree[root]) root++;//找出樹根 43 dp(root);//從樹根開始dp 44 int ans=max(f[root][1],f[root][0]); 45 cout<<ans; 46 return 0; 47 }
洛谷P2016 戰略遊戲
題目描述
Bob喜歡玩電腦遊戲,特別是戰略遊戲。但是他經常無法找到快速玩過遊戲的辦法。現在他有個問題。
他要建立一個古城堡,城堡中的路形成一棵樹。他要在這棵樹的結點上放置最少數目的士兵,使得這些士兵能了望到所有的路。
註意,某個士兵在一個結點上時,與該結點相連的所有邊將都可以被了望到。
請你編一程序,給定一樹,幫Bob計算出他需要放置最少的士兵.
和上個題一模一樣,就是初始化不同,上個題每個人都有不同的權值,而這個題每個人的權值都為1;
狀態轉移一模一樣 ;初始化,f[i][1]=1,f[i][1]=0;
最後上代碼
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<string> 5 #include<cstring> 6 using namespace std; 7 struct edge 8 { 9 int num,child[1501]; 10 }g[1501]; 11 int a[1501]; 12 int f[1501][2]; 13 int x,y; 14 int n; 15 void dp(int t) 16 { 17 f[t][1]=1;//初始化為1 18 f[t][0]=0; 19 for(int i=1;i<=g[t].num;i++) 20 { 21 dp(g[t].child[i]); 22 f[t][0]+=f[g[t].child[i]][1];//狀態轉移 23 f[t][1]+=min(f[g[t].child[i]][1],f[g[t].child[i]][0]);//狀態轉移 24 } 25 } 26 int main() 27 { 28 cin>>n; 29 for(int i=1;i<=n;i++) 30 { 31 scanf("%d",&x); 32 scanf("%d",&g[x].num);//記錄該結點的孩子個數 33 for(int j=1;j<=g[x].num;j++) 34 { 35 scanf("%d",&g[x].child[j]);//存儲該結點的孩子 36 a[j]=i;//記錄父親 37 } 38 } 39 int root=0; 40 while(a[root]) root++;//找出根節點 41 dp(root); 42 int ans=min(f[root][1],f[root][0]); 43 cout<<ans; 44 return 0; 45 }
洛谷P2458 [SDOI2006]保安站崗
題目描述
五一來臨,某地下超市為了便於疏通和指揮密集的人員和車輛,以免造成超市內的混亂和擁擠,準備臨時從外單位調用部分保安來維持交通秩序。
已知整個地下超市的所有通道呈一棵樹的形狀;某些通道之間可以互相望見。總經理要求所有通道的每個端點(樹的頂點)都要有人全天候看守,在不同的通道端點安排保安所需的費用不同。
一個保安一旦站在某個通道的其中一個端點,那麽他除了能看守住他所站的那個端點,也能看到這個通道的另一個端點,所以一個保安可能同時能看守住多個端點(樹的結點),因此沒有必要在每個通道的端點都安排保安。
編程任務:
請你幫助超市經理策劃安排,在能看守全部通道端點的前提下,使得花費的經費最少。
難度提升,如果看不懂可以先放一放
分析:
我們發現,要使所有點最終全部被覆蓋,那麽無非有3種狀態:
(以下全部都是對於要覆蓋任意一個以x為根的子樹來說的)
(其中我們設y節點為y的兒子,fa為x的父親)
1.x節點被自己覆蓋,即選擇x點來覆蓋x點
2.x節點被兒子y覆蓋,即選擇y點來覆蓋x點
3.x節點被父親fa覆蓋,即選擇fa點來覆蓋x點
借此三種狀態,我們可以設f[x][0/1/2]為讓以x為根的子樹中的節點全部被覆蓋,且x點的被覆蓋情況為1/2/3時的最小代價
為了方便,我們不妨設這三種情況分別為:
1.f[x][0]---對應上面的1
2.f[x][1]---對應上面的2
3.f[x][2]---對應上面的3
既然是DP,總是有轉移方程的,我們想一下dp方程要如何設計
設計狀態轉移方程:
(1):對應上面的1.
f[x][0]=∑ min(f[y][0],f[y][1],f[y][2]) + val[x]
其中val[x]是選擇x點的代價
我們很容易想到,在節點x被選擇之後,我們就可以無拘無束了(蛤?),也就是說對於x兒子節點y的狀態可以不去考慮,因為x節點被選擇之後y節點無論如何也會被覆蓋到,所以我們在兒子y的所有狀態裏取min,累加起來就行了
(2):對應上面的3(先講3,因為2比較難以理解,放到了後面)
f[x][2]=∑ min(f[y][0],f[y][1])
為什麽3情況對應的轉移方程要這樣寫呢?
我們不妨這樣理解,對於x節點我們讓它的父親節點fa覆蓋它,那麽根據我們的狀態設計,此時必須要滿足以x的兒子y為根的子樹之中所有點已經被覆蓋
那麽這時就轉化為一個子問題,要讓y子樹滿足條件,只有兩種決策:要麽y被y的兒子覆蓋,要麽被y自己覆蓋(即選擇y節點),只需要在y的這兩種狀態取min累加就可以了
(3):對應上面的2(DuangDuangDuang 敲黑板劃重點啦)
f[x][1]=∑ min(f[y][0],f[y][1]),如果選擇的全部都是f[y][1],要再加上min(f[y][0]-f[y][1])
到了這裏,我們就要回顧一下我們設計的dp狀態了:
設f[x][0/1/2]為讓以x為根的子樹中的節點全部被覆蓋,且x點的被覆蓋情況為1/2/3時的最小代價
先提示一下,如果你理解了下面,那麽本題是很簡單的。。如果你沒理解,就返回到這裏再看一遍吧,我就在這裏等著你
對於此時的狀態,f[x][1]代表對於節點x讓x被自己的兒子覆蓋,那麽和分析(2)一樣,都要先滿足此時以y的子樹已經滿足了條件,才能進行轉移,這就是前面那部分:∑ min(f[y][0],f[y][1])的來歷,那麽後面那一長串又是怎麽回事呢?
我們可以這樣理解,此時既然要保證x點是被自己的兒子覆蓋的,那麽如果此時y子樹已經滿足了全部被覆蓋,但是y此時被覆蓋的狀態卻是通過y節點自己的兒子達到的,那麽x就沒有被兒子y覆蓋到,那麽我們不妨推廣一下,如果x所有的兒子y所做的決策都不是通過選擇y點來滿足條件,那麽我們就必須要選擇x的一個子節點y,其中y滿足f[y][0]-f[y][1]最小,並把這個最小的差值累加到f[x][1]中去,這樣才能使得x點被自己的兒子覆蓋,狀態f[x][1]也才能合理地得到轉移
好了,如果你還是沒有太懂這個(3)的設計過程,請你回到之前再仔細看幾遍
如果你已經理解了上面,那麽恭喜你這個題,你已經A掉了
因為轉移方程既然有了,那麽我們就只需要最後的答案了
由於題目中沒有說這棵樹的根節點是哪個,所以你可以默認1就是根,或者開一個數組在加邊的時候記錄一下每個點的入度,最後沒有入度的點就是根(但好像沒有區別,畢竟我A掉了)
最後答案即為min(f[root][0],f[root][1])
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<string> 5 #include<cstring> 6 using namespace std; 7 struct edge 8 { 9 int num,child[6001]; 10 }g[6001]; 11 int a[6001]; 12 int dp[6001][3]; 13 int root[6001]; 14 int n,aa; 15 void dfs(int x) 16 { 17 dp[x][0]=a[x]; 18 int cc=1000000000; 19 int num=0; 20 for(int i=1;i<=g[x].num;i++) 21 { 22 dfs(g[x].child[i]); 23 int t=min(dp[g[x].child[i]][0],dp[g[x].child[i]][1]); 24 dp[x][0]+=min(t,dp[g[x].child[i]][2]); 25 dp[x][1]+=t; 26 dp[x][2]+=t; 27 if(dp[g[x].child[i]][0]<dp[g[x].child[i]][1]) num++; 28 else cc=min(cc,dp[g[x].child[i]][0]-dp[g[x].child[i]][1]); 29 } 30 if(num==0) dp[x][1]+=cc; 31 } 32 int main() 33 { 34 scanf("%d",&n); 35 for(int i=1;i<=n;i++) 36 { 37 scanf("%d",&aa); 38 scanf("%d",&a[aa]); 39 scanf("%d",&g[aa].num); 40 for(int j=1;j<=g[aa].num;j++) 41 { 42 scanf("%d",&g[aa].child[j]); 43 root[g[aa].child[j]]=aa; 44 } 45 } 46 aa=1; 47 while(root[aa]) aa++; 48 dfs(aa); 49 cout<<min(dp[aa][0],dp[aa][1]); 50 return 0; 51 }
二、樹形背包問題(在樹上進行分組背包處理)
未完待續~~~
樹形dp 入門