1. 程式人生 > >[提升性選講] 樹形DP進階:一類非線性的樹形DP問題(例題 BZOJ4403 BZOJ3167)

[提升性選講] 樹形DP進階:一類非線性的樹形DP問題(例題 BZOJ4403 BZOJ3167)

mit n-k add get cst -- log des 技術分享

轉載請註明原文地址:http://www.cnblogs.com/LadyLex/p/7337179.html

樹形DP是一種在樹上進行的DP相對比較難的DP題型.由於狀態的定義多種多樣,因此解法也五花八門,經常成為高水平考試的考點之一.

在樹形DP的問題中,有這樣一類問題:其數據範圍相對較小,並且狀態轉移一般與兩兩節點之間的某些關系有關。

今天,我們就來研究一下這類型的問題,並且總結一種(相對套路的)解決大多數類型題的思路。

首先,我們用一道相對簡單的例題來初步了解這個類型題的大致思路,以及一些基本的代碼實現

BZOJ 4033: [HAOI2015]樹上染色

Time Limit: 10 Sec Memory Limit: 256 MB

Description

有一棵點數為N的樹,樹邊有邊權。給你一個在0~N之內的正整數K,你要在這棵樹中選擇K個點,將其染成黑色,並 將其他的N-K個點染成白色。將所有點染色後,你會獲得黑點兩兩之間的距離加上白點兩兩之間距離的和的收益。 問收益最大值是多少。

Input

第一行兩個整數N,K。 接下來N-1行每行三個正整數fr,to,dis,表示該樹中存在一條長度為dis的邊(fr,to)。 輸入保證所有點之間是聯通的。 N<=2000,0<=K<=N

Output

輸出一個正整數,表示收益的最大值。

Sample Input

5 2
1 2 3
1 5 1
2 3 1
2 4 2

Sample Output

17
【樣例解釋】
將點1,2染黑就能獲得最大收益。
看完這道題,你有什麽想法?一頭霧水? 接下來,我們還是按照狀態確立,狀態轉移,代碼實習三個步驟來分析這道題,並且得出一些適用性的規律。

狀態確立

  首先,我們可以一眼看出,只用諸如"處理完以i為根的子樹的最大收益"等一維的狀態不能處理這個問題,

  這個時候,我們可以考慮加一維來表示更多的限制條件:設f[i][j]表示"在以i為根的子樹中染j個黑色點的最大收益",最終答案即是f[1][k]

狀態轉移

  其實狀態定義蠻好想,但是,怎麽狀態轉移呢?

  由於......數據範圍很小,而我們權值的計算又與兩兩點之間關系有關,因此我們可以考慮枚舉點對的暴力做法.

  我們考慮,對於每個點對來說,他們之間的貢獻只會在他們的LCA處貢獻O(1)的時間復雜度.

  由於一共只有n2數量級的點對,因此我們如果這樣做的話算法復雜度是O(n2)的.

  既然這種算法的復雜度是O(n2)的,我們就可以隨便轉移考慮一種暴力的轉移:

  枚舉當前考慮的子樹中有幾個黑點,並考慮合並子樹帶來的貢獻.

  我們考慮,如果我們只統計當前子樹內的貢獻,顯然是不好轉移的,因為無法考慮與子樹外面點的關系

  所以,我們把子樹外面的點與子樹內點的貢獻也統計在f數組裏面,也就是說"外面伸進來的邊"也被統計了進來

  這樣,由於子樹內可以被統計的邊的貢獻已經被全部統計完,我們就可以通過考慮當前合並的兩節點之間的這條邊來統計貢獻:

