1. 程式人生 > 其它 >換根 DP 學習筆記 & 題解【[APIO2014]連珠線】

換根 DP 學習筆記 & 題解【[APIO2014]連珠線】

概述

本題為換根 DP 經典題,比較模板,藉此題整理換根 DP。

題意簡述

今構造一棵由珠子和線構成的樹,可以通過下列方式中的一種新增一個珠子:

  1. 將一顆新珠子和已有的一顆珠子用紅線連線起來;
  2. 將一顆新珠子插入到兩顆用紅線連線的已有珠子之間。具體地,拆除原來的紅線,並用藍線將新珠子分別與兩顆珠子相連。

每條線有一個權值。現在已經知道樹的結構和每條線的權值,但不知道每條線的顏色。你需要輸出合法的構造方式中,藍線權值和的最大值。

提示

對於拿到的這棵樹,隨便選擇一個點作為根,那麼容易發現藍線的產生只可能是下面兩種方式(形態):

(打紅圈的是某一時刻插入的新珠子)

假如我們分類討論地 DP 子樹中的這兩種形態的藍線權值和最大值,會發現這是十分困難的。

同時,我們意識到這樣子分成兩種形態是十分生硬的,因為它們本質是一樣的。

考慮這兩種形態能否一統為一:你會發現必然存在一個節點,使得以它為根時所有的藍線都是可以以上左圖的方式構造的。

例如,樣例圖中,以 10 為根就可以實現:

題解

