1. 程式人生 > >樹形dp 入門

樹形dp 入門

include 過程 推廣 因此 pro 哪些 ati cout cst

今天學了樹形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 入門