技術分享

  在上圖中,子樹裏面紅色邊的貢獻以及考慮完,現在我們更新的是子樹外面的點與子樹內的點通過藍色邊貢獻的權值.

  設節點rt的子樹大小為size[rt],rt原來染色了j個黑點,設節點u的子樹大小為size[u],u原來染色了v個黑點,設邊權為val

  經過圖中的藍邊這條邊,u裏邊的白點與外面的白點產生了v*(k-v)個黑點對.

  同理,裏邊的白點與外面的白點產生了(size[u]-v])*(n-k-(size[u]-v))個白點對

  那麽rt->u這條邊總共產生了(v*(k-v)+(size[u]-v])*(n-k-(size[u]-v)))*val的新的貢獻.

  這樣我們就統計出來了新的貢獻,現在以rt為根的子樹總貢獻是f[rt][j]+f[u][v]+(v*(k-v)+(size[u]-v])*(n-k-(size[u]-v)))*val

  我們用上面這個式子去更新f[rt][j+v]的答案即可.

代碼實現

  在代碼中,這個算法是O(n2)就變得顯而易見了.先給出dp過程的代碼,我們開始分析:

 1 void dp(int rt,int fa)
 2 {
 3     f[rt][0]=f[rt][1]=0;size[rt]=1;
 4     for(int i=adj[rt];i;i=s[i].next)
 5     {
 6         int u=s[i].zhong;
 7         if(u!=fa)
 8         {
 9             dp(u,rt);
10             for(LL j=size[rt];~j;j--)
11                 for(int v=0;v<=size[u];v++)
12                 {
13                     LL match_num=(LL)v*(k-v)+(LL)(size[u]-v)*(n-k-(size[u]-v));
14                     f[rt][j+v]=max(f[rt][j+v],f[rt][j]+f[u][v]+(LL)(match_num*s[i].val));
15                 }
16             size[rt]+=size[u];
17         }
18     }
19 }

  就像上面說的,我們考慮把u這棵子樹合並到rt裏面產生的新貢獻.

  值得註意的一點是,我們如果先不合並起來,用刷表法去更新,要比先合並起來用填表法更新快不少.

  這一點帶來的優化很明顯,因為合並後j循環的次數變多了.

  具體的效率差別...大概是這樣(上面那個提交是後合並的打法):

  技術分享

  現在,這道題基本就我們解決了.完整代碼見下:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 typedef long long LL;
 6 const int N=2010;
 7 int n,k,e,adj[N];
 8 struct node{int zhong,val,next;}s[N<<1];
 9 inline void add(int qi,int zhong,int val)
10     {s[++e].zhong=zhong;s[e].next=adj[qi];s[e].val=val;adj[qi]=e;}
11 LL f[N][N],size[N];
12 void dp(int rt,int fa)
13 {
14     f[rt][0]=f[rt][1]=0;size[rt]=1;
15     for(int i=adj[rt];i;i=s[i].next)
16     {
17         int u=s[i].zhong;
18         if(u!=fa)
19         {
20             dp(u,rt);
21             for(LL j=size[rt];~j;j--)
22                 for(int v=0;v<=size[u];v++)
23                 {
24                     LL match_num=(LL)v*(k-v)+(LL)(size[u]-v)*(n-k-(size[u]-v));
25                     f[rt][j+v]=max(f[rt][j+v],f[rt][j]+f[u][v]+(LL)(match_num*s[i].val));
26                 }
27             size[rt]+=size[u];
28         }
29     }
30 }
31 int main()
32 {
33     scanf("%d%d",&n,&k);int a,b,c;
34     memset(f,0xaf,sizeof(f));
35     for(int i=1;i<n;i++)
36         scanf("%d%d%d",&a,&b,&c),add(a,b,c),add(b,a,c);
37     dp(1,0);printf("%lld\n",f[1][k]);
38 }

上面這道題還算一道比較簡單的樹形DP.這道題最大的特點就是那個非線性的O(n2)過程了.

這類非線性的DP一般狀態定義和狀態轉移都比較復雜,但是主要的思想要點是"合並".

如果你發現某個樹歸問題是與兩點間關系有關,那他很可能就是一個這種類型的DP

下面,我們再來看一道題.這道題可就沒有上題那麽簡單了......

BZOJ 3167: [Heoi2013]Sao

Time Limit: 30 Sec Memory Limit: 256 MB

Description

