算法:树的重心、树的直径

【树的重心】

如果在树中选择某个节点并删除,这棵树将分为若干棵子树 ,**统计子树节点数并记录最大值。**取遍树上所有节点,使此最大值取到最小的节点被称为整个树的重心。

这里以及下文中的子树若无特殊说明都是指无根树的子树,即包括向上的那棵子树,并且不包括整棵树自身。如果题目明确说明存在点权,那么统计子树的节点数变成统计子树的权值之和。

【解法 - 树形 dp】

  • 根据定义,利用树形 dp 求解:对于每一个结点,求出所有子树大小的最大值,找出最大值最小的结点即可。

以 x 为根的子树为例,除了所有的孩子结点 y 为根的子树以外,还有一棵向上的子树,这棵子树的大小可以用总的结点个数 n 减去以 x 为根的子树大小求得。因此:

  1. 状态表示:fx 表示以 x 为根的子树的大小;
  2. 状态转移方程:fx = 1 + sum (fy);
  3. 可以用全局变量来记录树的重心。

【性质】

  • 树的重心如果不唯一,则至多有两个,且这两个重心相邻。
  • 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
  • 树中所有点到某个点的距离和 中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
    • 推论一:即使边权不为 1,只要没有负边权,不影响各点到重心的距离和最小;
    • 推论二:即使边权和点权都不为 1,只要没有负边权,不影响各点到重心的距离和最小。

【模板题】

U104609 【模板】树的重心 - 洛谷https://www.luogu.com.cn/problem/U104609代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 5e4 + 10;

int n;
vector<int> edges[N];
int r1, r2;  // 最多有两个重心
int ret = N; // 记录所有最大子树的最小值
int f[N];    // 以x为根的子树的大小

void dfs(int x, int fa)
{
    f[x] = 1;
    int m = 0;
    for(auto y : edges[x])
    {
        if(y == fa) continue;
        dfs(y, x);
        m = max(m, f[y]);
        f[x] += f[y];
    }
    m = max(m, n - f[x]);
    if(m < ret)
    {
        r1 = x;
        r2 = 0;
        ret = m;
    }
    else if(m == ret)
    {
        r2 = x;
    }
}
signed main()
{
    cin >> n;
    for(int i = 1; i < n; i++)
    {
        int u, v; cin >> u >> v;
        edges[u].push_back(v);
        edges[v].push_back(u);
    }
    dfs(1, 0);
    if(r1 && r2)
    {
        if(r1 > r2) swap(r1, r2);
        cout << r1 << " " << r2;
    }
    else 
    {
        cout << r1 << endl;
    }
}

【树的直径】

树的直径 - 两次 dfs

【算法流程】

  1. 从任意结点 x 开始进行第一次 dfs,找到距离 x 最远的结点 y;
  2. 然后再从 y 结点开始再来一次 dfs,找到距离 y 最远的结点 z。

那么,y 到 z 就是直径的两端。

【性质】

  • 在一棵树上,从任意节点 x 开始进行一次 dfs,找到距离 x 最远的结点必定为直径的一端。 如果存在负边权,上述性质将不再成立。因此,两次 dfs 只适用于所有边权全部非负的情况。

从任意一点开始找到最远的点,一定是直径的一端。

【设计 dfs】

  • 我们发现,两次 dfs 执行的逻辑是一样的,因此只用设计一个 dfs 函数即可。

我们想要的是根节点与其余任意点的距离 d。从根节点递归的过程中,从 x 进入 y 结点之前,就可以更新出距离:dy = dx + w(x,y),然后再递归到 y 结点中即可。 用一些全局变量维护最大距离,以及对应的结点即可。

【顺便还可以维护出直径所在的路线】

  • 每次递归传入父结点,额外用一个数组维护父结点,还能还原出直径所在的路线。

【模板题】

B4016 树的直径 - 洛谷https://www.luogu.com.cn/problem/B4016

