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.

10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

换根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的套路:

  • 指定某个节点为根节点。
  • 第一次搜索完成预处理(如子树大小等),同时得到该节点的解。
  • 第二次搜索进行换根的动态规划,由已知解的节点推出相连节点的解。

账号10402852@qq.com 密码m****2

题目大意 给你一棵树的结点数nn-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;
}

AcWing 1073. 树的中心

AcWing 1148 秘密的牛奶运输