淺談換根DP


淺談換根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[x]=\Sigma_{y\in son[x]} \min(sum[y],val[i]) \]

需要注意的是初值,葉子節點的\(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啦~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM