LCA
一、定义
给定一颗有根数,若节点 z z z 即使节点 x x x 的祖先,又是节点 y y y 的祖先,则称 z z z 是 x x x 与 y y y 的公共祖先;
在 x x x 与 y y y 的所有公共祖先中,深度最大的一个则为 x x x 与 y y y 的最近公共祖先(LCA, Lowest Common Ancestor);
如图所示,
则
- b b b 与 c c c 的 LCA 为 a a a ;
- d d d 与 c c c 的 LCA 为 a a a ;
LCA 是唯一的;
二、特点
对于两个节点 u u u 与 v v v ,它们的相互关系与 LCA 必定是下列情况中的一种;
- v v v 是 u u u 的子节点 (或子孙节点) , 则 L C A ( u , v ) LCA(u, v) LCA(u,v) 为 u u u ;
- v v v 是 u u u 的父节点 (或祖先节点) , 则 L C A ( u , v ) LCA(u, v) LCA(u,v) 为 v v v ;
- u u u 是节点 w w w 的子节点 (或子孙节点) , v v v 是 w w w 的子节点 (或子孙节点) , 则 u u u 与 v v v 的 LCA 为 w w w 的父节点 (或祖先节点) ;
根据以上情况,可得到结论;
给定树上的节点 u u u ,则 u u u 与 u u u 的子树节点的 LCA 为 u u u , u u u 的不同子树节点之间的 LCA 也为 u u u ;
三、朴素算法
1. 思路
通过搜索,可确定每个节点的父节点,再通过父节点进行逆向查找,从而得到一条从指定节点到根节点的路径,两个点的 LCA 即为两条路径的第一个交点;
2. 过程
求 u , v u,v u,v 的 LCA 时,可以先确定节点 u u u 与节点 v v v 的所有祖先节点,从 u u u 与 v v v 从下向上对其父节点进行标记,第一个被两个节点标记的即为 u , v u, v u,v 的 LCA;
3.代码
以 n n n 个节点, m m m 条边,根结点为 r o o t root root 的树为例;
int n, m, root, father[MAXN];
vector < int > g[MAXN];
bool flag[MAXN];
void dfs(int i) { // dfs 预处理出节点的父节点
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (!flag[v]) {
father[v] = i;
dfs(v);
}
}
return;
}
int LCA(int u, int v) {
bool f[MAXN] = { };
while (u != root) { // 先标记 u 节点的父节点
f[u] = true;
u = father[u];
}
while (v != root) { // 标记 v 节点的父节点
if (f[v] == true) return v; // 若被重复标记,则已找到
v = father[v];
}
return root;
}
若有 q q q 个询问,则算法时间复杂度为 O ( q n l o g n ) O(qnlogn) O(qnlogn)
4. 优化
朴素算法查询中,需要反复查询才能确定两个节点的 LCA,效率较低,则可优化为两点一起向上走;
过程
搜索预处理时,处理出节点的父节点以及节点的深度;
对于节点 u , v u, v u,v ,若两节点的深度不同,则先将深度较大的节点沿着其的父结点移动至两点的深度相同,接下来,则可将两个节点每次同时向上移动一个节点,直到到同一个节点为止,此节点则为 u , v u, v u,v 的 LCA;
与朴素算法相比,优化利用了节点的深度,从而避免了反复查询,其时间复杂度为 O ( q n ) O(qn) O(qn) ;
代码
以 n n n 个节点, m m m 条边,根结点为 r o o t root root 的树为例;
int n, m, root, father[MAXN], dep[MAXN];
vector < int > g[MAXN];
bool flag[MAXN];
void dfs(int i) { // dfs 预处理出节点的父节点以及节点的深度
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (!flag[v]) {
dep[v] = dep[i] + 1;
father[v] = i;
dfs(v);
}
}
return;
}
int LCA(int u, int v) {
if (dep[u] < dep[v]) swap(u, v); // 为了方便处理,将 u 节点深度设为较大的
int dis = dep[u] - dep[v]; // 计算两点深度差
while (dis--) {
u = father[u]; // 使两点深度相同
}
while (u != v) { // 再使两点一起向上走至同一个结点,即为 LCA
u = father[u];
v = father[v];
}
return u;
}
四、倍增
朴素算法中,由于每次只能向上跳动一个节点,其效率较低;
如果能向上跳动时,跳过某些明显不可能是最近公共祖先的节点,其求解速度会明显提高;
1. 思路
利用二进制拆分原理,在每次向上移动节点时,以2的次数幂移动;
假设总共 n n n 个节点,则最大的跳跃跨度幂次为为 i = ⌈ l o g ∣ n ∣ ⌉ i = \lceil log |n| \rceil i=⌈log∣n∣⌉ ,即从叶结点向上跳跃 2 ⌈ l o g ∣ n ∣ ⌉ 2^{\lceil log |n| \rceil} 2⌈log∣n∣⌉ 个结点一定可以到达根结点;
预处理时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) ,查询时间复杂度为 O ( q l o g n ) O(qlogn) O(qlogn) ;
2. 过程
预处理
预处理出节点的深度以及节点第 2 i 2^i 2i 个祖先;
则可用树形动态规划进行处理;
状态
d p [ i ] [ j ] dp[i][j] dp[i][j] 节点 i i i 的第 2 j 2^j 2j 个祖先;
j j j 开到 32 即可 ( 2 31 = 2147483648 ) (2^{31} = 2147483648) (231=2147483648);
转移
根据2的幂次特性,
2
i
=
2
i
−
1
+
2
i
−
1
2^i = 2^{i - 1} + 2^{i - 1}
2i=2i−1+2i−1 ,节点
i
i
i 的第
2
j
2^j
2j 个结点即为节点
i
i
i 的第
2
i
−
1
2^{i - 1}
2i−1 个祖先节点的第
2
i
−
1
2^{i - 1}
2i−1 个祖先节点;
d
p
[
i
]
[
j
]
=
d
p
[
d
p
[
u
]
[
j
−
1
]
]
[
j
−
1
]
dp[i][j] = dp[dp[u][j - 1]][j - 1]
dp[i][j]=dp[dp[u][j−1]][j−1]
由于有2的幂次特性以及二进制拆分原理,可分别保证该 DP 的转移与正确性;
查询
思路
对于查询 ( u , v ) (u, v) (u,v) ;
先将 u u u 与 v v v 调整为相同深度,即将深度较大的节点每次向上跳 2 l o g 2 ( d e p [ u ] − d e p [ v ] ) 2^{log2(dep[u] - dep[v])} 2log2(dep[u]−dep[v]) 步,跳到两节点深度相同;
将两点同此向上跳 2 j j ∈ ( 0 , l o g 2 [ d e p [ v ] ] ) 2^j j\in (0, log2[dep[v]]) 2jj∈(0,log2[dep[v]]) 步,如果两点跳到的祖先结点不同,则当前点不可能为两点 LCA,可继续向上跳;当两点跳到相同祖先结点时,当前结点即为两点 LCA;
对于 j j j ,由于跳跃步数为2的幂次呈指数级增长,所以指数越大,增长越快,所以从大向小枚举,可使枚举次数更少
证明
如果倍增跳两点跳到的点不同,应继续向上跳
假设 u , v u, v u,v 分别跳到了 ( j , k ) (j, k) (j,k) , L C A ( u , v ) LCA(u, v) LCA(u,v) 为 i i i ;
- 当 d e p [ j ] = d e p [ k ] > d e p [ i ] dep[j] = dep[k] > dep[i] dep[j]=dep[k]>dep[i] 时;
则 i i i 点深度较 j , k j, k j,k 浅, ( j , k ) (j, k) (j,k) 应继续向上跳;
- 当 d e p [ j ] = d e p [ k ] < d e p [ i ] dep[j] = dep[k] < dep[i] dep[j]=dep[k]<dep[i] 时;
由于此结构为有根树,对于这种情况,其树根只能是 i i i ,又因为向上跳的 2 j j ∈ ( 0 , l o g 2 [ d e p [ v ] ] ) 2^j j\in (0, log2[dep[v]]) 2jj∈(0,log2[dep[v]]) 步,则矛盾;
3. 代码
int n, m, log1[MAXN], dep[MAXN], dp[MAXN][MAXX], root;
vector < int > g[MAXN];
bool flag[MAXN];
void logset() { // 预处理 log
log1[1] = 0;
for (int i = 2; i <= MAXN; i++) {
log1[i] = log1[i / 2] + 1;
}
return;
}
void dfs(int i) {
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (!flag[v]) {
dep[v] = dep[i] + 1; // 预处理深度
dp[v][0] = i; // v 节点向上跳1步,即为其父节点 i
for (int j = 1; j <= log1[dep[v]]; j++) {
dp[v][j] = dp[dp[v][j - 1]][j - 1];
}
dfs(v);
}
}
return;
}
int LCA(int u, int v) {
if (dep[u] < dep[v]) swap(u, v); // 为了方便处理,将 u 节点深度设为较大的
while (dep[u] != dep[v]) { // 将两点跳到相同深度
u = dp[u][log1[dep[u] - dep[v]]]; // 每次最多跳 log2(dep[u] - dep[v]) 步
}
if (u == v) return u; // 已经找到 LCA
for (int i = log1[dep[u]]; i >= 0; i--) { // 倒序枚举,枚举次数较正向枚举较小
if (dp[u][i] != dp[v][i]) {
u = dp[u][i], v = dp[v][i]; // 同时向根结点条
}
}
return dp[u][0];
}
五、Tarjan 算法
1. 思路
若已知所有寻要查询的 LCA 节点对,则可使用更为高效的离线算法 Tarjan 算法;
Tarjan 算法结合了 DFS 遍历图以及 LCA 的如下特点;
对于节点 u u u 与 u u u 的任意一个子节点 v v v ,以下两者相同
- v v v 与树中除了以 u u u 为根的子树以外的其他任意节点 x x x 的 LCA;
- u u u 与树中除了以 u u u 为根的子树以外的其他任意节点 x x x 的 LCA;
所以,可将 u u u 与以 u u u 为根的子树看作一个结点的集合,将 u u u 看作这个集合的代表,即集合中的节点与集合之外的树中任意节点的 LCA 等同于 u u u 与集合之外的树中任意节点的 LCA ;
由于树具有 “自相似” 的内部结构,即可将树看作很多个类似集合的组合,使用集合的代表进行 LCA 求解来得到集合中节点的解;
2. 过程
利用 DFS 遍历与并查集实现,并查集维护节点关系;
即在 DFS 遍历时,将根结点与其子节点合并;
由于两个节点通过其 LCA 被合并,所以在合并完子树后,遍历与当前节点有关的查询,若查询的另一个节点也已访问,则两点此时一定有且仅通过其的 LCA 合并,所以节点对所在并查集的根结点即为节点对的 LCA ;
即,Tarjan 算法按照深度优先遍历过程,从一个指定节点 u u u 开始遍历,逐个访问 u u u 的子节点 v v v ,递归求解节点对的 LCA ;
当遍历到节点时,
- 遍历完节点的子节点,并将节点与其子节点合并;
- 将节点标记为已访问;
- 遍历与当前节点有查询关系的节点,如果两个节点均已访问,那么节点对的 LCA 就是所在并查集的根结点,记录即可;
注意
由于询问的节点对先后顺序不一定按照算法遍历中结点的先后顺序给出,所以存储节点对时,应当双向存储,保证两节点不受访问先后顺序影响;
3. 代码
int n, m, root, father[MAXN], ans[MAXN];
vector < int > g[MAXN];
vector < int > q[MAXN]; // 存储访问
vector < int > q_id[MAXN]; // 存储访问编号
bool flag[MAXN], colour[MAXN]; // 标记节点
void firstset(int n) {
for (int i = 1; i <= n; i++) {
father[i] = i;
}
return;
}
int findset(int x) {
if (x == father[x]) return x;
return father[x] = findset(father[x]);
}
void push(int x, int y) {
father[findset(x)] = findset(y);
return;
}
void tarjan(int i) {
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (!flag[v]) {
tarjan(v); // DFS 遍历树
push(v, i); // 合并并查集
}
}
colour[i] = true; // 标记为已访问
for (int t = 0; t < q[i].size(); t++) { // 与当前节点有关的问题
int v = q[i][t], id = q_id[i][t];
if (colour[v]) { // 如果两节点均被访问
ans[id] = findset(v); // 即为两点所在集合的根结点
}
}
return;
}
int main() {
scanf("%d %d %d", &n, &m, &root);
firstset(n);
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x].push_back(y);
g[y].push_back(x);
}
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
q[x].push_back(y), q_id[x].push_back(i); // 双向建立节点对
q[y].push_back(x), q_id[y].push_back(i);
}
tarjan(root);
for (int i = 1; i <= m; i++) {
printf("%d\n", ans[i]);
}
return 0;
}
六、欧拉序RMQ算法
1. 思路
由于欧拉序可以将树形结构转化为线性结构,LCA 求两点深度最大的公共祖先,想到用欧拉序与 RMQ 维护区间最大值计算;
在欧拉序中,遍历的顺序为先遍历一棵子树,再遍历根结点,从而遍历其他的子树;
又因为在欧拉序中节点 x x x 第一次出现的位置到最后一次出现的位置之间都是 x x x 子树上的节点;
所以在 x x x 节点最后一次出现到 y y y 节点第一次出现间的节点即为 x x x 与 y y y 的公共祖先;
但因为 x x x 可能为 y y y 的祖先,或 y y y 可能为 x x x 的祖先;
所以因查找 x x x 与 y y y 第一次出现的区间内的节点;
用 RMQ 计算这个区间内深度最大的节点即可;
2. 过程
先遍历出树的欧拉序;
再用 RMQ 维护欧拉序中节点深度的最大值;
当查询 u u u 与 v v v 的 LCA 时,查询在欧拉序中 u u u 第一次出现位置与 v v v 第一次出现位置的区间内的深度最大值所在的节点即为 u u u 与 v v v 的 LCA;
3. 代码
int n, m, root, dep[MAXN], a[MAXN], len, b[MAXN], fi[MAXN];
int dp[MAXN][MAXX], pre[MAXN][MAXX], log1[MAXN];
vector < int > g[MAXN];
bool flag[MAXN];
void dfs(int i) { // 遍历欧拉序
flag[i] = true;
a[++len] = i;
b[len] = dep[a[len]];
fi[a[len]] = len;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (!flag[v]) {
dep[v] = dep[i] + 1;
dfs(v);
a[++len] = i;
b[len] = dep[a[len]];
}
}
return;
}
void init(int len) { // RMQ 维护欧拉序中深度最大值
for (int i = 1; i <= len; i++) {
dp[i][0] = b[i]; // dp 存储节点深度
pre[i][0] = a[i]; // pre 存储节点编号
}
for (int j = 1; (1 << j) <= len; j++) {
for (int i = 1; i + (1 << j) - 1 <= len; i++) {
if (dp[i][j - 1] < dp[i + (1 << j - 1)][j - 1]) {
dp[i][j] = dp[i][j - 1];
pre[i][j] = pre[i][j - 1];
} else {
dp[i][j] = dp[i + (1 << j - 1)][j - 1];
pre[i][j] = pre[i + (1 << j - 1)][j - 1];
}
}
}
return;
}
void logset() { // 预处理 log
log1[1] = 0;
for (int i = 2; i <= MAXN; i++) {
log1[i] = log1[i / 2] + 1;
}
return;
}
void firstset(int root) {
dfs(root); // 进行欧拉序遍历
init(len); // RMQ 预处理
logset(); // log 预处理
return;
}
int LCA(int l, int r) {
l = fi[l], r = fi[r]; // l,r 第一次出现位置
if (l > r) swap(l, r);
int k = log1[r - l + 1];
if (dp[l][k] < dp[r - (1 << k) + 1][k]) {
return pre[l][k];
} else {
return pre[r - (1 << k) + 1][k]; // 返回深度最大的节点编号
}
}
int main() {
scanf("%d %d %d", &n, &m, &root);
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x].push_back(y);
g[y].push_back(x);
}
firstset(root);
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
printf("%d\n", LCA(x, y));
}
return 0;
}
七、LCA应用
1. 求两点距离
思路
在树上,两点 ( u , v ) (u, v) (u,v) 的最短路径则为从 u u u 向上走到 L C A ( u , v ) LCA(u, v) LCA(u,v) 再向下走到 v v v ;
证明
若不经过两点的 LCA 继续向上走,由于是树形结构,所以一定会重复走边,则不是最短;
代码
int dis(int u, int v) {
return dep[u] - dep[v] - 2 * dep[LCA(u, v)];
}
2.树上差分
思路
树上差分可实现快速对两点路径上的节点权值进行修改;
以在 u u u 与 v v v 的路径上节点增加 x x x , i i i 节点的权值为 v a l i val_i vali 为例;
则先在 u u u 与 v v v 的权值上加 x x x ,但由于前缀和时为从深度大的节点像深度小的节点加,所以所有从 u , v u, v u,v 到根结点的路径上的节点均会 + x +x +x ;
则为了不影响到除了路径以外的点权值,则在 u u u 到 v v v 路径上深度最小的节点 L C A ( u , v ) LCA(u, v) LCA(u,v) 上使 v a l L C A ( u , v ) − = x ∗ 2 val_{LCA(u, v)} -= x * 2 valLCA(u,v)−=x∗2 即可,由于 u u u 与 v v v 上各增加了 x x x ,则到 LCA 除一共增加了两个 x x x ,所以减去 2 ∗ x 2 * x 2∗x ;
则修改方法为 v a l u + x , v a l v + x , v a l L C A ( u , v ) − = x ∗ 2 val_u + x, val_v + x, val_{LCA(u, v)} -= x * 2 valu+x,valv+x,valLCA(u,v)−=x∗2 ;
再做一次树上前缀和即可得到节点权值;
每条边遍历的次数即为该边所连接两点中深度较小的节点的权值;
代码
void change(int u, int v, int x) { // u 到 v 路径上增加 x
c[u] += x;
c[v] += x;
c[LCA(u, v)] -= 2 * x;
}
void dfs1(int i) { // 树上前缀和
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (!flag[v]) {
dfs1(v);
c[i] += c[v];
}
}
return;
}
3.次小生成树
思路
通过替换最小生成树中的一条边得到次小生成树;
当将一条非树边 ( x , y , z ) (x, y, z) (x,y,z) 加入最小生成树时,则会与 x x x 到 y y y 路径上的节点一起形成环;
则需替换一条边,设 x x x 到 y y y 的路径上的最大边权为 v a l 1 val_1 val1 ,严格次大边权为 v a l 2 val_2 val2 ,则对于次小生成树有两种情况;
- 若 z > v a l 1 z > val_1 z>val1 ,则把 v a l 1 val_1 val1 对应的边替换为 ( x , y , z ) (x, y , z) (x,y,z) ,此时严格次小生成树边权和为 s u m − v a l 1 + z sum - val_1 + z sum−val1+z ;
- 若 z = = v a l 1 z == val_1 z==val1 ,则把 v a l 2 val_2 val2 对应的边替换为 ( x , y , z ) (x, y , z) (x,y,z) ,此时严格次小生成树边权和为 s u m − v a l 2 + z sum - val_2 + z sum−val2+z ;
则枚举每条非树边,添加到最小生成树中,按上述条件计算出候选答案,从中选择最小的即可;
则问题转化为如何求 x x x 到 y y y 的路径上的最大边权与严格次大边权;
使用树上倍增法,
d p [ i ] [ j ] dp[i][j] dp[i][j] 表示节点 i i i 的第 2 j 2^j 2j 个祖先;
则转移同倍增 LCA 算法
d
p
[
i
]
[
j
]
=
d
p
[
d
p
[
u
]
[
j
−
1
]
]
[
j
−
1
]
dp[i][j] = dp[dp[u][j - 1]][j - 1]
dp[i][j]=dp[dp[u][j−1]][j−1]
d
p
1
[
i
]
[
k
]
dp1[i][k]
dp1[i][k] 表示从节点
i
i
i 到其的第
2
j
2^j
2j 个祖先的路径上的最大边权;
转移时类似 ST 表的转移,即取 从节点
i
i
i 到其的第
2
j
−
1
2^{j - 1}
2j−1 个祖先的路径上的最大边权 与 从节点
i
i
i 的第
2
j
−
1
2^{j - 1}
2j−1 个祖先到其的第
2
j
−
1
2^{j - 1}
2j−1 个祖先的路径上的最大边权 的最大值即可;
d
p
1
[
i
]
[
j
]
=
max
{
d
p
1
[
i
]
[
j
−
1
]
,
d
p
1
[
d
p
[
i
]
[
j
−
1
]
]
[
j
−
1
]
}
dp1[i][j] = \max \{ dp1[i][j - 1], dp1[dp[i][j - 1]][j - 1] \}
dp1[i][j]=max{dp1[i][j−1],dp1[dp[i][j−1]][j−1]}
d
p
2
[
i
]
[
k
]
dp2[i][k]
dp2[i][k] 表示从节点
i
i
i 到其的第
2
j
2^j
2j 个祖先的路径上的严格次大边权;
对于转移,有 3 种情况;
-
d p 1 [ i ] [ j − 1 ] > d p 1 [ d p [ i ] [ j − 1 ] ] [ j − 1 ] dp1[i][j - 1] > dp1[dp[i][j - 1]][j - 1] dp1[i][j−1]>dp1[dp[i][j−1]][j−1]
即最大边权在 节点 i i i 到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上时,则次大边权不能选取 节点 i i i 到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上的最大值,则选取 节点 i i i 到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上的次大值 以及 节点 i i i 的第 2 j − 1 2^{j - 1} 2j−1 个祖先到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上的最大边权 的最大值即可;
状态转移方程如下,
d p 2 [ i ] [ j ] = max { d p 2 [ i ] [ j − 1 ] , d p 1 [ d p [ i ] [ j − 1 ] ] [ j − 1 ] } dp2[i][j] = \max \{ dp2[i][j - 1], dp1[dp[i][j - 1]][j - 1] \} dp2[i][j]=max{dp2[i][j−1],dp1[dp[i][j−1]][j−1]} -
d p 1 [ i ] [ j − 1 ] < d p 1 [ d p [ i ] [ j − 1 ] ] [ j − 1 ] dp1[i][j - 1] < dp1[dp[i][j - 1]][j - 1] dp1[i][j−1]<dp1[dp[i][j−1]][j−1]
即最大边权在 节点 i i i 的第 2 j − 1 2^{j - 1} 2j−1 个祖先到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上时,则次大边权不能选取 节点 i i i 的第 2 j − 1 2^{j - 1} 2j−1 个祖先到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上的最大值,则选取 节点 i i i 的第 2 j − 1 2^{j - 1} 2j−1 个祖先到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上的次大值 以及 节点 i i i 到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上的最大边权 的最大值即可;
状态转移方程如下,
d p 2 [ i ] [ j ] = max { d p 1 [ i ] [ j − 1 ] , d p 2 [ d p [ i ] [ j − 1 ] ] [ j − 1 ] } dp2[i][j] = \max \{ dp1[i][j - 1], dp2[dp[i][j - 1]][j - 1] \} dp2[i][j]=max{dp1[i][j−1],dp2[dp[i][j−1]][j−1]} -
d p 1 [ d p [ i ] [ j − 1 ] ] [ j − 1 ] = = d p 1 [ i ] [ j − 1 ] dp1[dp[i][j - 1]][j - 1] == dp1[i][j - 1] dp1[dp[i][j−1]][j−1]==dp1[i][j−1]
即最大边权在 节点 i i i 到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 与 节点 i i i 的第 2 j − 1 2^{j - 1} 2j−1 个祖先到其的第 2 j − 1 2^{j - 1} 2j−1 个祖先的路径 上时,则选取这两条路径上的严格次小边权最大值即可;
状态转移方程如下,
d p 2 [ i ] [ j ] = max { d p 2 [ i ] [ j − 1 ] , d p 2 [ d p [ i ] [ j − 1 ] ] [ j − 1 ] } dp2[i][j] = \max \{ dp2[i][j - 1], dp2[dp[i][j - 1]][j - 1] \} dp2[i][j]=max{dp2[i][j−1],dp2[dp[i][j−1]][j−1]}
综上,有
d
p
2
[
i
]
[
j
]
=
{
max
{
d
p
2
[
i
]
[
j
−
1
]
,
d
p
1
[
d
p
[
i
]
[
j
−
1
]
]
[
j
−
1
]
}
(
d
p
1
[
i
]
[
j
−
1
]
>
d
p
1
[
d
p
[
i
]
[
j
−
1
]
]
[
j
−
1
]
)
max
{
d
p
1
[
i
]
[
j
−
1
]
,
d
p
2
[
d
p
[
i
]
[
j
−
1
]
]
[
j
−
1
]
}
(
d
p
1
[
i
]
[
j
−
1
]
<
d
p
1
[
d
p
[
i
]
[
j
−
1
]
]
[
j
−
1
]
)
max
{
d
p
2
[
i
]
[
j
−
1
]
,
d
p
2
[
d
p
[
i
]
[
j
−
1
]
]
[
j
−
1
]
}
(
d
p
1
[
d
p
[
i
]
[
j
−
1
]
]
[
j
−
1
]
=
=
d
p
1
[
i
]
[
j
−
1
]
)
dp2[i][j] = \left\{ \begin{matrix} \max \{ dp2[i][j - 1], dp1[dp[i][j - 1]][j - 1] \} (dp1[i][j - 1] > dp1[dp[i][j - 1]][j - 1]) \\ \max \{ dp1[i][j - 1], dp2[dp[i][j - 1]][j - 1] \} (dp1[i][j - 1] < dp1[dp[i][j - 1]][j - 1]) \\ \max \{ dp2[i][j - 1], dp2[dp[i][j - 1]][j - 1] \} (dp1[dp[i][j - 1]][j - 1] == dp1[i][j - 1]) \\ \end{matrix} \right.
dp2[i][j]=⎩⎨⎧max{dp2[i][j−1],dp1[dp[i][j−1]][j−1]}(dp1[i][j−1]>dp1[dp[i][j−1]][j−1])max{dp1[i][j−1],dp2[dp[i][j−1]][j−1]}(dp1[i][j−1]<dp1[dp[i][j−1]][j−1])max{dp2[i][j−1],dp2[dp[i][j−1]][j−1]}(dp1[dp[i][j−1]][j−1]==dp1[i][j−1])
对于每条非树边
(
x
,
y
,
z
)
(x, y, z)
(x,y,z) ,使用类似 LCA 的倍增算法,求出
x
,
y
x, y
x,y 到
L
C
A
(
x
,
y
)
LCA(x, y)
LCA(x,y) 的路径中的边权最大及次大值;
即用两个变量 t o t 1 tot1 tot1 与 t o t 2 tot2 tot2 分被存储最大值与次大值;
当 x , y x, y x,y 节点向上走时, t o t 1 tot1 tot1 取 d p 1 dp1 dp1 数组的最大值, t o t 2 tot2 tot2 则取 d p 2 dp2 dp2 与 t o t 1 tot1 tot1 更新前的权值最大值即可;
代码
#include <cstdio>
#include <vector>
#include <algorithm>
#define MAXN 300005
#define MAXX 32
#define INF 1e16
using namespace std;
long long n, m, dep[MAXN], log1[MAXN], dp[MAXN][MAXX];
long long sum, ans = INF, father[MAXN], tot1, tot2, dp1[MAXN][MAXX], dp2[MAXN][MAXX];
bool flag[MAXN];
struct edge { // 存储边
int x, y;
long long z;
bool f;
bool operator < (const edge &a) const {
return z < a.z;
}
} e[MAXN];
struct edge1 { // 存储最小生成树
int to;
long long val;
};
vector <edge1> g[MAXN];
void firstset(int n) {
for (int i = 1; i <= n; i++) {
father[i] = i;
}
return;
}
int findset(int x) {
if (father[x] == x) return x;
return father[x] = findset(father[x]);
}
void push(int x, int y) {
int a = findset(x), b = findset(y);
if (a != b) {
father[a] = b;
}
return;
}
void Kru() { // 最小生成树
sort(1 + e, 1 + e + m);
firstset(n);
for (int i = 1; i <= m; i++) {
int x = findset(e[i].x), y = findset(e[i].y);
if (x != y) {
push(e[i].x, e[i].y);
g[e[i].x].push_back( edge1({e[i].y, e[i].z}) );
g[e[i].y].push_back( edge1({e[i].x, e[i].z}) );
sum += e[i].z;
e[i].f = true; // 标记为树边
}
}
return;
}
void logset() {
log1[1] = 0;
for (int i = 2; i <= MAXN; i++) {
log1[i] = log1[i / 2] + 1;
}
return;
}
void dfs(int i) { // 预处理
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t].to;
long long tot = g[i][t].val;
if (!flag[v]) {
dep[v] = dep[i] + 1;
dp[v][0] = i, dp1[v][0] = tot, dp2[v][0] = -INF;
for (int j = 1; j <= log1[dep[v]]; j++) {
dp[v][j] = dp[dp[v][j - 1]][j - 1];
dp1[v][j] = max(dp1[v][j - 1], dp1[dp[v][j - 1]][j - 1]);
if (dp1[v][j - 1] > dp1[dp[v][j - 1]][j - 1]) {
dp2[v][j] = max(dp2[v][j - 1], dp1[dp[v][j - 1]][j - 1]);
} else if (dp1[v][j - 1] < dp1[dp[v][j - 1]][j - 1]) {
dp2[v][j] = max(dp1[v][j - 1], dp2[dp[v][j - 1]][j - 1]);
} else {
dp2[v][j] = max(dp2[v][j - 1], dp2[dp[v][j - 1]][j - 1]);
}
}
dfs(v);
}
}
return;
}
void update(long long x) { // 更新 tot1, tot2
if (x > tot1) {
tot2 = tot1, tot1 = x;
} else if (x > tot2 && x != tot1) {
tot2 = x;
}
return;
}
void get_data(int u, int v) {
tot1 = -INF, tot2 = -INF;
if (dep[u] < dep[v]) swap(u, v);
while (dep[u] != dep[v]) {
update(dp1[u][log1[dep[u] - dep[v]]]);
update(dp2[u][log1[dep[u] - dep[v]]]);
u = dp[u][log1[dep[u] - dep[v]]];
}
if (u == v) return;
for (int i = log1[dep[u]]; i >= 0; i--) {
if (dp[u][i] != dp[v][i]) {
update(max(dp1[u][i], dp1[v][i]));
update(max(dp2[u][i], dp2[v][i]));
u = dp[u][i], v = dp[v][i];
}
}
update(max(dp1[u][0], dp1[v][0]));
update(max(dp2[u][0], dp2[v][0]));
return;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf("%d %d %lld", &e[i].x, &e[i].y, &e[i].z);
e[i].f = false;
}
Kru();
logset();
dfs(1);
for (int i = 1; i <= m; i++) {
if (!e[i].f) {
get_data(e[i].x, e[i].y); // 替换树边
if (tot1 < e[i].z) {
ans = min(ans, sum - tot1 + e[i].z);
} else if (tot1 == e[i].z) {
ans = min(ans, sum - tot2 + e[i].z);
}
}
}
printf("%lld", ans);
return 0;
}