换根 dp 学习笔记
例题 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=fu−sizv+n−sizv=n−2×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 1∼i 的距离, 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=fu−valv+n−valv=fu+n−2⋅valv
#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
v→u 一条边是好的,根据乘法原理相乘即可。
现在进行换根 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}
fu←fv+1fu
所以
f
v
←
f
v
×
(
f
u
+
1
)
f_v \gets f_v \times (f_u + 1)
fv←fv×(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
fv←fv×(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=1∏j(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)=v∏j(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;
}
2406

被折叠的 条评论
为什么被折叠?



