【知识点】树的直径、中心、重心

前置知识

树的初步学习:树初步

直径

定义

树上任意两节点之间最长的简单路径即为树的直径。
若直径上有偶数个点,则这条直径有两个中点。否则只有一个中点。

性质

  1. 一棵树可以有多条直径,它们的长度相等。
  2. 若树上所有边边权均为正,则树的所有直径中点重合。

求法

两次DFS法

从任意节点(一般选1号节点)开始DFS,找到离该节点最远的节点v,则v一定是直径的一个端点。从v开始DFS,找到离v最远的节点u,则u和v之间的路径为直径。
若需要记录直径上的所有节点,可以在第二次DFS时,记录每个节点的前序节点。

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;

i64 n, u, v, c = 1;	//c为DFS找到的端点
vector<i64>edge[100100];
i64 d[100100];

void dfs(i64 now, i64 fa) {
	for (i64 nex : edge[now]) {
		if (nex == fa)
			continue;
		d[nex] = d[now] + 1;
		if (d[nex] > d[c])
			c = nex;
		dfs(nex, now);
	}
	return;
}

int main() {
	cin >> n;
	for (i64 i = 0; i < n - 1; i++) {
		cin >> u >> v;
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	dfs(1, 0);	//执行完后c为第一次DFS找到的端点
	d[c] = 0;
	dfs(c, 0);	//执行完后c为第二次DFS找到的端点
	cout << d[c];
	return 0;
}

树形DP法(双数组)

相较于两次DFS,树形DP的优势是在有负权边的情况下,仍然能找到直径。(此时直径不一定是以叶子节点为端点)

先假设1为根。记录每个节点为根的子树中,离根最远的最长路径长度\(d_1\)和次长路径(与最长路径无公共边)长度\(d_2\),那么直径就是对于每个点,\(d_1+d_2\)的最大值。

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;

i64 n, u, v, d = 0;
vector<i64>edge[100100];
i64 d1[100100], d2[100100];

void dfs(i64 now, i64 fa) {
	d1[now] = d2[now] = 0;
	for (i64 nex : edge[now]) {
		if (nex == fa)
			continue;
		dfs(nex, now);
		i64 t = d1[nex] + 1;
		if (t > d1[now])
			d2[now] = d1[now], d1[now] = t;
		else if (t > d2[now])
			d2[now] = t;
	}
	d = max(d, d1[now] + d2[now]);
	return;
}

int main() {
	cin >> n;
	for (i64 i = 0; i < n - 1; i++) {
		cin >> u >> v;
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	dfs(1, 0);
	cout << d;
	return 0;
}

树形DP法(单数组)

在双数组的情况下,可以删除\(d_2\)数组,只利用\(d_1\)数组,在更新\(d_1\)之前就更新\(d\),防止最长路径和次长路径有公共边。

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;

i64 n, u, v, d = 0;
vector<i64>edge[100100];
i64 d1[100100];

void dfs(i64 now, i64 fa) {
	d1[now] = 0;
	for (i64 nex : edge[now]) {
		if (nex == fa)
			continue;
		dfs(nex, now);
		d = max(d, d1[now] + d1[nex] + 1);
		d1[now] = max(d1[now], d1[nex] + 1);
	}
	return;
}

int main() {
	cin >> n;
	for (i64 i = 0; i < n - 1; i++) {
		cin >> u >> v;
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	dfs(1, 0);
	cout << d;
	return 0;
}

中心

定义

在树中,如果节点\(x\)作为根节点时,从\(x\)出发的最长链最短,那么称\(x\)为这棵树的中心。

性质

  1. 树的中心不一定唯一,但最多有2个,且这两个中心是相邻的。
  2. 树的中心一定位于树的直径上。
  3. 树上所有点到其最远点的路径一定交会于树的中心。
  4. 当树的中心为根节点时,其到达直径端点的两条链分别为最长链和次长链。
  5. 当通过在两棵树间连一条边以合并为一棵树时,连接两棵树的中心可以使新树的直径最小。
  6. 树的中心到其他任意节点的距离不超过树直径的一半。

求法

树形DP法,步骤如下:

  1. 维护\(d_1[x]\),表示以\(x\)为根的子树中,以\(x\)出发的最长链长度。
  2. 维护\(d_2[x]\),表示以\(x\)为根的子树中,以\(x\)出发的次长链(与最长链无公共边)长度。
  3. 维护\(up[x]\),表示除去\(x\)子树,剩余图中,经过\(x\)父亲的最长链长度。
  4. 找到点使得\(max(d_1[x], up[x])\)最小,即\(x\)为中心。
int d1[N], d2[N], up[N], x, y, mini = 1e9;
//x, y为两个中心

struct node {
  int to, val;  // to为边指向的节点,val为边权
};

vector<node> nbr[N];

void dfsd(int cur, int fa) {  // 求取d1和d2
  for (node nxtn : nbr[cur]) {
    int nxt = nxtn.to, w = nxtn.val;
    if (nxt == fa) {
      continue;
    }
    dfsd(nxt, cur);
    if (d1[nxt] + w > d1[cur]) {
      d2[cur] = d1[cur];
      d1[cur] = d1[nxt] + w;
    } else if (d1[nxt] + w > d2[cur]) {
      d2[cur] = d1[nxt] + w;
    }
  }
}

void dfsu(int cur, int fa) {
  for (node nxtn : nbr[cur]) {
    int nxt = nxtn.to, w = nxtn.val;
    if (nxt == fa) {
      continue;
    }
    up[nxt] = up[cur] + w;	//默认取值,逐步更新
    if (d1[nxt] + w != d1[cur]) {  // 如果自己子树里的最长链不在nxt子树里
      up[nxt] = max(up[nxt], d1[cur] + w);	//则使用最长链
    } else {  // 自己子树里的最长链在nxt子树里
      up[nxt] = max(up[nxt], d2[cur] + w);	//只能使用次长链
    }
    dfsu(nxt, cur);
  }
}

void GetTreeCenter() {  // 统计树的中心,记为x和y(若存在)
  dfsd(1, 0);
  dfsu(1, 0);
  for (int i = 1; i <= n; i++) {
    if (max(d1[i], up[i]) < mini) {  // 找到了当前max(len1[x],up[x])最小点
      mini = max(d1[i], up[i]);
      x = i;
      y = 0;
    } else if (max(d1[i], up[i]) == mini) {  // 另一个中心
      y = i;
    }
  }
}

重心

定义

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

性质

  1. 树的重心如果不唯一,则至多有两个,且这两个重心相邻。
  2. 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
  3. 树中所有点到某个点的距离和中,到重心的距离和是最小的如果有两个重心,那么到它们的距离和一样。
  4. 把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
  5. 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。

求法

DFS过程中统计,下方所有子树的最大大小,下方所有子树的总大小,再用总点数减去下方总大小得到上方子树大小,根据定义找重心。

int size[MAXN], weight[MAXN], centroid[2];  // centroid用于记录树的重心

void GetCentroid(int cur, int fa) {
  size[cur] = 1;
  weight[cur] = 0;
  for (int i = head[cur]; i != -1; i = e[i].nxt) {
    if (e[i].to != fa) {
      GetCentroid(e[i].to, cur);	//DFS递归
      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 @ 2025-08-08 14:43  Alkaid16  阅读(122)  评论(0)    收藏  举报