樹形DP

目錄:

  • 我的理解
  • 作題步驟
  • 例題的狀態轉移方程

1、我的理解:

  1. 樹形DP簡介:數組

    樹形DP就是在樹上的DP,通常用遞歸實現。有兩種實現的遞歸方式:優化

    • \(\rightarrow\) 根:先更新了葉節點的信息,在回溯回去更新父親節點的信息。(eg. P1352 沒有上司的舞會
    • \(\rightarrow\) 葉:先從葉節點往根節點DFS一遍(預處理)了之後,在從新往下更新。(不經常使用)
  2. 前提:本題是一棵或是一個森林(很是重要!)ui

  3. 難點:spa

    • 狀態轉移方程
    • 邊界條件
    • 剪枝優化
    • 細節(左子節點,右子節點,父節點)
  4. 注意事項:code

    • 建無向圖:若建有向圖,能夠不判斷父節點,但如果從子節點更新到父節點,則須要從子到父,不太方便。能夠直接建無向圖,記錄一下fa[],再判斷一下便可。
    • e[]開兩倍空間。
    • 遞歸必定要寫邊界條件。

2、作題步驟:

  1. 判斷此題是不是一棵樹或一個森林。(前提)blog

  2. 判斷此題爲二叉樹仍是多叉樹。(都用前向星儲存)遞歸

    如果二叉樹則用lc[],rc[],fa[]記錄;遊戲

    如果多叉樹則判斷能否化爲二叉樹,若不能則直接用fa[]ci

  3. 推狀態轉移方程get

  4. 化爲DFS形式。(在空間容許的狀況下能夠記搜)


3、常見的狀態轉移方程:

  1. 父節點不能和子節點同時選( P1352 沒有上司的舞會

    定義 \(f(i)(0/1)\) 表示 \(i\) 點的最優解,\(0\) 表示 \(i\) 不選,\(1\) 表示 \(i\) 點要選。\(crit(i)\) 表示選擇 \(i\) 能夠得到的價值。\(son(i)\) 表示 \(i\) 的子節點。

    \(i\) 點要選,則 \(i\) 的子節點都不能選,故狀態轉移方程爲:
    \[ f(i)(1)=\sum_{j\in son(i)}f(j)(0)+crit(i) \]
    \(i\) 點不選,則 \(i\) 的子節點可選可不選,故狀態轉移方程爲:
    \[ f(i)(0)=\sum_{j\in son(i)}\max\{f(j)(0),f(j)(1)\} \]

    下圖爲洛谷秋令營的課件講解:

  2. 樹形分組揹包(1)(P2014 選課

    定義 \(f(i)(j)\) 表示 \(i\) 點選擇 \(j\) 種課程的最優解。 \(crit(i)\)表示選擇 \(i\) 能夠得到的價值。\(son(i)\) 表示 \(i\) 的子節點。

    有分組揹包標準模型可得,狀態轉移方程爲:
    \[ f(i)(k)=\max_{l=0}^{k-1} \{f(i)(k-l)+f(j)(l)\}(k\in[1,m+1],j\in son(i)) \]

    \[ f(i)(1)=crit(i) \]

    關鍵代碼片斷以下:

    DP(1)//調用
    
    void DP(int fr)
    {
     for(int i=head[fr];i;i=e[i].next)//分組揹包中的枚舉總組數
     {
         int to=e[i].to;
         if(to==fa[fr]) continue; 
         DP(to);
         for(int j=m+1;j>=1;j--)//分組揹包中的枚舉揹包容量
         {
             for(int k=0;k<j;k++)//分組揹包中的枚舉每組中的物品個數
             {
                 dp[fr][j]=max(dp[fr][j],dp[fr][j-k]+dp[to][k]);
             }
         }
     }
    }
  3. 二叉樹去有限條邊後總邊權最大值(P2015 二叉蘋果樹

    定義 \(f(i)(j)\)\(i\) 點去 \(j\) 條邊的最優解。\(crit(i,j)\)\(i\) 點到 \(j\) 點的邊權。\(son(i)\) 表示 \(i\) 的子節點。

    \(i\) 點去 \(j\) 條邊的最優解爲 \(i\) 點的子節點去 \(k\) 條邊的最優解、\(i\) 點去 \(j-i-1\) 條邊的最優解與 \(i\)\(i\) 的子節點的邊權的和的最大值。

    故狀態轉移方程爲:
    \[ f(i)(k)=\max_{l=0}^{l-1} \{f(i)(k-l-1)+f(j)(l)+crit(i,j)\} \]
    \[ (k\in [1,\text{q}],j\in son(i)) \]

    關鍵代碼片斷以下:

    DP(1,0);//調用
    
    void DP(int fr,int fa)
    {
     for(int i=head[fr];i;i=e[i].next)
     {
         int to=e[i].to;
         int v=e[i].v;
         if(to==fa) continue;
         DP(to,fr);
         for(int j=Q;j>=1;j--)
         {
             for(int k=j-1;k>=0;k--)
             {
                 dp[fr][j]=max(dp[fr][j],dp[fr][j-k-1]+dp[to][k]+v);
             }
         }
     }
    }
  4. 樹的最小覆蓋集(P2016戰略遊戲

    注:此題可用二分圖匹配實現:

    這題其實有幾種方法,其中比較顯而易見的或許是樹形dp吧,樓下有不少大佬已經解釋過了,(這裏就再也不說了),仔細一看題就能夠發現這是一個典型的最小點覆蓋

    最小點覆蓋指的是在一個圖中:一個點覆蓋與之鏈接的邊,求用最少的點能夠覆蓋。

    這和題目要求如出一轍。同時還有一個定理,最小點覆蓋=最大匹配數。若是是無向圖則/2。————摘自 pengym 的題解

    定義 \(f(i)(0/1)\)\(i\) 點的最優解,\(0\) 表示 \(i\) 不選,\(1\) 表示 \(i\) 要選。\(son(i)\) 表示 \(i\) 的子節點。

    \(i\) 點要選,則 \(i\) 點的子節點可選可不選,最後還要加上本身一個節點,故狀態轉移方程爲:
    \[ f(i)(1)=\sum_{j\in son(i)}\min\{f(j)(0),f(j)(1)\}+1 \]
    \(i\) 點不選,則 \(i\) 點的子節點必須選,故狀態轉移方程爲:
    \[ f(i)(0)=\sum_{j\in son(i)}f(j)(1) \]
    特別提醒\(f(i)(0/1)\) 必須初始化爲 $+\infty $

  5. 樹形分組揹包(2)(P1273 有線電視網)

    定義 \(f(i)(j)\)\(i\) 點保留 \(j\) 個葉節點的最優解。\(crit(i,j)\)\(i\) 點到 \(j\) 點的邊權。\(son(i)\) 表示 \(i\) 的子節點。\(size(i)\) 表示 \(i\) 節點的子樹大小。\(\text{E}_{\text{Leaf}}\) 爲樹的葉節點的集合。\(\text{E}_{\text{All}}\) 爲樹的全部節點的集合。\(value(i)\)\(i\) 點所需的費用(\(i\in \text{E}_{\text{Leaf}}\) )。

    \(i\) 點的最優解爲子節點保留 \(k\) 個葉節點的最優解再加上 \(i\) 點能保留 \(j-k\) 個葉節點的最優解減去邊權。

    最後只需統計能夠更新到的最大值。

    故狀態轉移方程爲:
    \[ f(i)(k)=\max_{j\in son(i)}\{f(i)(k-l)+f(j)(l)-crit(i,j)\} \]

    \[ (k\in[0,size(i)],l\in [0,\min\{k,size(j)\}]) \]

    \[ f(i)(0)=0(i\in \text{E}_{\text{All}}),f(i)(1)=value(i)(i\in \text{E}_{\text{Leaf}}),size(i)=1(i\in \text{E}_{\text{Leaf}}) \]

    關鍵代碼以下:

    DP(1,-1);//調用
    
    void DP(int fr,int fa)
    {
        for(int i=head[fr];i;i=e[i].next)
        {
            int to=e[i].to;
            if(to==fa) continue;
            DP(to,fr);
            siz[fr]+=siz[to];
        }
        for(int i=head[fr];i;i=e[i].next)
        {
            int to=e[i].to;
            int v=e[i].v;
            if(to==fa) continue;
            for(int j=siz[fr];j>=0;j--)
            {
                for(int k=0;k<=min(j,siz[to]);k++)
                {
                    dp[fr][j]=max(dp[fr][j],dp[fr][j-k]+dp[to][k]-v);
                }
            }
        }
    }
    
    for(int i=m;i>=0;i--)//統計最終答案
    {
        if(dp[1][i]>=0)
        {
            printf("%lld\n",i);
            return 0;
        }
    }

    特別提醒

    • \(f(i)(j)\) 必定要初始化爲$-\infty $ ,而且 \(f(i)(0)\)\(f(i)(1) (i\in \text{E}_{\text{Leaf}})\)\(size(i)(i\in \text{E}_{\text{Leaf}})\) 必定要按上面寫的初始化。
    • 在輸入時,千萬不要搞錯 \(i\)\(j\)
  6. 基環樹DP(P2607 [ZJOI2008]騎士

    本題的DP模型同 P1352 沒有上司的舞會。本題的難點在於如何把基環樹DP轉化爲普通的樹上DP。

    考慮斷邊和換根。先找到其中的一個環,在上面隨意取兩個點, 斷開這兩個點的邊,使其變爲一棵普通樹。以其中的一點爲樹根作樹形DP,再以另外一點爲樹根再作一次樹形DP,由於相鄰的兩點不能同時選,因此最後統計一下 \(f(i)(0)\)\(g(j)(0)\) 的最大值便可。

    定義 \(f(i)(0/1)\) 爲第一次樹形DP的 \(i\) 點的最優解,\(g(i)(0/1)\) 爲第二次樹形DP的 \(i\) 點的最優解。$\text{Ans} $ 爲一次基環樹DP的答案。\(\text{E}_\text{Circle}\) 爲基環樹的環上的點的集合。

    故一次基環樹DP的答案爲:
    \[ \text{Ans}=\max\{f(i)(0),g(j)(0)\} \]

    \[ (i,j\in \text{E}_\text{Circle},i\neq j) \]

    下圖爲洛谷秋令營的課件講解:

    關鍵代碼以下:

    void covertree(int fr)//尋找基環樹
    {
     used[fr]=1;
     for(int i=head[fr];i;i=e[i].next)
     {
         int to=e[i].to;
         if(used[to]==0)
         {
             covertree(to);
         }
     }
    }
    
    
    void findcir(int fr,int fa)//尋找基環樹中的環
    {
     if(flag) return ;
     vis[fr]=1;
     for(int i=head[fr];i;i=e[i].next)
     {
         int to=e[i].to;
         if(vis[to]==0)
         {
             findcir(to,fr);
         }else if(to!=fa)
         {
             fri=fr;//第一個點
             toi=to;//第二個點
             E=i;//邊的編號
             flag=1;
             return ;
         }
     }
    }
    
    
    void DPf(int fr)//以其中的一點爲樹根作樹形DP
    {
     visf[fr]=1;
     f[fr][1]=crit[fr];
     for(int i=head[fr];i;i=e[i].next)
     {
         int to=e[i].to;
         if(visf[to]==0&&(i^1)!=E)//保證不會選到第一個點和第二個點,至關於斷邊
         {
             DPf(to);
             f[fr][0]+=max(f[to][0],f[to][1]);
             f[fr][1]+=f[to][0];
         }
     }
    }
    
    
    void DPg(int fr)//再以另外一點爲樹根再作一次樹形DP
    {
     visg[fr]=1;
     g[fr][1]=crit[fr];
     for(int i=head[fr];i;i=e[i].next)
     {
         int to=e[i].to;
         if(visg[to]==0&&(i^1)!=E)
         {
             DPg(to);
             g[fr][0]+=max(g[to][0],g[to][1]);
             g[fr][1]+=g[to][0];
         }
     }
    }
    
    for(int i=1;i<=n;i++)//調用+統計答案
    {
        if(used[i]==1) continue;
        covertree(i);
        flag=0;
        findcir(i,-1);
        DPf(fri);
        DPg(toi);
        ans+=max(f[fri][0],g[toi][0]);
    }

    特別注意

    • 本題是基環樹森林,而不是單棵基環樹,故要反覆尋找覆蓋基環樹,最後將全部答案加起來。
    • 由於要斷邊,因此前向星計數器 ei必定要初始化爲 1。
    • 用多個數組標記(used[],vis[],visf[],visg[])。
    • 必定要注意 f,gfr,to,不要手快打錯了。
相關文章
相關標籤/搜索