树的直径与树的重心

参考:oi-wiki

树的直径

图中所有最短路径的最大值即为「直径」,可以用两次 DFS 或者树形 DP 的方法在 O(n) 时间求出树的直径

做法1:两次 DFS

首先对任意一个结点做 DFS 求出最远的结点,然后以这个结点为根结点再做 DFS 到达另一个最远结点。第一次 DFS 到达的结点可以证明一定是这个图的直径的一端,第二次 DFS 就会达到另一端。这两端间的距离就是树的直径。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e7 + 10;

inline int read() {
  int x = 0, k = 1; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') k = -1;
  for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
  return x * k;
}

int head[maxn], cnt, n;
int maxdis, maxpos;
struct node{
  int to, nxt, val;
}e[maxn];

inline void add(int u, int v, int w) {
  e[++cnt].to = v;
  e[cnt].val = w;
  e[cnt].nxt = head[u];
  head[u] = cnt;
}

//x:dfs的源点,fa:x点的父亲节点,dis:x到源点的距离
inline void dfs(int x, int fa, int dis) {
  if (maxdis < dis) {
    maxdis = dis;
    maxpos = x;
  }
  for (int i = head[x]; i; i = e[i].nxt) {
    if (e[i].to == fa) continue; //父亲节点已经访问过,防止重复遍历
    dfs(e[i].to, x, dis + e[i].val);
  }
}

int main() {
  n = read();
  for (int i = 1; i <= n; i++) {
    register int u, v, w;
    u = read(); v = read(); w = read();
    add(u, v, w); add(v, u, w);
  }
  dfs(1, -1, 0); //从结点1开始遍历,找到最远点maxpos及对应的最远距离maxdis
  maxdis = 0;
  dfs(maxpos, -1, 0); //从结点maxpos开始遍历,找到最远点及其对应的maxdis
  printf("%d\n", maxdis);
  return 0;
}

做法2:树形DP

我们记录每个节点向下,所能延伸的最远距离 dis1,和次远距离 dis2,那么直径就是所有 dis1 + dis2 的最大值。

例题:HDU2196 Computer

注意:以下代码没有考虑多重输入,如要在 HDU 提交,记得修改输入部分并且注意初始化

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e7 + 10;

inline int read() {
  int x = 0, k = 1; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') k = -1;
  for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
  return x * k;
}

struct node{ 
  int to, nxt, val; 
}e[maxn];
int head[maxn], dis[maxn][3], maxson[maxn], vis[maxn], n, cnt;

inline void add(int u, int v, int w) {
  e[++cnt].to = v;
  e[cnt].nxt = head[u];
  e[cnt].val = w;
  head[u] = cnt;
}

inline void dfs1(int u) {
  vis[u] = 1; 
  dis[u][0] = dis[u][1] = 0;
  for (int i = head[u]; i; i = e[i].nxt) {
    int v = e[i].to, w = e[i].val;
    if (vis[v]) continue;
    dfs1(v);
    int dist = dis[v][0] + w;
    if (dist >= dis[u][0]) {
      dis[u][1] = dis[u][0]; dis[u][0] = dist;
      maxson[u] = v;
    }
    else if (dist > dis[u][1]) 
      dis[u][1] = dist;
  }
}

inline void dfs2(int u) {
  vis[u] = 1;
  for (int i = head[u]; i; i = e[i].nxt) {
    int v = e[i].to, w = e[i].val;
    if (vis[v]) continue;
    if (v != maxson[u]) dis[v][2] = max(dis[u][0], dis[u][2]) + w;
    else  dis[v][2] = max(dis[u][1], dis[u][2]) + w;
    dfs2(v);
  }
}

int main() {
  int u, v, w;
  n = read();
  for (int i = 2; i <= n; i++) {
    v = read(); w = read();
    add(i, v, w); add(v, i, w);
  }
  dfs1(1);
  memset(vis, 0, sizeof(vis));
  dfs2(1);
  for (int i = 1; i <= n; i++)
    printf("%d\n", max(dis[i][0], dis[i][2]));
  return 0;
}

树的重心

定义

  • 对于树上的每一个点,计算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心(这里以及下文中的“子树”都是指无根树的子树,即包括“向上”的那棵子树,并且不包括整棵树自身)

性质

  • 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。

  • 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。

  • 把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。

  • 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。

求法

  • 在 DFS 中计算每个子树的大小,记录“向下”的子树的最大大小,利用总点数 - 当前子树(这里的子树指有根树的子树)的大小得到“向上”的子树的大小,然后就可以依据定义找到重心了。

参考代码

// 这份代码默认节点编号从 1 开始,即 i ∈ [1,n]
int size[MAXN],  // 这个节点的“大小”(所有子树上节点数 + 该节点)
    weight[MAXN],  // 这个节点的“重量”
    centroid[2];   // 用于记录树的重心(存的是节点编号)
void GetCentroid(int cur, int fa) {  // cur 表示当前节点 (current)
  size[cur] = 1;
  weight[cur] = 0;
  for (int i = head[cur]; i != -1; i = e[i].nxt) {
    if (e[i].to != fa) {  // e[i].to 表示这条有向边所通向的节点。
      GetCentroid(e[i].to, cur);
      size[cur] += size[e[i].to];
      weight[cur] = max(weight[cur], size[e[i].to]);
    }
  }
  weight[cur] = max(weight[cur], n - size[cur]);
  if (weight[cur] <= n / 2) {  // 依照树的重心的定义统计
    centroid[centroid[0] != 0] = cur;
  }
}
posted @ 2021-03-13 16:30  Moominn  阅读(128)  评论(0编辑  收藏  举报