【动态规划】换根DP
设 \(f[u]\) 为以1为根时,u节点的子树中的答案。
设 \(g[u]\) 为以1为根时,u节点向父亲p方向看,包括父亲p但不包括u节点的子树中的答案。
换根dp的时候一定要画一张图,简单说就是画一个众字形的图,v节点在最左下角,其父亲是u节点,父亲的父亲是“众”字最顶端的节点,那么最上面的人和右边的人就是g[u],除去v节点以外的所有部分就是g[v],所以要处理的就是u节点的贡献以及v的兄弟节点的贡献。下面的实例代码中,sum没有加上u节点本身的贡献,所以直接减去contribution(v)就可以得到v的所有兄弟节点的贡献。记得要g[v]算上u节点本身的贡献。那么f[v]表示v往下看,g[v]表示除了v往下看的所有部分(包括u和v的所有兄弟),一般来说他们是加起来的。
则答案一般为 \(f[u]+g[u]\) 除非计算代价的时候有特殊的函数贡献,如下面的例题
当维护g的运算是一个群时,可以直接用逆运算去掉当前节点u的贡献。
void dfs1(int u, int p) {
f[u] = 1;
for(int &v : G[u]) {
if(v == p)
continue;
dfs1(v, u);
f[u] += f[v];
}
}
void dfs2(int u, int p) {
if(p == 0)
g[u] = 0;
int sum = 0;
for(int &v : G[u]) {
if(v == p)
continue;
sum += contribution(v);
}
for(int &v : G[u]) {
if(v == p)
continue;
g[v] = g[u] + sum - contribution(v);
dfs2(v, u);
}
}
当维护的运算是不存在逆元的半群时,要用奇奇怪怪的方法计算。例如假如是要求最大值,那么就维护第1大和第2大。
struct MAX2 {
int max1, max2;
void Init() {
max1 = -INF, max2 = -INF;
}
// Insert value v
void Insert(int v) {
if(v > max2) {
max2 = v;
if(max2 > max1)
swap(max1, max2);
}
}
// Remove value v and query max
int RemoveMax(int v) {
if(v == max1)
return max2;
return max1;
}
};
https://codeforces.com/contest/708/problem/C
题意:给一棵树,对于每个点,问是否可以在(你自己想要怎么操作就怎么操作)一次操作之后使得删去这个点之后剩下的连通块的大小都不超过n/2。操作为删除一条边然后加上一条边,并保证操作之后仍然是一棵树。
题解:设siz[u]为以1为根时,子树的siz。设 X[u] 为 以 u 为根时,最大的子树的siz,设Y1[u] 为以 1为根时,u节点内的不超过n/2的最大的子树的siz,设 Y2[u] 为从以 1 为根换到以u为根时,从u节点方向往下看,看到p节点内的不超过n/2的最大的子树的siz。答案为X[u]<=n/2,或者以u为根时,那棵唯一的最大的子树减去其不超过n/2的最大的子树(并把这个子树直接连接到u上面)之后的siz不超过n/2。
麻烦的是Y2[u],Y2[u]=n-siz[u]\leq n/2 ? n-siz[u] : max(Y2[p],\max_{v\in ch[p],v\neq u}Y1[v]) ,后面这个遍历p节点的子树的所有Y1[v],然后除去Y1[u]之后的最大值,需要用上面的MAX2数据结构来维护。
https://codeforces.com/contest/1882/problem/D
这个是换根dp里面比较可怕的类型,首先是contribution里引入了siz的因素,第二就是答案并不是f[u]+g[u]了,还跟黑白有关。
简单说:f[u][0]表示把子树u染成0色的代价,f[u][1]表示把子树u染成1色的代价,小心点维护可以做到。
然后开始换根,换根dp的时候一定要画一张图!
int n;
int a[200005];
int bi[200005];
vector<int> G[200005];
ll ans[200005];
int siz[200005];
ll f[200005][2]; // 以1为根时,把以u为根节点的子树全部染成颜色c的代价
ll g[200005][2]; // 以1为根时,除去以u为根节点的剩余部分全部染成颜色c的代价
ll contribution (int v, int c) {
// 把v子树染成颜色c的代价
return min (f[v][c], f[v][1 - c] + siz[v]);
}
void dfs (int u, int p) {
// 把u为根的子树全部染色为0或者染色为1的最小花费
ll sum[2] = {};
siz[u] = 1;
for (int v : G[u]) {
if (v == p)
continue;
dfs (v, u);
siz[u] += siz[v];
sum[0] += contribution (v, 0); // 把v直接染成0
sum[1] += contribution (v, 1); // 把v直接染成1
}
// 现在的sum[0]和sum[1]不包括根节点的颜色,表示把除了u以外的所有点都染成c色的代价
if (bi[u] == 0) {
f[u][0] = min (sum[0] + 0, sum[1] + siz[u] - 1);
f[u][1] = min (sum[1] + siz[u] - 1 + siz[u], sum[0] + siz[u]);
} else {
// 这种题都是完全对称的
f[u][1] = min (sum[1] + 0, sum[0] + siz[u] - 1);
f[u][0] = min (sum[0] + siz[u] - 1 + siz[u], sum[1] + siz[u]);
}
}
void dfs2 (int u, int p) {
if (p == 0) {
g[u][0] = 0;
g[u][1] = 0;
}
ll sum[2] = {};
for (int &v : G[u]) {
if (v == p)
continue;
sum[0] += contribution (v, 0);
sum[1] += contribution (v, 1);
}
for (int &v : G[u]) {
if (v == p)
continue;
if (bi[u] == 0) {
// 画个图
g[v][0] = min (g[u][0] + (sum[0] - contribution (v, 0)),
g[u][1] + (sum[1] - contribution (v, 1)) + (n - siz[v] - 1));
g[v][1] = min (g[u][0] + (sum[0] - contribution (v, 0)) + (n - siz[v]),
g[u][1] + (sum[1] - contribution (v, 1)) + (n - siz[v] - 1) + (n - siz[v]));
} else {
// 这种题都是完全对称的
g[v][1] = min (g[u][0] + (sum[0] - contribution (v, 0) + (n - siz[v] - 1)),
g[u][1] + (sum[1] - contribution (v, 1)));
g[v][0] = min (g[u][0] + (sum[0] - contribution (v, 0)) + (n - siz[v] - 1) + (n - siz[v]),
g[u][1] + (sum[1] - contribution (v, 1)) + (n - siz[v]));
}
dfs2 (v, u);
}
}
void solve() {
RD (n);
RDN (a, n);
for (int i = 1; i <= n; ++i) {
G[i].clear ();
}
for (int i = 1; i <= n - 1; ++i) {
int u, v;
RD (u, v);
G[u].push_back (v);
G[v].push_back (u);
}
for (int i = 1; i <= n; ++i) {
ans[i] = 0;
}
for (int b = 0; b < 20; ++b) {
for (int i = 1; i <= n; ++i) {
bi[i] = (a[i] >> b) & 1;
f[i][0] = 0;
f[i][1] = 0;
g[i][0] = 0;
g[i][1] = 0;
}
dfs (1, 0);
dfs2 (1, 0);
for (int i = 1; i <= n; ++i) {
if (bi[i] == 0) {
ll sum0 = f[i][0] + g[i][0];
ll sum1 = f[i][1] + g[i][0]; // 由于i节点本身是0色,所以f[i][1]的时候就会把g[i][0]染成1色
ll ansb = min (sum0, sum1) * 1LL * (1LL << b);
ans[i] += ansb;
} else {
ll sum0 = f[i][0] + g[i][1];
ll sum1 = f[i][1] + g[i][1]; // 同上,对称
ll ansb = min (sum0, sum1) * 1LL * (1LL << b);
ans[i] += ansb;
}
}
}
WTN (ans, n);
}