换根 dp 学习笔记

换根 dp 学习笔记

来自 dalao 的题单

例题 STA-Station

给定一个 n n n 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

u u u 为当前节点, v v v u u u 的其中一个儿子。令 f u f_u fu 表示以 u u u 为根的时候,深度之和的最大值。那么当根从 u u u 换到 v v v 时, v v v 子树的深度减少了 1 1 1,而非 v v v 子树的节点深度 + 1 +1 +1,所以:
f v = f u − s i z v + n − s i z v = n − 2 × s i z v f_v = f_u - siz_v + n - siz_v = n - 2 \times siz_v fv=fusizv+nsizv=n2×sizv

例题 Educational Codeforces Round 67, Problem E, Tree Painting

给定一个由 n n n 个顶点组成的树。你在这棵树上玩游戏。

最初所有的顶点都是白色的。在游戏的第一个回合中,你选择一个顶点并将其涂成黑色。然后在每个回合中,你选择一个与任何黑色顶点相邻的白色顶点 v v v(通过一条边连接),并将其涂成黑色。获得的点数 v v v 所在的白色连通块大小。

你的任务是使你获得的点数最大化。

容易发现,假如根为定,那么染色所获得的代价就是子树的大小,如上即可。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 5;
vector<int> g[N];
int n, siz[N];
ll f[N];

void dfs1(int u, int fa){
	siz[u] = 1;
	for(auto v : g[u]){
		if(v == fa)	continue;
		dfs1(v, u);
		siz[u] += siz[v];
	}
	f[1] += siz[u];
}