Welcome to SAO(Strange and Abnormal Online)。這是一個VRMMORPG,含有n個關卡。但是,挑戰不同關卡的順序是一 個很大的問題。有n–1個對於挑戰關卡的限制,諸如第i個關卡必須在第j個關卡前挑戰,或者完成了第k個關卡才 能挑戰第l個關卡。並且,如果不考慮限制的方向性,那麽在這n–1個限制的情況下,任何兩個關卡都存在某種程 度的關聯性。即,我們不能把所有關卡分成兩個非空且不相交的子集,使得這兩個子集之間沒有任何限制。

Input

第一行,一個整數T,表示數據組數。對於每組數據,第一行一個整數n,表示關卡數。接下來n–1行,每行為“i sign j”,其中0≤i,j≤n–1且i≠j,sign為“<”或者“>”,表示第i個關卡必須在第j個關卡前/後完成。 T≤5,1≤n≤1000

Output

對於每個數據,輸出一行一個整數,為攻克關卡的順序方案個數,mod1,000,000,007輸出。

Sample Input

5
10
5 > 8
5 > 6
0 < 1
9 < 4
2 > 5
5 < 9
8 < 1
9 > 3
1 < 7
10
6 > 7
2 > 0
9 < 0
5 > 9
7 > 0
0 > 3
7 < 8
1 < 2
0 < 4
10
2 < 0
1 > 4
0 > 5
9 < 0
9 > 3
1 < 2
4 > 6
9 < 8
7 > 1
10
0 > 9
5 > 6
3 > 6
8 < 7
8 > 4
0 > 6
8 > 5
8 < 2
1 > 8
10
8 < 3
8 < 4
1 > 3
1 < 9
3 < 7
2 < 8
5 > 2
5 < 6
0 < 9

Sample Output

2580
3960
1834
5208
3336
首先,我們可以看出,原題等價於給樹上的每個點分配一個權值,並使其滿足一些大於&小於關系; 同樣,一維的狀態無法滿足題目的要求. 為了方便處理,我們還是把原圖當做一棵樹處理.我們可以發現,一個點的子樹中有比他大的,也有比他小的. 那麽我們不妨再開一維來表示這種限制:設f[i][j]表示在以i為根的子樹中有j個比i小的數. 那麽狀態有了,我們怎麽轉移呢? 我們可以發現,訪問方案的不同與每對點的訪問先後順序有關.因此,我們可以考慮每一對點給最終方案帶來的不同影響, 那麽在轉移的時候依然采用合並子樹的思路,假設我們當前要合並rt的子樹u, 以rt要求比u大為例: 我們設合並前以rt為根的子樹中有i個比rt小,以u為根的子樹中有j個比rt小 首先,原來的合法排列就有f[rt][i]種.又由於u比rt小,因此在剛才那j個比rt小的數中有幾個比u小是不確定的,每一種方案都有可能出現,因此我們還需要乘上Σf[u][j],j∈[0,j] 接著,這j個比rt小的數插入的位置是不確定的,因此他們所處的位置不同會帶來C(i+j)(j)的貢獻種數. 同理,剩下size[u]-j個比rt大的數也會帶來C(size[rt]+size[u]-i-j-1)(size[u]-j)這麽多的貢獻. 那麽最終我們要更新的數量就是f[rt][i]*(Σf[u][j],j∈[0,j])*C(i+j)(j)*C(size[rt]+size[u]-i-j-1)(size[u]-j) 如果rt比u小那麽同理,只不過我們枚舉的方式變一下,看有幾個數比rt大 如果我們處理f數組的前綴和的話,就可以做到O(n2)的轉移啦! 代碼見下:
 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 typedef long long LL;
 6 const int mod=1000000007,N=1010;
 7 int n,adj[N],e;
 8 LL g[N],C[N][N],sum[N][N],size[N],f[N][N];//以i為根的子樹,有j個比i小(在i之前訪問)的方案數
 9 struct edge{int zhong,next,val;}s[N<<1];
