换根dp

Posted zcr-blog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了换根dp相关的知识,希望对你有一定的参考价值。

换根dp是用来解决一类不定根的树形dp,这种树形dp通常对于每个点做根时会有不同的答案。

换根dp通常使用二次扫描法来解决。步骤如下。

1、先推出最朴素的dp方程,即以每个点为根时的dp方程。

2、随便选一个点跑一遍普通的树形dp(一般都是自下而上的),一般都选1号节点(工具人石锤了)。

3、跑一遍自上而下的dp,即从父亲推到孩子。

我们通过两道例题简单看一看换根dp怎么实现。

ACWing 287. 积蓄程度

有一个树形的水系,由 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;
}

[POI2008]STA-Station

蛮老的一道题了。

题目大意就是让你求一个点使得以其为根时的深度和最大,输出那个点的编号(如果一样输出任意)。

题目并没有定义根节点的深度为多少,不过没关系它只要求最大的编号,而这样根节点深度为多少是无所谓的。

深度之和其实很好求,只需再额外记录子树大小即可。

#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;
}

题单:

Solution】[USACO12FEB] Nearby Cows G

【To Do】[USACO10MAR] Great Cow Gathering G

【To Do】CF708C Centroids

【To Do】CF1324F Maximum White Subtree

【To Do】[COCI2014-2015#1] Kamp

【To Do】[CEOI2017]Chase

【To Do】[CPS-s2019]树的重心

以上是关于换根dp的主要内容,如果未能解决你的问题,请参考以下文章

每日亿题--2021.5.11换根dp

树形dp--换根

换根DP

换根dp

[树形dp][换根]Maximum White Subtree

poj3585 Accumulation Degree(换根dp)