void dfs2(int u, int fa){
	for(auto v : g[u]){
		if(v == fa)	continue;
		f[v] = f[u] + n - 2 * siz[v];
		dfs2(v, u);
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	for(int i = 1;i < n;i++){
		int u, v;	cin>>u>>v;
		g[u].push_back(v), g[v].push_back(u);
	}
	dfs1(1, 0);
	dfs2(1, 0);
	ll ans = INT_MIN;
	for(int i = 1;i <= n;i++){
		ans = max(ans, f[i]);
	}
	cout<<ans;
	return 0;
}

例题 Codeforces Round 867, F. Gardening Friends

给定一个根为 1 1 1 的树,每条边长度为 k k k,每次可以花费 c c c 的代价将根移动到相邻的节点。选定根之后,可以获得根到顶点的最大距离的代价。

求代价的最大值。

前提知识,树的直径的性质:令树的直径两端为 a , b a, b a,b,那么 ∀ i \forall i i 到树的其他节点的最长距离,要么是 i i i a a a 的距离,要么是 i i i b b b 的距离。

所以这题可以先求出来树的直径,然后枚举 i i i,令根为 i i i,那么求 1 ∼ i 1 \sim i 1i 的距离, i i i a a a 的距离即可。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 5;
vector<int> g[N];
int n, k, c;
int dis1[N], disa[N], disb[N]; 

void dfs1(int u, int fa){
	for(auto v : g[u]){
		if(v == fa)	continue;
		dis1[v] = dis1[u] + 1;
		dfs1(v, u);
	}
}

void dfs2(int u, int fa){
	for(auto v : g[u]){
		if(v == fa)	continue;
		disa[v] = disa[u] + 1;
		dfs2(v, u);
	}
}

void dfs3(int u, int fa){
	for(auto v : g[u]){
		if(v == fa)	continue;
		disb[v] = disb[u] + 1;
		dfs3(v, u);
	}
}

void sol(){
	cin>>n>>k>>c;
	for(int i = 1;i < n;i++){
		int u, v;	cin>>u>>v;
		g[u].push_back(v), g[v].push_back(u);
	}
	dis1[1] = 0;
	dfs1(1, 0);
	
	priority_queue<pair<int, int>> q;
	for(int i = 1;i <= n;i++)	q.push({dis1[i], i});
	int a = q.top().second;
	disa[a] = 0;
	dfs2(a, 0);
	
	while(q.size())	q.pop();
	for(int i = 1;i <= n;i++)	q.push({disa[i], i});
	int b = q.top().second;
	disb[b] = 0;
	dfs3(b, 0);
	
	ll ans = 0;
	for(int i = 1;i <= n;i++){
		ans = max(ans, 1ll * max(disa[i], disb[i]) * k - 1ll * dis1[i] * c);
	}
	cout<<ans<<'\n';
	for(int i = 1;i <= n;i++)	g[i].clear();
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int _;	cin>>_;
	while(_--)	sol();
	return 0;
}

例题 Codeforces Round 527 (Div. 3), F. Tree with Maximum Cost

给定一棵由 n n n 个顶点组成的树,和数组 { a n } \{a_n\} {an}。任选一点 v v v,使得 ∑ d i s ( i , v ) ⋅ a i \sum dis(i, v) \cdot a_i dis(i,v)ai 最小。

d i s ( a , b ) dis(a, b) dis(a,b) 表示树上 a a a b b b 的距离。

容易发现,当根从 u u u v v v 转移的时候,对 v v v 的子树贡献少了「 v v v 的子树点权和」的贡献,对其他节点,又多了「总权值和 − v -v v 的子树点权和」的贡献。

所以推出式子( v a l u val_u valu 表示 u u u 的子树点权和, f u f_u fu 表示以 u u u 为根的时候的答案):
f v = f u − v a l v + n − v a l v = f u + n − 2 ⋅ v a l v f_v = f_u - val_v + n - val_v = f_u + n - 2 \cdot val_v fv=fuvalv+nvalv=fu+n2valv

#include <bits/stdc++.h>
#define pii pair<int, int>
#define pb push_back
using namespace std;
using ll = long long;
const int N = 2e5 + 5; 
int n, a[N];
vector<int> g[N];
ll val[N], f[N];

void dfs1(int u, int fa, int dep){
	val[u] = a[u];
	f[1] += 1ll * dep * a[u];
	for(auto v : g[u]){
		if(v == fa)	continue;
		dfs1(v, u, dep + 1);
		val[u] += val[v];
	}
}

void dfs2(int u, int fa){
	for(auto v : g[u]){
		if(v == fa)	continue;
		f[v] = f[u] + val[1] - 2 * val[v];
		dfs2(v, u);
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	for(int i = 1;i <= n;i++)	cin>>a[i];
	for(int i = 1;i < n;i++){
		int u, v;	cin>>u>>v;
		g[u].pb(v), g[v].pb(u);
	} 
	dfs1(1, 0, 0);
	dfs2(1, 0);
	ll ans = 0;
	for(int i = 1;i <= n;i++){
		ans = max(ans, f[i]);
	}
	cout<<ans;
	return 0;
}

例题 [Codeforces Round 302 (Div. 1), D. Road Improvement](Problem - 543D - Codeforces)

这个国家的交通网络是一棵有 n n n 个城市的树。城市用整数从 1 1 1 n n n 进行编号。

最初所有的公路都是坏的,政府可以修好道路。如果从位于 x x x 的首都到任何其他城市的路径最多包含一条坏路,那么市民对道路改善感到满意。

你需要为每一个可能的 x x x 确定「让市民满意的」改善道路质量的方法的数量,答案对 1 0 9 + 7 10^9+7 109+7 取模。

对于节点 u u u,令 f u f_u fu 表示 u u u 的子树到 u u u 有且仅有一条坏边,考虑转移方程:
f u = ∏ v = s o n ( u ) ( f v + 1 ) f_u = \prod_{v = son(u)} (f_v + 1) fu=v=son(u)(fv+1)
意思是 u u u 的儿子子树的坏边的方案 + + + 只有 v → u v \to u vu 一条边是好的,根据乘法原理相乘即可。

现在进行换根 DP。假如从 u u u 换到 v v v,那么现在 u u u v v v 的子树。在 u u u 成为 v v v 的子树之前, u u u 的方案是 f u f_u fu。而成为之后,有:
f u ← f u f v + 1 f_u \gets \frac{f_u}{f_v + 1} fufv+1fu
所以
f v ← f v × ( f u + 1 ) f_v \gets f_v \times (f_u + 1) fvfv×(fu+1)
注意:上面的式子是已经修改过后的 f u f_u fu

如果把它换成没有修改的 f u f_u fu,就是:
f v ← f v × ( f u f v + 1 + 1 ) = f u f v f v + 1 + f v f_v \gets f_v \times (\frac{f_u}{f_v + 1} +1) = \frac{f_uf_v}{f_v +1} + f_v fvfv×(fv+1fu+1)=fv+1fufv+fv
所以,~~居然需要逆元,~~还需要用逆元来处理 ( f v + 1 ) − 1 (f_v + 1) ^ {-1} (fv+1)1

实际上,存在一个问题,就是 f v + 1 f_v + 1 fv+1 可能是 0 0 0,而 0 0 0 没有逆元,所以这道题不能使用逆元处理,正确的做法使用前缀积。

vector<int> k[N]k[i][j] 表示 i i i 的第 j j j 个儿子以及之前的乘积。形式化的讲( s o n ( i ) son(i) son(i) 表示给定节点,第 i i i 个编号的儿子):
k i , j = ∏ k = 1 j ( f s o n ( k ) + 1 ) k_{i, j} = \prod_{k = 1}^{j}(f_{son(k)} + 1) ki,j=k=1j(fson(k)+1)
g u g_u gu 表示 u u u 为根的时候的答案,如果将根从 u u u 转移到 v v v,那么有:
g v = f v × ( ∏ k = 1 , s o n ( k ) ≠ v j ( f s o n ( k ) + 1 ) + 1 ) g_v = f_v \times (\prod_{k = 1, son(k) \not= v}^j(f_{son(k)} + 1) + 1) gv=fv×(k=1,son(k)=vj(fson(k)+1)+1)

看来 CF *2300 还是值得的。

#include <bits/stdc++.h>
#define pii pair<int, int>
#define pb push_back
using namespace std;
using ll = long long;
const int N = 2e5 + 5;
const int mod = 1e9 + 7;
vector<int> G[N];
vector<ll> f1[N], f2[N];
ll f[N], ans[N];
int n;

void dfs1(int u, int fa){
	f[u] = 1;
	for(auto v : G[u]){
		if(v == fa)	continue;
		dfs1(v, u);
		f[u] *= f[v] + 1;
		f[u] %= mod;
	}
}

void dfs2(int u, int fa){
	int i = 0;
	ans[u] = 1;
	for(auto v : G[u]){
		ans[u] = ans[u] * (f[v] + 1) % mod;
		if(v == fa)	continue;
		f1[u].pb(f[v] + 1), f2[u].pb(f[v] + 1);
	} 
	for(int i = 1;i < f1[u].size();i++){
		int j = f1[u].size() - i - 1;
		f1[u][i] = f1[u][i] * f1[u][i-1] % mod;
		f2[u][j] = f2[u][j] * f2[u][j+1] % mod;
	}
	for(auto v : G[u]){
		if(v == fa)	continue;
		f[u] = (fa ? f[fa] + 1 : 1);
		if(i)	f[u] = f[u] * f1[u][i-1] % mod;
		if(i + 1 < f1[u].size())
			f[u] = f[u] * f2[u][i+1] % mod;
		dfs2(v, u);
		i++;
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	for(int i = 2;i <= n;i++){
		int x;	cin>>x;
		G[x].pb(i);
		G[i].pb(x);
	}
	dfs1(1, 0);
	dfs2(1, 0);
	for(int i = 1;i <= n;i++){
		cout<<ans[i]<<" ";
	}
	return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值