定义
树的直径:树中最远的两个节点之间的距离被称为树的直径,连接这两个点的路径被称为树的最长链。
性质
1. 直 径 两 端 点 一 定 是 叶 子 节 点 。 2. 距 任 意 点 最 远 点 一 定 是 直 径 的 端 点 , 据 所 有 点 最 大 值 最 小 的 点 一 定 是 直 径 的 中 点 。 3. 两 棵 树 相 连 , 新 直 径 的 两 端 点 一 定 是 原 四 个 端 点 中 的 两 个 4. 两 棵 树 相 连 , 新 直 径 长 度 最 小 为 m a x ( m a x ( 直 径 1 , 直 径 2 ) , 半 径 1 + 半 径 2 + 新 边 长 度 ) ( 设 k 为 直 径 中 最 接 近 中 点 的 节 点 , 半 径 = m a x ( t o t − d [ k ] , d [ k ] ) ) 5. 一 棵 树 上 接 一 个 叶 子 结 点 , 直 径 最 多 改 变 一 个 端 点 6. 若 一 棵 树 存 在 多 条 直 径 , 多 条 直 径 交 于 一 点 , 且 交 点 是 直 径 的 严 格 中 点 ( 中 点 可 能 在 某 条 边 内 ) 1.直径两端点一定是叶子节点。\\2.距任意点最远点一定是直径的端点,据所有点最大值最小的点一定是直径\\的中点。\\3.两棵树相连,新直径的两端点一定是原四个端点中的两个\\4.两棵树相连,新直径长度最小为\\max(max(直径1,直径2),半径1+半径2+新边长度 ) \\(设k为直径中最接近中点的节点,半径=max(tot-d[k],d[k]))\\5.一棵树上接一个叶子结点,直径最多改变一个端点\\6.若一棵树存在多条直径,多条直径交于一点,且交点是直径的严格中点\\(中点可能在某条边内) 1.直径两端点一定是叶子节点。2.距任意点最远点一定是直径的端点,据所有点最大值最小的点一定是直径的中点。3.两棵树相连,新直径的两端点一定是原四个端点中的两个4.两棵树相连,新直径长度最小为max(max(直径1,直径2),半径1+半径2+新边长度)(设k为直径中最接近中点的节点,半径=max(tot−d[k],d[k]))5.一棵树上接一个叶子结点,直径最多改变一个端点6.若一棵树存在多条直径,多条直径交于一点,且交点是直径的严格中点(中点可能在某条边内)
求法
我们假设树以n个点n-1条边的无向图给出,存储于邻接表中。
树形dp求树的直径:
该 无 向 图 可 以 看 作 以 某 一 节 点 为 根 的 有 根 树 , 设 以 1 号 节 点 为 根 , 设 D [ x ] 为 从 x 出 发 , 到 其 子 树 最 远 节 点 的 距 离 , 设 x 的 子 节 点 为 y 1 , y 2 , . . . , y t , 则 有 D [ x ] = max 1 < = i < = t { D [ y i ] + e d g e [ x ] [ y i ] } ‾ 接 下 来 考 虑 经 过 点 x 的 最 长 链 长 度 F [ x ] , 假 设 在 求 D [ x ] 的 过 程 中 我 们 将 要 枚 举 到 y i 节 点 , 则 此 时 D [ x ] 存 储 则 x 到 前 i − 1 颗 子 树 的 最 长 路 , 由 于 F [ x ] 必 然 经 过 D [ x ] 的 路 径 , 则 我 们 可 以 在 求 D [ x ] 的 过 程 中 不 断 用 D [ x ] + D [ y i ] + e d g e [ x ] [ y i ] 更 新 F [ x ] , 通 过 枚 举 经 过 所 有 点 的 链 的 最 长 链 即 可 求 出 树 的 直 径 . 算 法 复 杂 度 : O ( n ) ; 该无向图可以看作以某一节点为根的有根树,设以1号节点为根,设D[x]为从\\x出发,到其子树最远节点的距离,设x的子节点为y_1,y_2,... \ ,y_t,则有\\\underline{\mathbf{D[x] = \max_{1 <= i <= t}\left \{ D[y_i]+edge[x][y_i] \right \} }} \\接下来考虑经过点x的最长链长度F[x],假设在求D[x]的过程中我们将要枚举\\到y_i节点,则此时D[x]存储则x到前i-1颗子树的最长路,由于F[x]必然经过\\D[x]的路径,则我们可以在求D[x]的过程中不断用D[x]+D[y_i]+edge[x][y_i]\\更新F[x],通过枚举经过所有点的链的最长链即可求出树的直径.\\算法复杂度:O(n); 该无向图可以看作以某一节点为根的有根树,设以1号节点为根,设D[x]为从x出发,到其子树最远节点的距离,设x的子节点为y1,y2,... ,yt,则有D[x]=1<=i<=tmax{D[yi]+edge[x][yi]}接下来考虑经过点x的最长链长度F[x],假设在求D[x]的过程中我们将要枚举到yi节点,则此时D[x]存储则x到前i−1颗子树的最长路,由于F[x]必然经过D[x]的路径,则我们可以在求D[x]的过程中不断用D[x]+D[yi]+edge[x][yi]更新F[x],通过枚举经过所有点的链的最长链即可求出树的直径.算法复杂度:O(n);
void dp(int x)
{
v[x] = 1;
for(int i = head[x];i;i = next[i])
{
int y = ver[i],z = edge[i];
if(v[y]) continue;
dp(y);
ans = max(ans,D[x]+D[y]+z);
D[x] = max(D[x],D[y]+z);
}
}
两次BFS/DFS求树的直径:
1. 从 任 意 节 点 出 发 , 通 过 B F S 或 D F S 找 出 距 离 该 点 最 远 的 点 p ; 2. 从 p 点 出 发 , 再 次 通 过 B F S 或 D F S 找 出 距 离 p 点 最 远 的 点 q ; 3. p 到 q 的 路 径 即 为 树 的 一 条 直 径 . 因 为 p 必 定 是 直 径 的 一 个 端 点 , 则 据 端 点 最 远 的 点 必 为 直 径 的 令 一 端 ; 算 法 限 制 : 仅 适 用 于 边 权 值 均 非 负 的 无 向 图 . 优 点 : 方 便 记 录 途 径 节 点 . 算 法 复 杂 度 : O ( n ) . 1.从任意节点出发,通过BFS或DFS找出距离该点最远的点p;\\2.从p点出发,再次通过BFS或DFS找出距离p点最远的点q;\\3.p到q的路径即为树的一条直径.因为p必定是直径的一个端点,\\则据端点最远的点必为直径的令一端;\\{\color{Blue} 算法限制:仅适用于边权值均非负的无向图.} \\{\color{Blue}优点:方便记录途径节点.}\\算法复杂度:O(n). 1.从任意节点出发,通过BFS或DFS找出距离该点最远的点p;2.从p点出发,再次通过BFS或DFS找出距离p点最远的点q;3.p到q的路径即为树的一条直径.因为p必定是直径的一个端点,则据端点最远的点必为直径的令一端;算法限制:仅适用于边权值均非负的无向图.优点:方便记录途径节点.算法复杂度:O(n).
例题:
题目大意:
在一个地区有 n 个村庄,编号为 1,2,…,n。
有 n−1 条道路连接着这些村庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其他任一个村庄。
每条道路的长度均为 1 个单位。
为保证该地区的安全,巡警车每天都要到所有的道路上巡逻。
警察局设在编号为 1 的村庄里,每天巡警车总是从警局出发,最终又回到警局。
为了减少总的巡逻距离,该地区准备在这些村庄之间建立 K 条新的道路,每条新道路可以连接任意两个村庄。
两条新道路可以在同一个村庄会合或结束,甚至新道路可以是一个环。
因为资金有限,所以 K 只能为 1 或 2。
同时,为了不浪费资金,每天巡警车必须经过新建的道路正好一次。
编写一个程序,在给定村庄间道路信息和需要新建的道路数的情况下,计算出最佳的新建道路的方案,使得总的巡逻距离最小。
由 于 每 条 道 路 都 至 少 经 过 一 次 , 且 需 要 返 回 原 点 ; 1. K = 0 时 , 需 要 经 过 的 距 离 a n s = 2 ∗ ( n − 1 ) , 即 每 条 道 路 都 经 过 了 两 次 ; 2. k = 1 时 , 只 需 要 将 图 的 直 径 两 端 连 接 起 来 , 直 径 之 间 的 路 即 只 需 要 走 一 次 , 设 直 径 两 端 的 距 离 为 L 1 , 则 此 时 a n s = 2 ∗ ( n − 1 ) − L 1 + 1 ; 3. k = 2 时 , 求 情 况 2 下 的 另 一 个 环 , 两 个 环 不 重 叠 部 分 只 需 要 走 一 次 , 而 简 单 推 导 可 知 两 个 环 重 叠 部 分 的 路 需 要 走 两 次 , 此 时 我 们 可 以 在 计 算 了 树 的 直 径 L 1 的 后 , 将 其 路 径 上 的 边 权 改 为 − 1 , 再 次 求 树 的 直 径 L 2 , 则 a n s = 2 ∗ ( n − 1 ) − ( L 1 − 1 ) − ( L 2 − 1 ) = 2 ∗ ( n − 1 ) − L 1 − L 2 ; 总 结 : 第 一 次 求 直 径 我 们 可 以 用 两 次 B F S 记 录 路 径 , 方 便 后 续 修 改 边 权 , 第 二 次 求 直 径 利 用 树 形 d p 求 有 负 权 边 情 况 下 树 的 直 径 , 代 码 如 下 : 由于每条道路都至少经过一次,且需要返回原点;\\1.K = 0时,需要经过的距离ans = 2*(n-1),即每条道路都经过了两次;\\2.k = 1时,只需要将图的直径两端连接起来,直径之间的路即只需要走一\\次,设直径两端的距离为L_1,则此时ans = 2*(n-1)-L_1+1;\\3.k = 2时,求情况2下的另一个环,两个环不重叠部分只需要走一次,而简\\单推导可知两个环重叠部分的路需要走两次,此时我们可以在计算了\\树的直径L_1的后,将其路径上的边权改为-1,再次求树的直径L_2,则\\ans = 2*(n-1)-(L_1-1)-(L_2-1) = 2*(n-1)-L_1-L_2;\\总结:第一次求直径我们可以用两次BFS记录路径,方便后续修改边\\权,第二次求直径利用树形dp求有负权边情况下树的直径,代码如下: 由于每条道路都至少经过一次,且需要返回原点;1.K=0时,需要经过的距离ans=2∗(n−1),即每条道路都经过了两次;2.k=1时,只需要将图的直径两端连接起来,直径之间的路即只需要走一次,设直径两端的距离为L1,则此时ans=2∗(n−1)−L1+1;3.k=2时,求情况2下的另一个环,两个环不重叠部分只需要走一次,而简单推导可知两个环重叠部分的路需要走两次,此时我们可以在计算了树的直径L1的后,将其路径上的边权改为−1,再次求树的直径L2,则ans=2∗(n−1)−(L1−1)−(L2−1)=2∗(n−1)−L1−L2;总结:第一次求直径我们可以用两次BFS记录路径,方便后续修改边权,第二次求直径利用树形dp求有负权边情况下树的直径,代码如下:
#include<bits/stdc++.h>
#define next ne
using namespace std;
typedef pair<int,int> P;
int head[100005],next[200005],ver[200005],L,id[100005],edge[200005],d[200005];
bool v[100005];
void init()
{
//memset(head,0,sizeof(head));
memset(id,0,sizeof(id));
memset(v,0,sizeof(v));
L = 0;
}
void dp(int x)
{
v[x] = 1;
int ans;
for(int i = head[x];i;i = next[i])
{
int y = ver[i];
if(v[y]) continue;//不能从下往上算;
dp(y);
L = max(L,d[x]+d[y]+edge[i]);
d[x] = max(d[x],d[y]+edge[i]);
}
}
int bfs(int x)
{
int ans;
queue<P>q;
q.push(P(x,0));
while(!q.empty())
{
int now = q.front().first,l = q.front().second;
q.pop();
if(v[now]) continue;
v[now] = 1;
if(L < l)
{
ans = now;
L = l;
}
int pos = head[now];
while(pos)
{
if(v[ver[pos]])
{
pos = next[pos];
continue;
}
id[ver[pos]] = now;
q.push(P(ver[pos],edge[pos]+l));
pos = next[pos];
}
}
return ans;
}
int main()
{
int n,k;cin >> n >> k;
for(int i = 1;i < n;++i)
{
int x,y;scanf("%d%d",&x,&y);
next[2*i-1] = head[x];
head[x] = 2*i-1;
ver[2*i-1] = y;
edge[2*i-1] = 1;
next[2*i] = head[y];
head[y] = 2*i;
ver[2*i] = x;
edge[2*i] = 1;
}
int ans = 2*(n-1);
int p = bfs(1);
init();
int q = bfs(p);
ans -= L-1;
//cout << L << ' ';
if(k^2)
{
printf("%d\n",ans);
return 0;
}
for(int i = q;id[i];i = id[i])
{
int now = head[i];
while(now)
{
if(ver[now] == id[i])
{
edge[now] = -1;
break;
}
now = next[now];
}
now = head[id[i]];
while(now)
{
if(ver[now] == i)
{
edge[now] = -1;
break;
}
now = next[now];
}
}
init();
dp(1);
ans -= L-1;
cout << ans << endl;
return 0;
}