10 inline void add(int qi,int zhong,int val)
11     {s[++e].zhong=zhong;s[e].val=val;s[e].next=adj[qi];adj[qi]=e;}
12 void dfs(int rt,int fa)
13 {
14     size[rt]=f[rt][0]=1;
15     for(int i=adj[rt];i;i=s[i].next)
16     {
17         int u=s[i].zhong;
18         if(u!=fa)
19         {
20             dfs(u,rt);int limit=size[rt]+size[u];
21             for(int i=0;i<limit;i++)g[i]=0;
22             if(s[i].val==1)//rt比u小
23                 for(int j=0;j<size[rt];j++)//已經合並完成的以rt為根節點的子樹中有j個比rt大(在rt之前訪問)
24                     for(int k=0;k<=size[u];k++)//以u為根節點的子樹中有k個比rt大(在rt之後訪問)
25                     {
26                         LL tmp1=f[rt][size[rt]-j-1]/*比rt小的size[rt]-j-1的合法方案數*/%mod*(sum[u][size[u]-1]-sum[u][size[u]-k-1]+mod)%mod;
27                             //u裏面有k個比rt大的,不一定有幾個比u大
28                         LL tmp2=C[j+k][k]*C[limit-j-k-1][size[u]-k]%mod;
29                                //組合數看方案數,前者表示在新的j+k個比rt大的數中新插入的k個數所在的位置
30                             //後者表示比rt小的size-j-k-1個數中u剩下的size[u]-k的排列
31                         g[limit-j-k-1]=(g[limit-j-k-1]+tmp1*tmp2%mod)%mod;
32                         //此時有limit-j-k-1個數比rt小,更新答案
33                     }
34             else//rt比u大(在u之後訪問)
35                 for(int j=0;j<size[rt];j++)//以rt為根節點的子樹中有j個比rt小
36                     for(int k=0;k<=size[u];k++)//以u為根節點的子樹中有k個比rt小
37                     {
38                         LL tmp1=f[rt][j]%mod*sum[u][k-1]%mod;
39                             //u裏面有k個比rt小的,不一定幾個比u小
40                         LL tmp2=C[j+k][k]*C[limit-j-k-1][size[u]-k]%mod;//和上面組合數的統計類似.
41                         g[j+k]=(g[j+k]+tmp1*tmp2%mod)%mod;
42                     }
43             size[rt]+=size[u];//不斷合並每棵子樹
44             for(int j=0;j<size[rt];j++)f[rt][j]=g[j];//更新f數組
45         }
46     }
47     sum[rt][0]=f[rt][0];
48     for(int j=1;j<size[rt];j++)sum[rt][j]=(sum[rt][j-1]+f[rt][j])%mod;//全部合並完成,計算合法方案前綴和
49 }
50 int main()
51 {
52     for(int i=0;i<=1000;i++)
53     {
54         C[i][0]=1;
55         for(int j=1;j<=i;j++)
56             C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
57     }
58     int t,a,b;char c[3];scanf("%d",&t);
59     while(t--)
60     {
61         scanf("%d",&n);
62         memset(size,0,sizeof(size));
63         memset(f,0,sizeof(f));
64         memset(sum,0,sizeof(sum));
65         e=0;memset(adj,0,sizeof(adj));
66         for(int i=1;i<n;i++)
67         {
68             scanf("%d%s%d",&a,c,&b),a++,b++;
69             if(c[0]==>)add(b,a,1),add(a,b,-1);
70             else add(a,b,1),add(b,a,-1);
71         }
72         dfs(1,0);int ans=0;
73         for(int i=0;i<n;i++)
74             ans=(ans+f[1][i])%mod;
75         printf("%d\n",ans);
76     }
77 }

非線性的樹形DP是一類很考驗DP思維,尤其是DP狀態定義能力的問題,這就需要OIer們通過刷題來不斷積累做題經驗了(其實什麽類型題不是呢).希望大家能從我的博文中有所收獲:)

[提升性選講] 樹形DP進階:一類非線性的樹形DP問題(例題 BZOJ4403 BZOJ3167)