【代码实现(带边权,可还原路径)】

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 5e4 + 10;

int n;
vector<int> edges[N];
int r1, r2;  // 最多有两个重心
int ret = N; // 记录所有最大子树的最小值
int f[N];    // 以x为根的子树的大小

void dfs(int x, int fa)
{
    f[x] = 1;
    int m = 0;
    for(auto y : edges[x])
    {
        if(y == fa) continue;
        dfs(y, x);
        m = max(m, f[y]);
        f[x] += f[y];
    }
    m = max(m, n - f[x]);
    if(m < ret)
    {
        r1 = x;
        r2 = 0;
        ret = m;
    }
    else if(m == ret)
    {
        r2 = x;
    }
}
signed main()
{
    cin >> n;
    for(int i = 1; i < n; i++)
    {
        int u, v; cin >> u >> v;
        edges[u].push_back(v);
        edges[v].push_back(u);
    }
    dfs(1, 0);
    if(r1 && r2)
    {
        if(r1 > r2) swap(r1, r2);
        cout << r1 << " " << r2;
    }
    else 
    {
        cout << r1 << endl;
    }
}

树的直径 - 树形 dp

【算法原理】

对于一棵无根树,把它看作是以 1 为根节点的有根树,求出经过 x 的最长路径:

  • 如果这条路径既向上,又向下,是很难求的;
  • 可以固定这条路径只在以 x 为根的子树中考虑。因为向上的路线,会在回溯到父节点的时候考虑进去;
  • 那么,对于 x 结点的孩子结点 y,需要提供从 y 开始向下走的,能走到的最长路径;

因此,树形 dp 的逻辑为:

  1. 状态表示:fx 表示从 x 出发,只往下走的最长路径;

  2. 状态转移方程:fx = max(fx, fy+w(x,y))。
    如何找出经过 x 的最长路径呢?

  3. 遍历所有孩子,找出 fy1 + w(x,y1) + fy2 +w(x,y2)) 的最大值;

  4. 其实不用那么麻烦。在树形 dp 的过程中,当我们从 yi 回溯时,fx 尚未更新,里面存储着前面所有路径的最大值。那么,在用 yi 更新 fx 之前,就能求出两条路径之和的最大值。

【模板题】

U81904 【模板】树的直径 - 洛谷https://www.luogu.com.cn/problem/U81904

【代码实现(树形 DP,带边权)】

cpp 复制代码
int n;
vector<PII> edges[N];
int f[N], ret;

void dfs(int x, int fa)
{
    for(auto& t : edges[x])
    {
        int y = t.first, z = t.second;
        if(y == fa) continue;
        dfs(y, x);
        ret = max(ret, f[x] + f[y] + z);
        f[x] = max(f[x], f[y] + z);
    }
}

void fun()
{
    dfs(1, 0);
    cout << ret << endl;
}
相关推荐
专注仿真1 小时前
高代价计算后置:探测、交战、毁伤三条链路的统一设计哲学
算法·探测·毁伤
xie0510_1 小时前
Any类简要实现
开发语言·c++·算法
在所不辞兄1 小时前
为什么PINN非常适合求解有限元模型
人工智能·深度学习·神经网络·算法·机器学习
一棵树73512 小时前
数据结构闲聊
数据结构·算法
bro_Java6662 小时前
《二叉树的基础知识、操作方法与代码示例》
java·数据结构·算法·广度优先
PascalXie2 小时前
地面机器人VSLAM技术调研与实现方案参考:算法、硬件平台与工程落地
算法·机器人
aqiu1111114 小时前
【C++算法打怪专栏】8. 字符串转换整数 (atoi)
开发语言·c++·算法
信奥卷王12 小时前
[GESP202609 六级] 分树规划
算法
夏幻灵12 小时前
LeetCode 21:合并两个有序链表——双指针与哑节点详解
算法·leetcode·链表