换根dp是用来解决一类不定根的树形dp,这种树形dp通常对于每个点做根时会有不同的答案。
换根dp通常使用二次扫描法来解决。步骤如下。
1、先推出最朴素的dp方程,即以每个点为根时的dp方程。
2、随便选一个点跑一遍普通的树形dp(一般都是自下而上的),一般都选1号节点(工具人石锤了)。
3、跑一遍自上而下的dp,即从父亲推到孩子。
我们通过两道例题简单看一看换根dp怎么实现。
有一个树形的水系,由 N-1 条河道和 N 个交叉点组成。
我们可以把交叉点看作树中的节点,编号为 1~N,河道则看作树中的无向边。
每条河道都有一个容量,连接 x 与 y 的河道的容量记为 c(x,y)。
河道中单位时间流过的水量不能超过河道的容量。
有一个节点是整个水系的发源地,可以源源不断地流出水,我们称之为源点。
除了源点之外,树中所有度数为 1 的节点都是入海口,可以吸收无限多的水,我们称之为汇点。
也就是说,水系中的水从源点出发,沿着每条河道,最终流向各个汇点。
在整个水系稳定时,每条河道中的水都以单位时间固定的水量流向固定的方向。
除源点和汇点之外,其余各点不贮存水,也就是流入该点的河道水量之和等于从该点流出的河道水量之和。
整个水系的流量就定义为源点单位时间发出的水量。
在流量不超过河道容量的前提下,求哪个点作为源点时,整个水系的流量最大,输出这个最大值。
先考虑暴力。
假设当前根为1,设(dp[u])代表整个水系为(u)的子树时的最大流量。
有转移方程:(dp[u]=sum_{v是u的儿子} min(val(u,v),dp[v]))。若一个点为叶节点时它的(dp)值是(inf)。
之后以每个点为根跑一遍,取最大值即可。这样做的复杂度是(O(n^2))的。
怎么优化呢?用我之前说的,先以(1)为根跑一遍普通的树形dp,设(f[u])代表以(u)为根时的最大值。显然有(f[1]=dp[1])。
之后对于树上的一条边((u,v)),用(f[u])来更新(f[v])。
我们来观察(v)的构成:它的子树以及其它部分。
所以(f[v])一定包含(dp[v]),即它自己的子树。那除了它的子树的部分怎么求呢?其实就是(f[u]-min(f[v], val(u,v))),然后再与(val(u,v))取(min)。
注意边界。在之前我说叶节点为无限,在这里就会有问题,要设成0。而转移时就要特判叶节点。具体看代码吧。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 200010;
const int inf = 0x3f3f3f3f;
template <typename T> void read(T &x) {
T w = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') w = -1;
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
x *= w;
}
struct node{
int pre, to, val;
}edge[N << 1];
int head[N], tot;
int T;
int n;
int dp[N], f[N];
int ans;
int deg[N];
void add(int u, int v, int l) {
edge[++tot] = node{head[u], v, l};
head[u] = tot;
}
void dfs1(int x, int fa) {
dp[x] = 0;
for (int i = head[x]; i; i = edge[i].pre) {
if (edge[i].to == fa) continue;
dfs1(edge[i].to, x);
if (deg[edge[i].to] == 1) dp[x] += edge[i].val;
else dp[x] += min(edge[i].val, dp[edge[i].to]);
}
}
void dfs2(int x, int fa) {
for (int i = head[x]; i; i = edge[i].pre) {
if (edge[i].to == fa) continue;
f[edge[i].to] = dp[edge[i].to];
if (deg[x] == 1) f[edge[i].to] += edge[i].val;
else if (deg[edge[i].to] == 1) f[edge[i].to] += min(f[x] - edge[i].val, edge[i].val);
else f[edge[i].to] += min(f[x] - min(dp[edge[i].to], edge[i].val), edge[i].val);
dfs2(edge[i].to, x);
}
}
int main() {
read(T);
while (T--) {
read(n);
for (int i = 1; i <= n; i++) head[i] = deg[i] = 0;
tot = 0;
ans = 0;
for (int i = 1, x, y, z; i < n; i++) {
read(x); read(y); read(z);
add(x, y, z);
add(y, x, z);
deg[x]++;
deg[y]++;
}
dfs1(1, 0);
f[1] = dp[1];
dfs2(1, 0);
for (int i = 1; i <= n; i++) ans = max(ans, f[i]);
cout << ans << "
";
}
return 0;
}
蛮老的一道题了。
题目大意就是让你求一个点使得以其为根时的深度和最大,输出那个点的编号(如果一样输出任意)。
题目并没有定义根节点的深度为多少,不过没关系它只要求最大的编号,而这样根节点深度为多少是无所谓的。
深度之和其实很好求,只需再额外记录子树大小即可。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 1000010;
const int inf = 0x3f3f3f3f;
template <typename T> void read(T &x) {
T w = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') w = -1;
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
x *= w;
}
struct node{
int pre, to;
}edge[N << 1];
int head[N], tot;
int n;
ll sz[N], dp[N], f[N];
void add(int u, int v) {
edge[++tot] = node{head[u], v};
head[u] = tot;
}
void dfs1(int x, int fa) {
sz[x] = 1;
for (int i = head[x]; i; i = edge[i].pre) {
int y = edge[i].to;
if (y == fa) continue;
dfs1(y, x);
dp[x] += dp[y] + sz[y];
sz[x] += sz[y];
}
}
void dfs2(int x, int fa) {
for (int i = head[x]; i; i = edge[i].pre) {
int y = edge[i].to;
if (y == fa) continue;
f[y] = dp[y] + f[x] - dp[y] - sz[y] + n - sz[y];
dfs2(y, x);
}
}
int ans;
ll mx;
int main() {
read(n);
for (int i = 1, u, v; i < n; i++) {
read(u); read(v);
add(u, v);
add(v, u);
}
dfs1(1, 0);
f[1] = dp[1];
dfs2(1, 0);
for (int i = 1; i <= n; i++) {
if (f[i] > mx) {
ans = i;
mx = f[i];
}
}
cout << ans;
return 0;
}
换根dp的另类写法。
我们上面的换根dp是基于容斥的思想的,其实还有一种真·换根dp。
假设我们第二遍dfs时的树根即为x。我们现在想要把树根换成y。那就要有一个函数change_root(x,y)代表将原本的根x换成y。
change_root又由两部分组成,第一部分是cut,第二部分是link。
其实如果我们将x设为树的根的时候可以将树看成一个有向图。如果我们将x->y这条有向边切掉,然后连一条y->x的有向边那是不是就是将y变成了根。
然后递归下去,最后将根从y再改成x。
这样写第二题:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 1000010;
const int inf = 0x3f3f3f3f;
template <typename T> void read(T &x) {
T w = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') w = -1;
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
x *= w;
}
struct node{
int pre, to;
}edge[N << 1];
int head[N], tot;
int n;
ll sz[N], dp[N];
void add(int u, int v) {
edge[++tot] = node{head[u], v};
head[u] = tot;
}
void dfs1(int x, int fa) {//第一个dfs不用改
sz[x] = 1;
for (int i = head[x]; i; i = edge[i].pre) {
int y = edge[i].to;
if (y == fa) continue;
dfs1(y, x);
dp[x] += dp[y] + sz[y];
sz[x] += sz[y];
}
}
void cut(int x, int y) {
dp[x] -= dp[y] + sz[y];
sz[x] -= sz[y];
}
void link(int x, int y) {
dp[x] += dp[y] + sz[y];
sz[x] += sz[y];
}
void change_root(int x, int y) {
cut(x, y);
link(y, x);
}
int ans;
ll mx;
void dfs2(int x, int fa) {
if (dp[x] > mx) {
ans = x;
mx = dp[x];
}
for (int i = head[x]; i; i = edge[i].pre) {
int y = edge[i].to;
if (y == fa) continue;
change_root(x, y);
dfs2(y, x);
change_root(y, x);
}
}
int main() {
read(n);
for (int i = 1, u, v; i < n; i++) {
read(u); read(v);
add(u, v);
add(v, u);
}
dfs1(1, 0);
dfs2(1, 0);
cout << ans;
return 0;
}
这么实现的优点是不用再想容斥,直接套第一遍的dp方程即可。而缺点是常数略大(其实也没大到哪去)。
总结
换根dp的特点是不定根(有可能要你求每个点做根的情况),这种情况我们通常可以考虑换根。
具体步骤是先推出一个点做根时的dp方程,然后考虑将其换为其儿子,递归去做这一步,最后再换回来。
或者考虑子树内和子树外容斥来做都是可以的。
树形dp通常有两种方程:
1、(dp_u=sum_{(u,v) in E} dp_v)
对于这种dp方程直接减掉再加回来即可。典型的有求子树大小等。
2、(dp_u=max_{(u,v) in E} dp_v)
这种情况存一个最大的然后分类讨论再重新求依然可以在(O(n))的时间复杂度内解决问题。典型的有求重儿子等。(这种方法好像之前没有人提到过)
题单:
【Solution】[USACO12FEB] Nearby Cows G
【Solution】[USACO10MAR] Great Cow Gathering G
【Solution】CF708C Centroids
【Done】CF1324F Maximum White Subtree
【Done】[COCI2014-2015#1] Kamp
【To Do】[CEOI2017]Chase(涉及概率期望的知识、待填坑)
【Solution】[CPS-s2019]树的重心