You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6.7 KiB
6.7 KiB
换根DP
换根DP
,又叫二次扫描,是树形DP
的一种。
其相比于一般的树形DP
具有以下特点:
- ① 以树上的不同点作为根,其解不同
- ② 故为求解答案,不能单求某点的信息,需要求解每个节点的信息
- ③ 故无法通过一次搜索完成答案的求解,因为一次搜索只能得到一个节点的答案
难度也就要比一般的树形
DP
高一点。
题单
POI2008
STA-Station
题意:给定一个
n
个点的无根树,问以树上哪个节点为根时,其所有节点的深度和最大? 深度:节点到根的简单路径上边的数量
如果我们假设某个节点为根,将无根树化为有根树,在搜索回溯时统计子树的深度和,则可以用一次搜索算出以该节点为根时的深度和,其时间复杂度为 O(N)
。
但这样求解出的答案只是以该节点为根的,并不是最优解。
如果要暴力求解出最优解,则我们可以枚举所有的节点为根,然后分别跑一次搜索,这样的时间复杂度会达到O(N^2)
,显然不可接受。
所以我们考虑在第二次搜索时就完成所有节点答案的统计——
-
① 我们假设第一次搜索时的根节点为
1
号节点,则此时只有1
号节点的答案是已知的。同时第一次搜索可以统计出所有子树的大小。 -
② 第二次搜索依旧从
1
号节点出发,若1
号节点与节点x
相连,则我们考虑能否通过1
号节点的答案去推出节点x
的答案。 -
③ 我们假设此时将根节点换成节点
x
,则其子树由两部分构成,第一部分是其原子树,第二部分则是1
号节点的其他子树(如下图)。
- ④ 根从
1
号节点变为节点x
的过程中,我们可以发现第一部分的深度降低了1
,第二部分的深度则上升了1
,而这两部分节点的数量在第一次搜索时就得到了。
故得到递推公式:
f[v]=f[u]-siz[v]+(siz[1]-siz[v]),fa[v]=u
简化一下就是
f[v]=f[u]+siz[1]-2\times siz[v]=f[u]+n-2\times siz[v]
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010, M = N << 1;
#define int long long
#define endl "\n"
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int n; // n个节点
int depth[N]; // depth[i]:在以1号节点为根的树中,i号节点的深度是多少
int sz[N]; // sz[i]:以i号节点为根的子树中有多少个节点
int f[N]; // DP结果数组,f[i]记录整个树以i为根时,可以获取到的深度和是多少
// 第一次dfs
void dfs1(int u, int fa) {
sz[u] = 1; // 以u为根的子树,最起码有u一个节点
depth[u] = depth[fa] + 1; // u节点的深度是它父节点深度+1
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
dfs1(v, u); // 深搜v节点,填充 sz[v],depth[v]
sz[u] += sz[v]; // 在完成了sz[v]和depth[v]的填充工作后,利用儿子更新父亲的sz[u]+=sz[v];
}
}
// 第二次dfs
void dfs2(int u, int fa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
f[v] = f[u] + n - 2 * sz[v];
dfs2(v, u);
}
}
signed main() {
memset(h, -1, sizeof h); // 初始化链式前向星
cin >> n;
for (int i = 1; i < n; i++) { // n-1条边
int a, b;
cin >> a >> b;
add(a, b), add(b, a); // 换根DP,无向图
}
// 1、第一次dfs,以1号节点为根,它的父节点不存在,传入0
dfs1(1, 0);
// 2、换根
for (int i = 1; i <= n; i++) f[1] += depth[i]; // DP初始化,以1号节点为根时,所有节点的深度和
dfs2(1, 0); // 从1号节点开始,深度进行换根
// 3、找答案
int ans = 0, id = 0;
for (int i = 1; i <= n; i++) // 遍历每个节点
if (ans < f[i]) ans = f[i], id = i; // ans记录最大的深度值,id记录以哪个节点为根时取得最大值
// 输出以哪个节点为根时,深度和最大
cout << id << endl;
}
总结与进阶
由此我们可以看出换根DP
的套路:
- 指定某个节点为根节点。
- 第一次搜索完成预处理(如子树大小等),同时得到该节点的解。
- 第二次搜索进行换根的动态规划,由已知解的节点推出相连节点的解。
[USACO10MAR]Great Cow Gathering G
CF1324F.Maximum White Subtree
[USACO12FEB]Nearby Cows G
[COCI2014-2015#1]Kamp
[APIO2014]连珠线
POJ3585 Accumulation Degree
CF708C Centroids
AcWing
1072
树的最长路径
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = N << 1;
int n; // n个结点
// 链式前向星
int h[N], e[M], w[M], ne[M], idx;
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// 换根dp模板
int ans; // 答案,直径
int d1[N], d2[N]; // d1[i],d2[i]:经过i点的最长,次长长度是多少
bool st[N]; // 是不是遍历过了
void dfs(int u) {
st[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (st[v]) continue; // v点访问过了
// 走v子树,完成后,v子树中每个节点的d1[v],d2[v]都已经准备好,u节点可以直接利用
dfs(v);
// w[i]:u->v的路径长度,d1[u]:最长路径,d2[u]:次长路径
if (d1[v] + w[i] >= d1[u]) // v可以用来更新u的最大值
d2[u] = d1[u], d1[u] = d1[v] + w[i]; // 最长路转移
else if (d1[v] + w[i] > d2[u])
d2[u] = d1[v] + w[i]; // 次长路转移
}
// 更新结果
ans = max(ans, d1[u] + d2[u]);
}
int main() {
cin >> n;
memset(h, -1, sizeof h); // 初始化邻接表
for (int i = 1; i < n; i++) { // n-1条边
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c); // 换根dp一般用于无向图
}
dfs(1); // 任选一个点作为根节点,此处选择的是肯定存在的1号结点
cout << ans << endl;
return 0;
}