【二次扫描与换根】总结

定义

在一棵无根树上需要以多个节点为根求解答案,可以运用二次扫描与换根法
具体操作是通过实现一次自底向上的深度优先搜索和一次自顶向下的深度优先搜索来计算“换根”后的解。

算法

对于这类题目,我们不妨令 u u u 为根,进行一遍树形DP,求解出该节点为根时的答案。考虑将 u → v u \rightarrow v uv,由于树的根发生了变化,故产生了状态转移方程,那么该算法便是要求出换根的转移方式

算法大致流程如下:

  1. 第1次扫描:任选一个结点为根出发,执行一次深度优先搜索,在递归回溯时自底向上进行状态转移,用子节点的状态更新父节点的状态。
  2. 第2次扫描:从刚才选出的跟出发,再进行一次深度优先搜索,在每次递归前都自顶向下进行状态转移,用父节点的状态更新子节点的状态,计算出“换根”后的结果。

实现

以模板题Sta为例

题意简述:给出一个有 n 个节点的树,试求出节点 u,满足以 u 为根时各节点深度之和最大。

根据二次扫描的套路,不妨先考虑,已知树根为某节点 x 时如何求出答案。
对于该种情况,以 x 为起点,通过 dfs 求出每个节点的深度,并累加即可。

接下来考虑,若要将树根 u → v u \rightarrow v uv 应如何转移。

如上图所示,若要将树根 5 → 4 5 \rightarrow 4 54,较易发现,蓝色部分节点深度都减小了 1,红色部分深度都增加了 1。而蓝色部分对应的是以 4 为根的子树,红色部分是除了以 4 为根的子树的剩余节点。

从而我们求得了换根方式,当 u → v u \rightarrow v uv 时, d p v = d p u + ( n − s i z v ) − s i z v dp_v = dp_u + (n - siz_v) - siz_v dpv=dpu+(nsizv)sizv,整理可得 d p v = d p u + n − 2 × s i z v dp_v = dp_u + n - 2 \times siz_v dpv=dpu+n2×sizv,其中 d p u \mathrm{dp_u} dpu 表示以 u 为根时的答案, s i z u \mathrm{siz_u} sizu 表示以 u 为根的子树大小。

得出了换根方式,通过二次 dfs 便可求出答案。

#include<bits/stdc++.h> 
#define ll long long
using namespace std;
const int MAXN = 2e6 + 5;
int n , cnt , head[MAXN] , siz[MAXN] , dep[MAXN];
ll dp[MAXN];
struct edge{
    int to , nxt;
}a[MAXN];
void add(int u , int v) {
    a[++ cnt].to = v;
    a[cnt].nxt = head[u];
    head[u] = cnt;
}
void Dp(int u , int fa) { // 对于已知根的情况进行求解 
    siz[u] = 1;
    dep[u] = dep[fa] + 1;
    for (int i = head[u] ; i ; i = a[i].nxt) {
        int v = a[i].to;
        if (v == fa) continue;
        Dp(v , u);
        siz[u] += siz[v];
    }
}
void Dp2(int u , int fa) {// 换根 
    for (int i = head[u] ; i ; i = a[i].nxt) {
        int v = a[i].to;
        if (v == fa) continue;
        dp[v] = dp[u] + n - 2 * siz[v];
        Dp2(v , u);
    }
}
int main() {
    scanf("%d", &n);
    for (int i = 1 , u , v ; i < n ; i ++) {
        scanf("%d %d", &u , &v);
        add(u , v);
        add(v , u);
    }
    Dp(1 , 0);
    for (int i = 1 ; i <= n ; i ++) dp[1] += siz[i];
    Dp2(1 , 0);
    int ans = 1;
    for (int i = 2 ; i <= n ; i ++) {
        if (dp[i] > dp[ans]) ans = i;
    }
    printf("%d", ans);
    return 0;
}

例题

最大疯子树

题意简述:求出一个最大联通子图,令其节点按权值从小到大排序后的点序列为 b,并且 b i → b i + 1 b_i \rightarrow b_{i+1} bibi+1唯一路径上节点的权值都小于 w b i w_{b_i} wbi

不妨简化问题,将树降维成链,较易发现, b 1 b_1 b1 b 2 b_2 b2 是相邻的,推广一下,若该链的根节点的权值最小,那么从根到叶节点的权值是不严格递增的。

处理完链的情况,不妨考虑树结构。由于树由多条从根节点到叶节点的简单路径(链)组成,所以推广一下,若该树的根节点权值最小,那么每条根节点到叶节点的简单路径都满足权值不严格递增。

有了如上结论,不妨令 d p u dp_u dpu 表示以 u 为根时最大疯子树的节点个数,若满足 v ∈ s o n u v \in son_u vsonu w v ≤ w u w_v \leq w_u wvwu,则 d p u dp_u dpu 可以累加 d p v dp_v dpv

考虑换根的方式,如上图所示,对 w v w_v wv w 1 w_1 w1 的大小关系进行分类讨论:

  1. w v ≥ w 1 w_v \geq w_1 wvw1,则以 v 为根的最大疯子树不能包含 1 节点及其子树,否则不满足节点权值非严格递增
  2. w v ≤ w 1 w_v \leq w_1 wvw1,则 d p u = d p u + d p v dp_u = dp_u + dp_v dpu=dpu+dpv,即令黄色部分与红色部分构成新的疯子树,此时依旧满足 w u w_u wu 最小且节点权值非严格递增。

有了扫描方法,即可进行两次 dfs 进行求解。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 5;
int cnt , head[MAXN] , T , n ,  dp[MAXN] , w[MAXN];
struct edge{
    int to , nxt;
}a[MAXN];
void add(int u , int v) {
    a[++ cnt].to = v;
    a[cnt].nxt = head[u];
    head[u] = cnt;
}
void dfs1(int u , int fa) {
    dp[u] = 1;
    for (int i = head[u] ; i ; i = a[i].nxt) {
        int v = a[i].to;
        if (v != fa) {
            dfs1(v , u);
            if (w[v] >= w[u]) dp[u] += dp[v]; // 节点权值单调递增 
        }
    }
}
void dfs2(int u , int fa) { // 换根 
    if (w[fa] >= w[u]) dp[u] += dp[fa]; // 满足合并的情况 
    for (int i = head[u] ; i ; i = a[i].nxt) {
        int v = a[i].to;
        if (v != fa) {
            dfs2(v , u);
        } 
    } 
}
int main() {
    while(~scanf("%d", &n)) {    
        memset(dp , 0 , sizeof(dp)); // 多测清空 
        memset(head, 0 , sizeof(head));
        cnt = 0;
        for (int i = 1 ; i <= n ; i ++) scanf("%d", &w[i]);
        for (int i = 1 , u , v ; i < n ; i ++) {
            scanf("%d %d", &u , &v);
            add(u , v);
            add(v , u);
        }
        dfs1(1 , 0);
        dfs2(1 , 0);
        int ans = 0;
        for (int i = 1 ; i <= n ; i ++) ans = max(ans , dp[i]); // 求最大疯子树 
        printf("%d\n", ans);
    }
    return 0;
}
posted @   Fracture_Dream  阅读(6)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示