树上公共祖先(LCA)
概念
给定一棵有根树,若节点
求解方法
1. 树上倍增法
思路:
由向上标记法优化而来。
向上标记法是每次向上走一步,效率较低。而树上倍增法优化了“走”的过程,每次向上走
设
其中
节点的深度为动态规划的“阶段”,所以应该对树执行广度优先遍历,按照层次顺序,在节点入队前,计算它对应的
这样,就可以在
对于每组询问
此时,如果节点
否则,再将
此时
综上所述,树上倍增法求
void bfs(int s) {
queue<int> q;
q.push(s);
dep[s] = 1;
while(q.size()) {
int t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(dep[j]) continue;
dep[j] = dep[t] + 1;
f[j][0] = t;
for(int k = 1; k <= T; k++) f[j][k] = f[f[j][k - 1]][k - 1];
q.push(j);
}
}
}
int lca(int x, int y) {
if(dep[x] > dep[y]) swap(x, y);
for(int i = T; i >= 0; i--) {
if(dep[f[y][i]] >= dep[x]) y = f[y][i];
}
if(x == y) return x;
for(int i = T; i >= 0; i--) {
if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
}
return f[x][0];
}
2.tarjan 算法
本质上也是对向上标记法的优化。它是个离线算法,所以局限性很大,很不常用。
思路:
在深度优先遍历的任意时刻,树中的节点分为
- 已经访问且回溯的节点。这些节点标记为
; - 已经访问过但还没回溯的节点,此时这些节点就是正在访问的节点
或 的祖先。这些节点标记为 ; - 尚未访问的节点。这些节点标记为
。
这样,对于正在访问的节点
若
可以用并查集优化这个操作,当一个节点被标记为
所以查询
在
时间复杂度为
#include <cmath>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 500010;
typedef pair<int, int> PII;
int n, m, root;
int h[N], e[N << 1], w[N << 1], ne[N << 1], idx;
int dist[N];
int ans[N];
vector<PII> que[N];
int st[N];
int p[N];
int find(int x) {
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void add_query(int a, int b, int id) {
que[a].push_back({b, id});
que[b].push_back({a, id});
//注意两边都要 push,因为可能在更新其中之一时另一个点未被标记成 2,导致未计算答案
}
void tarjan(int u) {
st[u] = 1;
for(int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if(st[j]) continue;
tarjan(j);
p[j] = u;
}
for(int i = 0; i < que[u].size(); i++) {
int j = que[u][i].first, id = que[u][i].second;
if(st[j] == 2) ans[id] = find(j);
}
++st[u];
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d%d", &n, &m, &root);
int a, b;
for(int i = 1; i < n; i++) {
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
for(int i = 1; i <= m; i++) {
scanf("%d%d", &a, &b);
if(a != b) add_query(a, b, i);
else ans[i] = a;
}
for(int i = 1; i <= n; i++) p[i] = i;
tarjan(root);
for(int i = 1; i <= m; i++) printf("%d\n", ans[i]);
return 0;
}
一些例题
一. 利用树的性质求 LCA 维护信息
题目大意:
给定一棵树,
思路:
在树上,两点之间的路径唯一,即:
再加上距离具有结合律,所以我们可以在求 LCA 时顺便处理出根节点到所有节点的距离。
这样对于每个询问
再加上一些小细节即可。
#include <cmath>
#include <queue>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010;
int n, m, T;
int h[N], e[N << 1], ne[N << 1], idx;
int f[N][25], dep[N];
int v[N];
int dist[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void bfs(int s) {
queue<int> q;
q.push(s);
dep[s] = 1, dist[s] = v[s];
while(q.size()) {
int t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(dep[j]) continue;
dep[j] = dep[t] + 1;
f[j][0] = t;
dist[j] = v[j] + dist[t];
for(int k = 1; k <= T; k++) f[j][k] = f[f[j][k - 1]][k - 1];
q.push(j);
}
}
}
int lca(int x, int y) {
if(dep[x] > dep[y]) swap(x, y);
for(int i = T; i >= 0; i--)
if(dep[f[y][i]] >= dep[x]) y = f[y][i];
if(x == y) return x;
for(int i = T; i >= 0; i--)
if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
return f[x][0];
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
T = (int)log2(n);
int a, b;
for(int i = 1; i < n; i++) {
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
++v[a], ++v[b];
}
bfs(1);
while(m--) {
scanf("%d%d", &a, &b);
int p = lca(a, b);
printf("%d\n", dist[a] + dist[b] - 2 * dist[p] + v[p]);
}
return 0;
}
P5836 [USACO19DEC] Milk Visits S
题目大意:
给定一棵树,树上每一个节点都有一个类型为
思路:
考虑到倍增 LCA 能预处理出类似于前缀和的数据,维护具有结合率的信息,所以提前处理出根节点到所有点的路径上两种物品的数目各是多少,然后用类似求距离的方法维护。
题目大意:
给定一棵树,
思路:
注意到
注意:为防止对负数取模,在取模之前要加上模数!
二. 树上差分
题目大意:
给定一棵树,
思路:
考虑暴力,对于每个操作
其实这种操作很像 DS 中的区间加操作,又因为这是个静态问题,所以可以树上差分。
树上差分类似于序列上的差分,想象一下把
如图所示:
好丑
这样操作之后,每个节点的子树的大小就是该点的被覆盖次数。
#include <cmath>
#include <queue>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 50010;
int n, m, T;
int h[N], e[N << 1], ne[N << 1], idx;
int dep[N];
int f[N][22];
int siz[N], v[N];
int ans;
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void bfs(int s) {
queue<int> q;
q.push(s);
dep[s] = 1;
while(q.size()) {
int t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(dep[j]) continue;
dep[j] = dep[t] + 1;
f[j][0] = t;
for(int k = 1; k <= T; k++) f[j][k] = f[f[j][k - 1]][k - 1];
q.push(j);
}
}
}
int LCA(int x, int y) {
if(dep[x] > dep[y]) swap(x, y);
for(int i = T; i >= 0; i--)
if(dep[f[y][i]] >= dep[x]) y = f[y][i];
if(x == y) return x;
for(int i = T; i >= 0; i--)
if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
return f[x][0];
}
int dfs(int u, int fa) {
siz[u] = v[u];
for(int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if(j == fa) continue;
siz[u] += dfs(j, u);
}
return siz[u];
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
T = (int)log2(n);
int a, b;
for(int i = 1; i < n; i++) {
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
bfs(1);
while(m--) {
scanf("%d%d", &a, &b);
int lca = LCA(a, b);
++v[a], ++v[b], --v[lca], --v[f[lca][0]];
}
dfs(1, -1);
for(int i = 1; i <= n; i++) ans = max(ans, siz[i]);
printf("%d\n", ans);
return 0;
}
题目大意:
给定一棵树,要求按顺序走完给定的所有点,每移动一步就要给这次移动经过的点增加
和上一道题十分相似,只需注意最后一个点的
P6869 [COCI2019-2020#5] Putovanje
题目大意:
求按节点编号顺序遍历一棵树的最小费用,边权分成单程票和多程票两种。
思路:
把每条边算作附属于它下面的点(深度更大的点),然后用树上差分求出每条边的经过次数,比较单程票和多程票费用。
只需注意处理每条边在附属过后在原来费用数组中的位置即可。
三. 树上问题分类讨论
很有意思的一道分讨题。
题目大意:
给定一棵树,
先将两条路径拆开,得到
不难看出,这两条路径相交当且仅当这
(1) ① 与 ③ 相交
如图:
(2) ① 与 ④ 相交
如图:
(3) ② 与 ③ 相交
如图:
(4) ② 与 ④ 相交
如图:
最后综合一下就能写出
inline bool check(int a, int b, int c, int d) {
int x = lca(a, b), y = lca(c, d), p1 = lca(a, c), p2 = lca(a, d), p3 = lca(b, c), p4 = lca(b, d);
if(lca(p1, d) == y && lca(p1, b) == x) return true;
if(lca(p2, c) == y && lca(p2, b) == x) return true;
if(lca(p3, d) == y && lca(p3, a) == x) return true;
if(lca(p4, c) == y && lca(p4, a) == x) return true;
return false;
}
题目大意:
给定一棵树,
首先思考什么点是距离和最小的点。
不难发现,如果随便选一个点,那么有些边可能要重复走几遍,而如果选择三个点互相通达的简单路径上的一个点,那么就没有边被重复走过。
直接讲有点抽象,如图:
若选择
若选择
而
多画几个图,总结出:选择三个点 LCA 中深度最大的那个点是最优的。
此时最小距离为:
四. LCA 综合运用
题目大意:
给定一张无向图,
根据贪心思想,我们肯定优先选择边权大的边走,这启示我们可以先求一遍原无向图的最大生成树,去掉永远也不会走过的边。
利用
否则就转化成了树上问题,等价于求两点之间路径中的边权最小值,默写模板即可。
#include <queue>
#include <cmath>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 25, E = 50010;
typedef long long ll;
typedef pair<int, int> PII;
int n, m, q, T;
int h[N], e[N << 1], ne[N << 1], w[N << 1], idx;
int f[N][M], dep[N];
int mind[N][M];
struct node{
int a, b, w;
bool operator < (const node &o) const {
return w > o.w;
}
}edges[M];
int p[N];
int cnt;
vector<int> uni[N];
int v[N];
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int find(int x) {
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void kruskal() {
for(int i = 1; i <= n; i++) p[i] = i;
sort(edges + 1, edges + m + 1);
for(int i = 1; i <= m; i++) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
int x = find(a), y = find(b);
if(x != y) {
p[x] = y;
add(a, b, w), add(b, a, w);
}
}
}
void dfs(int u) {
v[u] = cnt;
uni[cnt].push_back(u);
for(int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if(v[j]) continue;
dfs(j);
}
}
void bfs(int s) {
queue<int> q;
q.push(s);
for(int i = 1; i <= n; i++) {
if(dep[i]) continue;
for(int j = 0; j <= T; j++)
mind[i][j] = 0x3f3f3f3f;
}
dep[s] = 1;
while(q.size()) {
int t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(dep[j]) continue;
dep[j] = dep[t] + 1;
f[j][0] = t;
mind[j][0] = w[i];
// printf("------%d\n", mind[j][0]);
for(int k = 1; k <= T; k++) {
f[j][k] = f[f[j][k - 1]][k - 1];
mind[j][k] = min(mind[j][k - 1], mind[f[j][k - 1]][k - 1]);
}
q.push(j);
}
}
}
int lca(int x, int y) {
int res = 0x3f3f3f3f;
if(dep[x] > dep[y]) swap(x, y);
for(int i = T; i >= 0; i--)
if(dep[f[y][i]] >= dep[x]) {
res = min(res, mind[y][i]);
y = f[y][i];
}
if(x == y) return res;
for(int i = T; i >= 0; i--)
if(f[x][i] != f[y][i]) {
res = min(res, min(mind[x][i], mind[y][i]));
x = f[x][i], y = f[y][i];
}
res = min(res, min(mind[x][0], mind[y][0]));
return res;
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
T = (int)log2(n);
int a, b, c;
for(int i = 1; i <= m; i++) {
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
kruskal();
for(int i = 1; i <= n; i++)
if(!v[i]) {
cnt++;
dfs(i);
}
for(int i = 1; i <= cnt; i++) bfs(uni[i][0]);
scanf("%d", &q);
while(q--) {
scanf("%d%d", &a, &b);
if(v[a] != v[b]) puts("-1");
else {
printf("%d\n", lca(a, b));
}
}
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 让容器管理更轻松!