10 KiB
换根DP
换根DP
,又叫二次扫描,是树形DP
的一种。
其相比于一般的树形DP
具有以下特点:
- ① 以树上的不同点作为根,其解不同
- ② 故为求解答案,不能单求某点的信息,需要求解每个节点的信息
- ③ 故无法通过一次搜索完成答案的求解,因为一次搜索只能得到一个节点的答案
难度也就要比一般的树形
DP
高一点。
题单
P3478
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
的套路:
- 指定某个节点为根节点。
- 第一次搜索完成预处理(如子树大小等),同时得到该节点的解。
- 第二次搜索进行换根的动态规划,由已知解的节点推出相连节点的解。
C
. Link
Cut
Centroids
(求树的重心)
账号:
10402852@qq.com
密码:m****2
题目大意
给你一棵树的结点数n
和n-1
条边,你可以删除一条边再增加一条边,使得树的重心唯一
性质:
① 删除重心后所得的所有子树,节点数不超过原树的1/2
,一棵树最多有两个重心
② 树中所有节点到重心的距离之和最小,如果有两个重心,那么他们距离之和相等
③ 两个树通过一条边合并,新的重心在原树两个重心的路径上
④ 树删除或添加一个叶子节点,重心最多只移动一条边
⑤ 一棵树最多有两个重心,且相邻
树的重心定义为树的某个节点,当去掉该节点后,树的各个连通分量中,节点数最多的连通分量其节点数达到最小值。树可能存在多个重心。如下图,当去掉点1
后,树将分成两个连通块:(2,4,5),(3,6,7)
,则最大的连通块包含节点个数为3
。若去掉点2
,则树将分成3
个部分,(4),(5),(1,3,6,7)
最大的连通块包含4
个节点;第一种方法可以 得到更小的最大联通分量。可以发现,其他方案不可能得到比3
更小的值了。所以,点1
是树的重心。
思路
- 如果找到只有一个重心,那么直接删一个重心的直连边然后加回去就好
- 如果找到两个重心,那么在其中一个重心上找到一个直连点不是另一个重心,删除连另外一个就好
如何求树的重心?
1、先任选一个结点作为根节点(比如1
号节点),把无根树变成有根树。然后设sz[i]
表示以i
为根节点的子树节点个数。转移方程为\displaystyle sz[u]=\sum_{fa[v]=u} (sz[v])
2、设son[i]
表示删去节点i
后剩下的连通分量中最大子树节点个数。其中一部分在原来i
其为根的子树。\displaystyle son[i]=max(son[i],sz[j])
解释:
j
的含义是i
的所有儿子节点
另外一部分在i
的 上方 子树有n-sz[i]
个。
son[i]=max(son[i],n-sz[i])
#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<cstdio>
#include<algorithm>
#define debug(a) cout<<#a<<"="<<a<<endl;
using namespace std;
const int maxn=1e5+100;
typedef long long LL;
vector<LL>g[maxn];
LL siz[maxn],son[maxn];
LL r1,r2,n;
void dfs(LL u,LL fa)
{
siz[u]=1;
son[u]=0;
for(LL i=0;i<g[u].size();i++){
LL v=g[u][i];
if(v==fa) continue;
dfs(v,u);
siz[u]+=siz[v];
son[u]=max(son[u],siz[v]);
}
son[u]=max(son[u],n-siz[u]);
if((son[u]<<1)<=n) r2=r1,r1=u;
}
int main(void)
{
cin.tie(0);std::ios::sync_with_stdio(false);
LL t;cin>>t;
while(t--){
cin>>n;
for(LL i=0;i<=n+10;i++) g[i].clear(),siz[i]=0,son[i]=0;
for(LL i=1;i<n;i++){
LL x,y;cin>>x>>y;
g[x].push_back(y);g[y].push_back(x);
}
r1=r2=0;
dfs(1,0);
if(!r2){
LL r3=g[r1][0];
cout<<r1<<" "<<r3<<endl;
cout<<r1<<" "<<r3<<endl;
}
else{
LL r3=r1;
// debug(r1);debug(r2);
for(LL i=0;i<g[r2].size();i++){
r3=g[r2][i];
// debug(r3);
if(r3!=r1) break;
}
cout<<r3<<" "<<r2<<endl;
cout<<r3<<" "<<r1<<endl;
}
}
return 0;
}
P1364
医院设置
P2986
Great
Cow
Gathering
G
https://blog.csdn.net/zstuyyyyccccbbbb/article/details/108952302
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;
}