携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情
一. 定义
树上任意两点间最大距离(最长路)
二. 性质(重要)
-
设树T存在直径a1-b1,a2-b2......an-bn,从树上任意点c搜索最远点,可能有多个最远点,它们与c的距离一定相同,且任取一最远点,一定是某条直径的端点,因此最长距离d = max(dis(c, a1), dis(c, b1)) = max(dis(c, a2), dis(c, b2)) = ... = max(dis(c, an), dis(c, bn))。
推论:以某直径端点a搜索最远点,可能有多个最远点,且a与这些最远点距离都为树上最大距离,即a与它们都能组成直径。
-
一棵树的所有直径必定以同一点为中点。
-
只有a1~b1直径上的点i才满足这个条件:d1[i]+d2[i] == d1[b1],其中d1[i]表示i点距a1的距离,d2[i]表示i点距b1的距离。
证明:当i点在直径a1~b1上时,显然成立。当i点不在直径上时,反证法,假设满足d1[i]+d2[i] == d1[b1],首先i一定通过某条路径与直径上某点j连通,因为在树上,所以d1[i]与d2[i]路径唯一,有d1[i] = dis(a1, j)+dis(j, i),d2[i] = dis(a2, j)+dis(j, i),二者相加显然不等于dis(a1, j)+dis(j, a2),因此得证。
三. 两种方法求直径长度
-
根据性质及推论,可以通过两次dfs/bfs找到一条直径的两个端点,并得到直径长度。
具体代码如下:
int _max = 0, id;//id记录最远点标号 void dfs(int now, int fa, int dis)//dis记录起点距离now的距离 { if(dis > _max) { _max = dis; id = now; } for(int i = head[now]; i; i = edges[i].next)//链式前向星遍历子节点 { if(edges[i].v == fa)//如果下一步要原路返回就跳过,防止无限递归 continue; dfs(edges[i].v, now, dis+edges[i].w); } }第一次调用时任选起点,这里以1为例,同时起点的父节点也可以任取:dfs(1,0,0)。
递归结束后id中保存距离起点最远的节点标号。
第二次调用时以id为起点:dfs(id,0,0)。
递归结束后_max保存树上最长距离,即树的直径。
-
树形dp,dp1[i]记录以i为根的子树中最长路径长度,dp2[i]记录以i为根的子树中次长路径长度。(实际的根不变)
具体代码如下:
void dfs(int now, int fa)//dfs的任务就是更新dp[now] { for(int i = head[now]; i; i = edge[i].next)//遍历每条边 { if(edge[i].to == fa) continue; dfs(edge[i].to, now); if(dp1[edge[i].to]+edge[i].w > dp1[now]) { dp2[now] = dp1[now]; dp1[now] = dp1[edge[i].to]+edge[i].w; } else if(dp2[now] < dp1[edge[i].to]+edge[i].w) dp2[now] = dp1[edge[i].to]+edge[i].w; } ans = max(dp1[now]+dp2[now], ans); }