淺談換根DP
本篇隨筆淺談一下算法競賽中的換根DP。
換根DP概念
換根DP其實是樹形DP的一種延伸技巧或者說是方法。
它的使用范圍是,對樹上的每個點跑樹形DP。這樣的話,不用換根DP一點一點跑的復雜度就是\(O(n^2)\),必炸。那么換根DP應運而生。簡單來講,就是我們會通過推理發現,我們先以一個選定節點跑出來的最優解,通過另一個轉移方程,就可以得出與他有關系的其他節點的答案。也就是說,我們相當於進行了兩次DP,第一次的樹形DP可以算作一種預處理,第二次的DP就是換根DP。其根本奧義就是用\(O(2N)\)的復雜度完成了\(O(N^2)\)的問題。
換根DP例題
POJ 3585
讓我們用一道例題來更深理解換根DP~
題目大意:
有一棵有\(n\)個節點、\(n-1\)條邊的無根樹,每邊有一流量限制。令某一節點為根節點,向根節點灌水,最終從葉子節點流出的水量和為這一節點的最大流量。問:在做根節點的所有節點中,最大的最大流量是多少?
題解:
很容易想到這個某個節點的最大流量可以用樹形DP來維護,但是因為一次樹形DP是\(O(n)\)的復雜度,如果有\(n\)個點,那么其復雜度就是\(O(n^2)\)的,\(n\le 2*10^5\),還是多組數據,必炸無疑。
那么就不能暴力地在每個節點都跑一次樹形DP,即需要一種不需要每次都跑的船新操作。
我們叫他換根DP。我的理解就是,樹形DP+換根。
俗稱扭一扭,因為在換根的過程中,樹的形態發生了扭轉。
那么我們考慮,用一次樹形DP作為信息的預處理,然后之后的答案能否通過預處理,使用換根DP來維護呢?
PS:先講樹形DP預處理。
一般來講,樹的形態固定的情況下,才可以把邊權轉點權(把邊權值給兒子,比如樹鏈剖分等等,比較常見的操作)。但換根DP因為樹的形態會扭,所以不適合把邊權轉點券。那么我們DP設置的狀態就需要以邊作維護。
狀態設置為:\(sum[x]\)表示以\(x\)點為根的子樹所能提供的最大流量和,那么顯然,兒子節點對於父親的貢獻就是這個\(sum[x]\)和父親到兒子的邊權的較小值。比如下圖(以1為根),\(sum[4]=15\),但是\(4\)號點對答案的貢獻其實是13,因為被限制了。
所以轉移方程就是:
需要注意的是初值,葉子節點的\(sum\)值應該為0,所以轉移的時候應該從倒數第二層節點開始轉,這個處理我們可以通過特判解決。
於是我們處理出了一個以\(1\)為根的\(sum\)數組,答案就是\(sum[1]\)。
然后就是扭一扭的過程,先上圖。
比如,以1為根的情況和以4為根的情況:(如圖)
我們發現,4-3-5這棵子樹的信息是沒有變化的,只是原先1是4的兒子,現在兒子翻身當爹了而已,也就是,只有以1為根的子樹的信息需要重新統計。我們又發現,1有很多兒子,其中只有4當了爹,其他的兒子依然是兒子,所以只需要把1之前與4的關系斷掉,進行重新統計。也就是說,原來的\(sum[1]\)要減去\(sum[4]\)和它倆之間的邊權的較小值,也就是13。成為新的\(sum[1]\)。
然后在新的根節點4上加上新的\(sum[1]\)即可。
這個扭一扭的過程可以通過第二次深搜來實現。
需要注意的細節是,當我們進行到葉子節點的時候,需要進行特殊判斷,很容易得出,在葉子節點和非葉子節點的轉移方程是不一樣的。
詳見代碼:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=2*1e5+10;
int n;
int tot,to[maxn<<1],nxt[maxn<<1],val[maxn<<1],head[maxn];
int sum[maxn<<1],dp[maxn<<1],du[maxn],ans;
void add(int x,int y,int z)
{
to[++tot]=y;
val[tot]=z;
nxt[tot]=head[x];
head[x]=tot;
}
void dfs1(int x,int f)
{
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(y==f)
continue;
dfs1(y,x);
if(du[y]>1)
sum[x]+=min(val[i],sum[y]);
else
sum[x]+=val[i];
}
}
void dfs2(int x,int f)
{
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(y==f)
continue;
if(du[x]==1)//leaf
dp[y]=sum[y]+val[i];
else
dp[y]=sum[y]+min(dp[x]-min(sum[y],val[i]),val[i]);
dfs2(y,x);
}
}
void clean()
{
tot=0;
ans=-1;
memset(sum,0,sizeof(sum));
memset(du,0,sizeof(du));
memset(dp,0,sizeof(dp));
memset(to,0,sizeof(to));
memset(nxt,0,sizeof(nxt));
memset(head,0,sizeof(head));
memset(val,0,sizeof(val));
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
clean();
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
du[x]++;
du[y]++;
}
dfs1(1,0);
dp[1]=sum[1];
dfs2(1,0);
for(int i=1;i<=n;i++)
ans=max(ans,dp[i]);
printf("%d\n",ans);
}
return 0;
}
這就是換根DP啦~