• 震惊!换根dp还能这么写!看了3sec切CSP压轴题,不看后悔一辈子!


    换根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;
    }
    

    这么实现的优点是不用再想容斥,直接套第一遍的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]树的重心

  • 相关阅读:
    自定义asp.net mvc Filter 过滤器
    基于委托的C#异步编程的一个小例子 带有回调函数的例子
    ASCII、Unicode和UTF-8编码的区别
    Specification模式的一个不错的示例代码
    codesmith 自动生成C# model 实体模板
    Quartz.NET 实现定时任务调度
    FtpHelper类匿名获取FTP文件
    crc32 根据字符串获取校验值
    机器学习能做什么
    RunHelper,一个为跑步而设计的开源的android app
  • 原文地址:https://www.cnblogs.com/zcr-blog/p/13195695.html
Copyright © 2020-2023  润新知