根據提示,我們可以進行換根 DP。即:

  • 先進行第一輪 dfs,求出以 1 為根的時候的按上左圖方式構造最大的藍線長度和 \(f_{i,0/1}\)

    • \(f_{i,0/1}\) 表示以 \(i\) 為根的子樹內,\(i\) 是不是(\(1/0\))“重要點”(即上上圖中的打紅圈的點)的答案(例如上圖中 \(f_{2,1}=3+2+21+8=34\))。
    • \(f_{i,0}=\sum_{j\in son(i)}\max(f_{j,0},f_{j,1}).\)
    • 由於上述轉移方式不利於後期程式碼實現,因此更改 \(f_{i,1}\) 的定義:
      • 上圖中 \(f_{2,1}\) 只算 \(f_{2,1}=3+2+8=13\)\(21\) 先不算,等 1 號點算的時候再加上)
      • 也就是說,只記錄原 \(f_{i,1}\) 算的邊當中真的在 \(i\) 的子樹中的部分。
    • 於是 \(f_{i,0}=\sum_{j\in son(i)}\max(f_{j,0},f_{j,1}+w(i,j)).\)
    • \(v=\min_{j\in son(i)}\{\max(f_{j,0},f_{j,1}+w(i,j))-(f_{j,0}+w(i,j))\}\),則 \(f_{i,1}=f_{i,0}-v\)
  • 在進行第二輪 dfs(換根),求出以不同節點為根時的答案。

    • \(g_{i,0/1}\) 記錄 \(i\) 被某一個兒子吊起時,\(i\) 子樹內的 \(f_{0/1}\)。注:\(g_{i,0/1}\) 會根據是被哪一個兒子吊起重新計算,因此複雜度不正確,後面會優化。

    • \(F_{0}=f,F_1=g\)

    • 則有

      \[F_{1,i,0}=\sum_{j\in link(i),j\ne p}\max(F_{j=fa[i],j,0},F_{j=fa[i],j,1}+w(i,j))\\ v=\min_{j\in link(i),j\ne p}\{\max(F_{j=fa[i],j,0},F_{j=fa[i],j,1}+w(i,j)-(F_{j=fa[x],j,0}+w(i,j))\}\\ F_{1,i,1}=F_{1,i,0}-v \]

      其中 \(p\) 表示吊起 \(i\) 的兒子,\(link(i)\) 表示與 \(i\) 相連的所有點的集合,\(fa[i]\) 代表以 1 為根時 \(i\) 的父節點,\(j=fa[i]\) 為一個 \(0/1\) 值。

  • 時間複雜度在菊花樹時可能被卡到 \(O(n^2)\),無法通過。

  • 發現每次查詢的本質其實是 \(link(i)\) 除去一個元素後的 \(\max(F_{j=fa[i],j,0},F_{j=fa[i],j,1}+w(i,j))\) 之和,以及 \(\max(F_{j=fa[i],j,0},F_{j=fa[i],j,1}+w(i,j)-(F_{j=fa[x],j,0}+w(i,j))\) 的最小值。這種東西容易通過預處理出所求表示式的字首值和字尾值,然後算 \(val_{pre}[i-1]\otimes val_{suf}[i+1]\) 來獲得,其中 \(\otimes\) 表示一種合併運算,對應上面的 \(+\)\(\min\)

  • 這樣一來,複雜度被降到了 \(O(n)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,ans,f[2][N][2],fa[N];
vector<pair<int,int> >G[N],vec[N],val[2][N];
void dfs1(int x,int p,int pe){
	fa[x]=p;
	int v=1e9;
	for(int i=0;i<G[x].size();i++){
		int y=G[x][i].first,z=G[x][i].second;
		if(y^p){
			dfs1(y,x,z);
			f[0][x][0]+=max(f[0][y][0],f[0][y][1]+z);
			v=min(v,max(f[0][y][0],f[0][y][1]+z)-(f[0][y][0]+z));
		}
	}
	f[0][x][1]=f[0][x][0]-v;
}
void calc(int x){
	int v=1e9,tot=0;
	vec[x].resize(G[x].size()+2),val[0][x].resize(G[x].size()+2),val[1][x].resize(G[x].size()+2);
	for(int i=0;i<G[x].size();i++){
		int y=G[x][i].first,z=G[x][i].second;
		vec[x][++tot]=G[x][i];
		f[1][x][0]+=max(f[y==fa[x]][y][0],f[y==fa[x]][y][1]+z);
		v=min(v,max(f[y==fa[x]][y][0],f[y==fa[x]][y][1]+z)-(f[y==fa[x]][y][0]+z));
	}
	val[0][x][0]=val[1][x][tot+1]=make_pair(0,1e9);
	for(int i=1;i<=tot;i++){
		int y=vec[x][i].first,z=vec[x][i].second;
		val[0][x][i]=make_pair(val[0][x][i-1].first+max(f[y==fa[x]][y][0],f[y==fa[x]][y][1]+z),
		min(val[0][x][i-1].second,max(f[y==fa[x]][y][0],f[y==fa[x]][y][1]+z)-(f[y==fa[x]][y][0]+z)));
	}
	for(int i=tot;i;i--){
		int y=vec[x][i].first,z=vec[x][i].second;
		val[1][x][i]=make_pair(val[1][x][i+1].first+max(f[y==fa[x]][y][0],f[y==fa[x]][y][1]+z),
		min(val[1][x][i+1].second,max(f[y==fa[x]][y][0],f[y==fa[x]][y][1]+z)-(f[y==fa[x]][y][0]+z)));
	}
}
void upd(int x,int i){
	if(!i){
		f[1][x][0]=val[1][x][i+1].first;
		f[1][x][1]=f[1][x][0]-val[1][x][i+1].second;
	}
	else {
		f[1][x][0]=val[0][x][i-1].first+val[1][x][i+1].first;
		f[1][x][1]=f[1][x][0]-min(val[0][x][i-1].second,val[1][x][i+1].second);
	}
}
void dfs2(int x,int p,int pe,int po){
	if(p)upd(p,po);
	calc(x);
	ans=max(ans,f[0][x][0]+max(f[1][p][0],f[1][p][1]+pe));
	int tot=vec[x].size()-2;
	for(int i=1;i<=tot;i++){
		int y=vec[x][i].first,z=vec[x][i].second;
		if(y^p)dfs2(y,x,z,i);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1,u,v,w;i<n;i++){
		scanf("%d%d%d",&u,&v,&w);
		G[u].push_back(make_pair(v,w));
		G[v].push_back(make_pair(u,w));
	}
	dfs1(1,0,0);
	dfs2(1,0,0,0);
	cout<<ans;
}

總結

換根 DP 的基本思路是兩次 dfs,第一次求任一根的所有值,第二次求所有根的根的值。

換根 DP 最需要考慮的問題是如何在第二次 dfs 中轉移,即求出一個點被吊起的答案。為了降低複雜度,常常採用預處理前後綴答案的方式來求與一個點相連的所有點中去除一個點後某種滿足結合律的運算的答案。