樹形DP簡介:數組
樹形DP就是在樹上的DP,通常用遞歸實現。有兩種實現的遞歸方式:優化
前提:本題是一棵樹或是一個森林(很是重要!)ui
難點:spa
注意事項:code
fa[]
,再判斷一下便可。e[]
開兩倍空間。判斷此題是不是一棵樹或一個森林。(前提)blog
判斷此題爲二叉樹仍是多叉樹。(都用前向星儲存)遞歸
如果二叉樹則用lc[]
,rc[]
,fa[]
記錄;遊戲
如果多叉樹則判斷能否化爲二叉樹,若不能則直接用fa[]
ci
推狀態轉移方程get
化爲DFS形式。(在空間容許的狀況下能夠記搜)
父節點不能和子節點同時選( 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)\} \]
下圖爲洛谷秋令營的課件講解:
樹形分組揹包(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]); } } } }
二叉樹去有限條邊後總邊權最大值(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); } } } }
樹的最小覆蓋集(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 $
樹形分組揹包(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; } }
特別提醒:
基環樹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
,g
和 fr
,to
,不要手快打錯了。