基礎樹形DP
模板
P1352 沒有上司的舞會
樹狀\(dp\)模板題。
設\(f[i][0]\)為第\(i\)個人來了的方案數
\(f[i][1]\)為第\(i\)和人沒來的方案數
若第\(i\)個人來了,那麼其下屬均不回來
若不來,其下屬則有來和不來兩種選擇
因此狀態轉移方程為:
-
\(f[i][0]+=f[son][1]\)
-
\(f[i][1]+=max(f[son][0],f[son][1])\)
P2015 二叉蘋果樹
樹上揹包模板題
每一個枝條都有"剪"和"不剪"兩種可能
把每一個兒子都看成一個"分組揹包"
設\(dp[i][j]\)
每加入一個"兒子"後,列舉該"兒子"保留的邊數,如圖
(ps:這裡i-k後面還要減1是因為還要多保留從u->v這條邊)
故狀態轉移方程為:
- \(f[u][i]=max(f[v][i-k-1]+f[u][k]+w[u][v])(i\in[1,m+1)]\)
樹上揹包
P2014 [CTSC1997]選課
和二叉蘋果樹一樣的套路。
把每一個子課程都看作是一個"分組揹包",倒序列舉即可
由於題目中可能有多棵樹
因此多開一個節點把所有"樹根"連在一起
同時,在倒序列舉時也要把這個新節點算進去
轉移方程:
- \(f[u][i]=max(f[v][i-k-1]+f[u][k]+w[v])(i\in[1,m+1])\)
核心程式碼:
void dp(int k){ vis[k]=1; for(int i=0;i<son[k].size();i++){ if(vis[son[k][i]]!=1){ dp(son[k][i]); for(int v=m+1;v>=1;v--){ for(int K=0;K<v;K++){ f[k][v]=max(f[k][v],w[son[k][i]]+f[son[k][i]][K]+f[k][v-K-1]); } } } } return; }
P1273 有線電視網
也是比較經典的一個樹上揹包問題
題目中要求的是在不虧本的情況下最多的觀看使用者個數
設\(f[i][j]\)表示第\(i\)個站傳輸給\(j\)個使用者觀看最終剩餘的錢數
若最終剩餘錢數大於等於0,則說明未虧本
反之,則說明虧本
轉移方程則為:
- \(f[u][i]=max(f[v][k]+f[u][i-k]-w[u][v])\)
\(dp\)完後從總人數開始倒序判斷是否虧本即可
貼個核心程式碼:
\(dp部分\)
void dfs(int x){
dp[x][0]=0;
if(val[x]){//如果是根節點
size[x]=1;//人數加一
dp[x][1]=val[x];
return;
}
for(int i=0;i<son[x].size();i++){
dfs(son[x][i]);
size[x]+=size[son[x][i]];//計算x節點下的人數總和
for(int j=size[x];j>=0;j--){//滾動陣列,倒序列舉
for(int k=1;k<=size[son[x][i]];k++){//列舉子樹傳輸的觀眾數量
dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[son[x][i]][k]-W[x][son[x][i]]);
}
}
}
return;
}
\(判斷部分\)
for(int i=m;i>=0;i--){
if(dp[1][i]>=0){//如果不虧本
cout<<i;
break;
}
}
P1270 “訪問”美術館
跟P1273 有線電視網一樣的套路
設\(f[i][j]\)為在第\(i\)個節點下偷\(j\)幅畫所需要的最小總時間
狀態轉移方程也就呼之欲出了
- \(f[u][i]=min(f[v][k]+f[u][i-k]-2w[u][v])\)
這裡\(w[u][v]\)要乘2是因為要進出各一趟
核心程式碼:
int dfs(int x){
if(paint[x]!=0){
return paint[x];
}
int s=0;
for(int i=0;i<son[x].size();i++){
int v = son[x][i];
int t=dfs(v);
s+=t;
for(int j =s;j>0;j--){
for(int k=0;k<=t;k++){
dp[x][j] = min(dp[x][j] , dp[v][k] + dp[x][j-k]+w[x][v]*2);
}
}
}
return s;
}
P1272 重建道路
同樣也是一道比較經典的樹上揹包問題
設\(f[i][j]\)為第\(i\)個節點斷出一個大小為\(j\)的子樹所需要的斷開總數
狀態轉移方程:
- \(f[u][i]=min(f[v][k]+f[u][i-k]-1)\)
(\(v\)為根的子樹提供\(k\)個節點,\(u\)和其他兒子提供\(j-k\)個節點)
同時,由於一開始時一個子樹都沒有加進來
即把\(u\)的所有"兒子"都切斷了
因此當把\(v\)兒子加進來的時候要把之前那段減去的邊加回來
核心程式碼:
void dfs(int x){
size[x] = 1;
if(!is_son[x]){
dp[x][1] = 0;
size[x] = 1;
return;
}
for(int i=0;i<son[x].size();i++){
int v = son[x][i];
dfs(v);
size[x]+=size[v];
for(int j = size[x];j>=0;j--){
for(int k = 1;k<=size[v];k++){//這裡題解裡很多人都寫成了<j,問題是子樹可能本身就沒有這麼多子節點,感覺有些問題
dp[x][j] = min(dp[x][j],dp[x][j-k]+dp[v][k]-1);
}
}
}
}
普通樹形\(DP\)
P4084 [USACO17DEC]Barn Painting G
P2016 戰略遊戲
帶了點貪心思想的樹形\(DP\)
如果父節點放了一個守衛
那其子節點就都不用放守衛了
反之,子節點都要放一個守衛
轉移方程:
- \(f[u][0]+=f[v][1]\)
- \(f[u][1]+=min(f[v][1],f[v][0])\)
為什麼不用兒子的兒子("孫子")節點來看守兒子節點?
如果一個節點不是葉子節點,那他的子節點數必定大於或等於\(1\),因此如果用兒子節點來看守其父節點,花費的數量肯定會更多(或不變)。
遺憾的是題解裡似乎沒人說正確性的證明?,還是說太簡單了都懶得證了
核心程式碼:
void dfs(int x){
if(x!=1509)
dp[x][1]=1,dp[x][0]=0;
for(int i=0;i<son[x].size();i++){
dfs(son[x][i]);
dp[x][0]+=dp[son[x][i]][1];
dp[x][1]+=min(dp[son[x][i]][1],dp[son[x][i]][0]);
}
}
關於這道題目的帶點權版:
P2458 [SDOI2006]保安站崗
題解連結
P4084 [USACO17DEC]Barn Painting G
樹上\(DP\)求方案數。
還算是比較簡單的題目吧...
設:
\(f[i][0]\)為第\(i\)個節點塗紅色的方案數
\(f[i][1]\)為第\(i\)個節點塗綠色的方案數
\(f[i][2]\)為第\(i\)個節點塗藍色的方案數
假設第\(i\)號節點塗了紅色,那麼它的上一個節點就只能塗綠色和藍色
其他情況也同理
用乘法定理乘一下即可。
轉移方程:
- \(\begin{cases}f[u][1]=f[u][1]*((f[v][2]+f[v][3]))\\f[u][2]=f[u][2]*((f[v][1]+f[v][3]))\\f[u][3]=f[u][3]*((f[v][1]+f[v][2]))\end{cases}\)
換根\(DP\)
一種形式十分優美的樹形\(DP\)
P2986 [USACO10MAR]Great Cow Gathering G
P3047 [Nearby Cows G]Great Cow Gathering G
P3478 [POI2008]STA-Station
換根DP的模板題。
這裡我們設
-
\(size[i]\)為以\(1\)為根節點時節點\(i\)的子樹大小
-
\(dep[i]\)為以\(1\)為根節點時節點\(i\)的深度大小
-
\(dp[i]\)為以\(i\)為根節點時深度之和的大小
很明顯,我們可以通過一遍DFS求出以\(1\)為根節點時的深度之和
如果一個個的去算的話
照這個資料範圍,顯然會T飛
這個時候就要用到換根DP了
換根\(DP\)優化
可以看出,當我們把根節點從1換到3時
對子節點3的貢獻由兩部分組成
1.自己子樹的貢獻(圖中的k)
2.父親節點\(1\)的貢獻
如何轉移
-
首先是\(k\),作為自己子樹所產生的貢獻肯定要加上
-
\(dp[u]\)為以\(u\)為根節點時的深度總值,在計算時,要減去\(v\)的子樹所產生的貢獻,不然就重複計算了,同時
在以 \(u\)為根時,v節點及其子樹內的所有節點的深度都增加了\(1\),需要減去
(圖中紅色的節點)
合起來就是\(dp[u]-(size[v]+k)\)
- 除v子樹外的其他節點也一樣
在以\(v\)為根時,除\(v\)節點及其子樹外的其他節點的深度都增加了\(1\)
(圖中藍色的節點)
合起來就是\((size[1]-size[v])\)
得到轉移方程
- \(dp[v] = k+(dp[u]-(k+size[v]))+(size[1]-size[v])\)
化簡一下
- \(dp[v] = dp[u]-2size[v]+size[1]\)
核心程式碼:
void dfs1(int x){
size[x] = 1;
vis[x] = 1;
for(int i=0;i<son[x].size();i++){
int v = son[x][i];
if(!vis[v]){
dep[v] = dep[x] +1;
dfs1(v);
size[x]+=size[v];
}
}
}
void dfs2(int x){
vis[x] = 1;
for(int i=0;i<son[x].size();i++){
int v = son[x][i];
if(!vis[v]){
dp[v] = dp[x] +size[1] - 2*size[v];
dfs2(v);
}
}
}
P2986 [USACO10MAR]Great Cow Gathering G
前面那道題目的帶權值版
一模一樣的思路,只需要把狀態轉移方程轉換一下即可。
void dfs(int u,int fa){
for(int i=head[u];i;i=edge[i].next){
int v =edge[i].v;
if(v==fa) continue;
dfs(v,u);
size[u] += size[v];
sum[u]+=(sum[v]+edge[i].w*size[v]);
}
}
void dp(int u,int fa){
for(int i=head[u];i;i=edge[i].next){
int v =edge[i].v;
if(v==fa) continue;
f[v] = 1LL*f[u] + AN*edge[i].w - 2*size[v]*edge[i].w;
ans = min(ans,f[v]);
dp(v,u);
}
}
P3047 [Nearby Cows G]
1.\(狀態表示\)
設\(size[i][j]\)為第i個節點向下\(j\)層所包含的點權和
\(f[i][j]\)為第\(i\)個點距離它不超過 \(j\)的所有節點權值和
2.狀態轉移
對於\(size[i][j]:\)
\(size[u][j] =\sum\ size[v][j-1]\) 自己向下\(j\)層即為兒子向下\(j-1\)
對於\(f[i][j]:\)
兒子對它的貢獻:
\(size[v][j]\)
自己向下\(j\)層,兒子節點肯定也要向下\(j\)層
父親對它的貢獻:
\(f[u][j-1]-size[v][j-2]\)
父親節點擴充套件\(j-1\)層的值減去和兒子節點的值所重複包含的\(j-2\)層值
轉移方程:
\(f[v][j] = f[u][j-1]+size[v][j]-size[v][j-2]\)
核心程式碼:
void dfs(int u,int fa){
for(int i=head[u];i;i=edge[i].next){
int v =edge[i].v;
if(v==fa) continue;
dep[v]=dep[u]+1;
dfs(v,u);
for(int i=1;i<=k;i++){
size[u][i]+=size[v][i-1];
}
}
}
void dp(int u,int fa){
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].v;
if(v==fa) continue;
for(int i=1;i<=k;i++){
if(i-2>=0)
f[v][i] = size[v][i]+f[u][i-1] - size[v][i-2];
else f[v][i] = size[v][i]+f[u][i-1];
}
dp(v,u);
}
}
CF708C Centroids
一道做起來比較麻煩的換根\(DP\)
分析
首先對於一個節點來說,大小大於\(n/2\)的節點肯定只有一個,這個顯而易見
再來看如何改造
如果說該節點本身的重兒子就小於\(n/2\),那肯定可以成為樹的重心
反之,肯定要在重兒子裡找出一個重量最大的且小於等於\(n/2\)的子樹,並將其斷開,連線到根節點上(相當於刪去這顆子樹)
如果重兒子的大小減去被刪去兒子的大小小於等於\(n/2\),則說明可以改造
反之,無法改造
如何轉移
分兩種情況來討論
\(1\).該節點不是其父親節點重兒子
其父節點的重兒子不會被改變,只需要判斷該節點的重兒子是否改成其父節點即可
\(2\).該節點是其父親節點的重兒子
其父親節點的重兒子會變為其"次大"兒子,其兒子節點的重兒子不會改變
核心程式碼:
void dfs(int u){
vis[u] = 1;
size[u] = 1;
for(int i=0;i<son[u].size();i++){
int v = son[u][i];
if(!vis[v]){
dfs(v);
size[u]+=size[v];
if(size[v] > size[maxson[u]])
maxson[u] = v;
}
}
if(maxson[u]!=0){
if(size[maxson[u]]<=n/2) dp[u] = size[maxson[u]];
else dp[u] = dp[maxson[u]];
}
}
void exchange(int u,int v){
size[u] = size[u] - size[v];
size[v] = size[v] + size[u];
if(v==maxson[u]){
maxson[u] = 0;
for(int i=0;i<son[u].size();i++){
int V = son[u][i];
if(V!=v&&size[V] > size[maxson[u]]){
maxson[u] = V;
}
}
if(maxson[u]!=0){
if(size[maxson[u]]<=n/2) dp[u] = size[maxson[u]];
else dp[u] = dp[maxson[u]];
}
}
if(size[maxson[v]]<size[u]){
maxson[v] = u;
if(maxson[v]!=0){
if(size[maxson[v]]<=n/2) dp[v] = size[maxson[v]];
else dp[v] = dp[maxson[v]];
}
}
}
void dfs2(int u){
vis[u] = 1;
if(size[maxson[u]]<=n/2||size[maxson[u]] - dp[maxson[u]]<=n/2) ans[u]=1;
for(int i=0;i<son[u].size();i++){
int v = son[u][i];
if(!vis[v]){
exchange(u,v);
dfs2(v);
exchange(v,u);
}
}
}
end.
基環樹部分還是先緩緩吧,暫時還未完全掌握