最近公共祖先
公共祖先: 在一棵有根树上,若节点
最近公共祖先(LCA): 在
LCA 显然有以下性质。
- 在所有公共祖先中,
到 和 的距离都是最短的。例如,在 和 的所有祖先中, 距离更短。 与 之间最短的路径经过 。例如,从 到 的最短路径经过 。 和 本身也可以是它们自己的公共祖先。若 是 的祖先,则有 ,如图中 。
如何求 LCA?根据 LCA 的定义,很容易想到一个简单直接的方法:分别从
倍增法求 LCA
可以把标记法换一种方式实现,分为以下两个步骤。
- 先把
和 提到相同的深度。例如, 比 深,就把 提到 的高度(既让 走到 的同一高度),如果发现 直接就跳到 的位置上了,那么就停止查找,否则继续下一步。 - 让
和 继续同步向上走,每走一步就判断是否相遇,相遇点就是 停止。
上面的两个步骤,如果
步骤 1
把
因为已知条件是只知道每个节点的父节点,所以如果没有其他辅助条件,
有了预计算出的这些祖先做跳板,能从
- 从
跳 步,到达 的第 个祖先 ; - 从
跳 步,到达 的第 个祖先 ; - 从
跳 步到达祖先 ; - 从
跳 步到达祖先 。
共跳了
显然,用倍增法从
剩下的问题是如何快速预计算每个节点的“倍增”的祖先。定义
特别地,
步骤 2
经过上一个步骤,
从一个节点跳到根节点,最多跳
,这是一个公共祖先,它的深度小于或等于 LCA(x, y),这说明跳过头了,退回去换一个小的 重新跳一次。 ,说明还没跳到公共祖先,那么更新 ,从新的起点 继续开始跳。由于新的 的深度比原来位置的深度减少超过一半,再跳时就不用跳 步,跳 步就够了。
以上两种情况,分别是比 LCA(x, y) 浅和深的两种位置。用
查询一次 LCA 的时间复杂度是多少?这里的
倍增法的总计算量包括预计算
和查询 次 LCA,总时间复杂度为 。
例题:P3379 【模板】最近公共祖先(LCA)
参考代码
#include <cstdio> #include <vector> #include <algorithm> using namespace std; const int N = 500005; const int LOG = 19; vector<int> tree[N]; int depth[N], fa[N][LOG]; void dfs(int cur, int pre) { depth[cur] = depth[pre] + 1; // 深度比父节点深度多1 for (int nxt : tree[cur]) { // 遍历所有邻居节点 if (nxt != pre) { // 除了父节点以外都是子节点 fa[nxt][0] = cur; // 记录父节点fa[][0] dfs(nxt, cur); } } } int lca(int x, int y) { if (depth[x] < depth[y]) swap(x, y); // 保证x深度更大 // 将x和y提到相同高度 int delta = depth[x] - depth[y]; for (int i = LOG - 1; i >= 0; i--) if (delta & (1 << i)) x = fa[x][i]; if (x == y) return x; // 如果提到相同深度后已经重合,则直接返回 // x和y同步往上跳 for (int i = LOG - 1; i >= 0; i--) if (fa[x][i] != fa[y][i]) { // 如果祖先相等,说明跳过头了,换一个小的i继续尝试 x = fa[x][i]; y = fa[y][i]; // 如果祖先不相等,就更新x和y继续跳 } // 最终x和y位于LCA的下一层,此时x或y的父节点就是LCA return fa[x][0]; } int main() { int n, m, s; scanf("%d%d%d", &n, &m, &s); for (int i = 1; i < n; i++) { int x, y; scanf("%d%d", &x, &y); // 建树 tree[x].push_back(y); tree[y].push_back(x); } dfs(s, 0); // 预处理深度等信息 for (int i = 1; i < LOG; i++) for (int j = 1; j <= n; j++) fa[j][i] = fa[fa[j][i - 1]][i - 1]; // 从fa[][0]开始递推 while (m--) { int a, b; scanf("%d%d", &a, &b); printf("%d\n", lca(a, b)); } return 0; }
常见的应用思想:
- 对于
间的路径,拆成 到 和 到 分别倍增。 - 对于
间的路径,拆成 到 和 到 ,去掉 到 。
例题:P1967 [NOIP2013 提高组] 货车运输
分析:假设询问的点是
如果只有一次询问,可以按边权从大到小加边,直至
现在问题就放到树上了。如果把路径拆成
在倍增求祖先时同时维护这段路径上的边权最小值,进行询问的计算时,在找
参考代码
#include <cstdio> #include <utility> #include <algorithm> #include <vector> using std::sort; using std::min; using std::swap; using std::pair; using std::vector; using g_edge = pair<int, pair<int, int>>; // (边权,(x,y)) using t_edge = pair<int, int>; // (点,边权) const int N = 10005; const int M = 50005; const int LOG = 14; const int INF = 100000; g_edge e[M]; vector<t_edge> tree[N]; bool vis[N]; int d[N], f[N][LOG], minw[N][LOG]; struct DSU { int fa[N]; void init(int n) { for (int i = 1; i <= n; i++) fa[i] = i; } int query(int x) { return fa[x] == x ? x : fa[x] = query(fa[x]); } bool merge(int x, int y) { int qx = query(x), qy = query(y); if (qx != qy) fa[qx] = qy; return qx != qy; } }; DSU dsu; void dfs(int u, int fa) { vis[u] = true; for (t_edge e : tree[u]) { int v = e.first, w = e.second; if (v == fa) continue; d[v] = d[u] + 1; f[v][0] = u; minw[v][0] = w; dfs(v, u); } } int lca(int x, int y) { // 这个lca函数求的是路径上的最小边权 if (d[x] < d[y]) swap(x, y); int delta = d[x] - d[y]; int res = INF; for (int i = LOG - 1; i >= 0; i--) if (delta & (1 << i)) { res = min(res, minw[x][i]); x = f[x][i]; } if (x == y) return res; for (int i = LOG - 1; i >= 0; i--) { if (f[x][i] != f[y][i]) { res = min(res, min(minw[x][i], minw[y][i])); x = f[x][i]; y = f[y][i]; } } // 别忘了最后两条边 res = min(res, min(minw[x][0], minw[y][0])); return res; } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { int x, y, z; scanf("%d%d%d", &x, &y, &z); e[i] = {z, {x, y}}; } // 构建最大生成树 sort(e + 1, e + m + 1); dsu.init(n); for (int i = m; i >= 1; i--) { int w = e[i].first; int x = e[i].second.first, y = e[i].second.second; if (dsu.merge(x, y)) { tree[x].push_back({y, w}); tree[y].push_back({x, w}); } } // 倍增预处理 for (int i = 1; i <= n; i++) if (!vis[i]) dfs(i, 0); for (int j = 1; j < LOG; j++) { for (int i = 1; i <= n; i++) { f[i][j] = f[f[i][j - 1]][j - 1]; minw[i][j] = min(minw[i][j - 1], minw[f[i][j - 1]][j - 1]); } } int q; scanf("%d", &q); for (int i = 1; i <= q; i++) { int x, y; scanf("%d%d", &x, &y); printf("%d\n", dsu.query(x) != dsu.query(y) ? -1 : lca(x, y)); } return 0; }
例题:P4180 [BJWC2010] 严格次小生成树
设一张图的最小生成树边权之和为
,则该图的严格次小生成树定义为该图所有边权之和大于 的生成树中边权之和最小者(可能不存在,也可能存在多棵)。
现给出一张个点, 条边的无向图,边权为 ,求出该图的严格次小生成树边权之和。数据保证原图存在严格次小生成树。
数据范围:。
分析:一种简单的思路是尝试找到原图的所有生成树,然后通过比较得出答案。但由于生成树数量过多,这样的算法显然效率很低。
由于严格次小生成树的边权和仅大于最小生成树边权和,因此可以猜测,严格次小生成树很可能就是在最小生成树上替换一条或几条边得到。事实上,可以证明,一定存在一棵严格次小生成树,使得它与某棵最小生成树仅有一条边的差距。
考虑一条不在原来的最小生成树上的边,如果把它加入最小生成树后会形成一个环,显然这个环上其他边的边权都小于等于刚加的这条边的边权(不然一开始的最小生成树就不成立了)。更进一步,这里可以把等于的情况去掉,因为如果存在等于的情况,说明是另一棵边权和相等但树的形态不同的最小生成树。所以如果严格次小生成树和最小生成树之间有两条以上的边不同,那么我们可以把这些不同的边中的其中一条改为在最小生成树上的边,剩下的不变,则此时得到的生成树边权和变小了,但还是比最小生成树的边权和要大。由此得知,最多只选
有了这个性质,就可以考虑在建完最小生成树之后寻找那条不属于最小生成树,但属于严格次小生成树的边。枚举每一条非树边,在加入这条边之后,生成树上出现了一个环,再断掉环中其他边(环中其他的边实际上就是这条非树边的两点在最小生成树中的路径)里面边权最大的边(若该边边权与环内其他边权最大者相等,则断掉边权中严格次大的,注意有可能不存在这样的严格次大边),那么就得到了包含这条边的生成树中权值最小的。将所有这样的生成树权值和取
可以采用树上倍增的方法,定义
整个算法的时间复杂度为
#include <cstdio> #include <algorithm> #include <vector> using std::sort; using std::swap; using std::min; using std::max; using std::vector; typedef long long LL; const int N = 1e5 + 5; const int M = 3e5 + 5; const int LOG = 17; const LL INF = 1e15; struct Edge { int x, y, z; }; Edge edges[M]; bool mst[M]; // 记录每条边是否是最小生成树上的边 vector<Edge> tree[N]; // root用于并查集 // depth存储节点深度 // fa/w1/w2[u][i]代表节点u向上2的i次方层祖先/边权最大值/边权次大值 int root[N], depth[N], fa[N][LOG], w1[N][LOG], w2[N][LOG]; int query(int x) { return root[x] == x ? x : root[x] = query(root[x]); } // update函数实现对两组最大、次大值合并得到新的最大、次大值 void update(int& mx1, int& mx2, int a1, int a2, int b1, int b2) { mx1 = max(a1, b1); mx2 = a1 == b1 ? max(a2, b2) : max(min(a1, b1), max(a2, b2)); } void dfs(int u, int pre) { depth[u] = depth[pre] + 1; fa[u][0] = pre; for (Edge e : tree[u]) { int v = e.y, w = e.z; if (v == pre) continue; w1[v][0] = w; dfs(v, u); } } LL lca(int x, int y, int w, LL sum) { // 在倍增法求lca的过程中实现最大边权和次大边权的计算 if (depth[x] < depth[y]) swap(x, y); int delta = depth[x] - depth[y]; int res1 = 0, res2 = 0; // 最大、严格次大 for (int i = LOG - 1; i >= 0; i--) if (delta & (1 << i)) { update(res1, res2, res1, res2, w1[x][i], w2[x][i]); x = fa[x][i]; } if (x == y) { // 有可能加的非树边与环内其他最长边相等 // 也有可能环内不存在次长边 if (res1 == w) return res2 == 0 ? INF : sum + w - res2; else return res1 == 0 ? INF : sum + w - res1; } int tmp1 = 0, tmp2 = 0; for (int i = LOG - 1; i >= 0; i--) if (fa[x][i] != fa[y][i]) { update(tmp1, tmp2, w1[x][i], w2[x][i], w1[y][i], w2[y][i]); update(res1, res2, res1, res2, tmp1, tmp2); x = fa[x][i]; y = fa[y][i]; } update(tmp1, tmp2, w1[x][0], w2[x][0], w1[y][0], w2[y][0]); update(res1, res2, res1, res2, tmp1, tmp2); // 有可能加的非树边与环内其他最长边相等 // 也有可能环内不存在次长边 if (res1 == w) return res2 == 0 ? INF : sum + w - res2; else return res1 == 0 ? INF : sum + w - res1; } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) root[i] = i; for (int i = 1; i <= m; i++) { scanf("%d%d%d", &edges[i].x, &edges[i].y, &edges[i].z); } // 先求一棵最小生成树 sort(edges + 1, edges + m + 1, [](Edge& e1, Edge& e2) { return e1.z < e2.z; }); LL sum = 0; for (int i = 1; i <= m; i++) { int x = edges[i].x, y = edges[i].y, z = edges[i].z; int qx = query(x), qy = query(y); if (qx != qy) { root[qx] = qy; sum += z; mst[i] = true; tree[x].push_back({x, y, z}); tree[y].push_back({y, x, z}); } } dfs(1, 0); for (int i = 1; i < LOG; i++) for (int j = 1; j <= n; j++) { // 预处理倍增表 fa[j][i] = fa[fa[j][i - 1]][i - 1]; int a1 = w1[j][i - 1], a2 = w2[j][i - 1]; int b1 = w1[fa[j][i - 1]][i - 1], b2 = w2[fa[j][i - 1]][i - 1]; update(w1[j][i], w2[j][i], a1, a2, b1, b2); } LL ans = INF; for (int i = 1; i <= m; i++) { int x = edges[i].x, y = edges[i].y, z = edges[i].z; if (x == y) continue; if (!mst[i]) ans = min(ans, lca(x, y, z, sum)); } printf("%lld\n", ans); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!