目录
1 介绍
本博客用来记录"对于有根图中,求最近公共祖先"的题目。
求解方法:
- 向上标记法。每次求两个结点的最近公共祖先的时间复杂度是
O(N)
。由于时间复杂度较高,通常不用。 - 倍增法。
- Tarjan法。
1.1 向上标记法
字面意思理解即可,存储每个结点的父结点。从结点a出发,往上走,一直走到根结点,标记走过的结点;然后从结点b出发,往上走,一直走到根节点,第一次遇到的标记过的结点就是这两个结点的最近公共祖先。
1.2 倍增法
倍增法重要思路:预处理出两个数组fa[i][j]
和depth[i]
。其中fa[i][j]
表示从i
开始,向上走2^j
步所能走到的结点。0<=j<=logn
。depth[i]
表示深度,为到根结点的距离再加上1。
哨兵:如果从i
开始跳2^j
步会跳过根结点,那么fa[i][j] = 0
,depth[0] = 0
。
倍增法重要步骤:
- 先将两个点跳到同一层。
- 让两个点同时往上跳,一直跳到它们的最近公共祖先的下一层。
倍增法的时间复杂度分析:预处理的时间复杂度为O(NlogN)
,查询的时间复杂度为O(logN)
。
1.3 Tarjan法
对结点进行分类,离线做法。
2 训练
题目1:1172祖孙询问
C++代码如下,
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
const int N = 40010;
int n, m;
int depth[N], fa[N][16];
int ancestor;
unordered_map<int, vector<int>> g;
void bfs(int root) {
memset(depth, 0x3f, sizeof depth);
depth[0] = 0;
depth[root] = 1;
queue<int> q;
q.push(root);
while (!q.empty()) {
int a = q.front();
q.pop();
for (auto b : g[a]) {
if (depth[b] > depth[a] + 1) {
depth[b] = depth[a] + 1;
q.push(b);
fa[b][0] = a;
for (int k = 1; k <= 15; ++k) {
fa[b][k] = fa[fa[b][k-1]][k-1];
}
}
}
}
return;
}
int lca(int a, int b) {
//倍增法
if (depth[a] < depth[b]) swap(a, b);
for (int k = 15; k >= 0; --k) {
if (depth[fa[a][k]] >= depth[b]) {
a = fa[a][k];
}
}
if (a == b) return a;
for (int k = 15; k >= 0; --k) {
if (fa[a][k] != fa[b][k]) {
a = fa[a][k];
b = fa[b][k];
}
}
return fa[a][0];
}
int main() {
cin >> n;
int a, b;
for (int i = 0; i < n; ++i) {
cin >> a >> b;
if (b == -1) {
ancestor = a;
} else {
g[a].emplace_back(b);
g[b].emplace_back(a);
}
}
cin >> m;
vector<pair<int,int>> queries;
for (int i = 0; i < m; ++i) {
cin >> a >> b;
queries.emplace_back(a,b);
}
//从根结点开始遍历
bfs(ancestor);
for (auto [a, b] : queries) {
int x = lca(a, b);
if (a == x) {
puts("1");
} else if (b == x) {
puts("2");
} else {
puts("0");
}
}
return 0;
}
题目2:1171距离
C++代码如下,
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 10010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int p[N];
int res[M];
int st[N];
vector<PII> query[N]; //first存b,second存i
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u, int fa) {
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == fa) continue;
dist[j] = dist[u] + w[i];
dfs(j, u);
}
}
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void tarjan(int u) {
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (!st[j]) {
tarjan(j);
p[j] = u;
}
}
for (auto item : query[u]) {
int y = item.first, id = item.second;
if (st[y] == 2) {
int anc = find(y);
res[id] = dist[u] + dist[y] - dist[anc] * 2;
}
}
st[u] = 2;
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; ++i) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < m; ++i) {
int a, b;
cin >> a >> b;
if (a != b) {
query[a].push_back({b,i});
query[b].push_back({a,i});
}
}
for (int i = 1; i <= n; ++i) p[i] = i;
dfs(1, -1);
tarjan(1);
for (int i = 0; i < m; ++i) printf("%d\n", res[i]);
return 0;
}
题目3:356次小生成树
C++代码如下,
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010, INF = 0x3f3f3f3f;
int n, m;
struct Edge {
int a, b, w;
bool used;
bool operator< (const Edge& t) const {
return w < t.w;
}
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
LL kruskal() {
for (int i = 1; i <= n; ++i) p[i] = i;
sort(edge, edge + m);
LL res = 0;
for (int i = 0; i < m; ++i) {
int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
if (a != b) {
p[a] = b;
res += w;
edge[i].used = true;
}
}
return res;
}
void build() {
memset(h, -1, sizeof h);
for (int i = 0; i < m; ++i) {
if (edge[i].used) {
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
add(a, b, w), add(b, a, w);
}
}
}
void bfs() {
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
q[0] = 1;
int hh = 0, tt = 0;
while (hh <= tt) {
int t = q[hh++];
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (depth[j] > depth[t] + 1) {
depth[j] = depth[t] + 1;
q[++tt] = j;
fa[j][0] = t;
d1[j][0] = w[i], d2[j][0] = -INF;
for (int k = 1; k <= 16; ++k) {
int anc = fa[j][k-1];
fa[j][k] = fa[anc][k-1];
int distance[4] = {d1[j][k-1], d2[j][k-1], d1[anc][k-1], d2[anc][k-1]};
d1[j][k] = d2[j][k] = -INF;
for (int u = 0; u < 4; ++u) {
int d = distance[u];
if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
int lca(int a, int b, int w) {
static int distance[N * 2];
int cnt = 0;
if (depth[a] < depth[b]) swap(a, b);
for (int k = 16; k >= 0; --k) {
if (depth[fa[a][k]] >= depth[b]) {
distance[cnt++] = d1[a][k];
distance[cnt++] = d2[a][k];
a = fa[a][k];
}
}
if (a != b) {
for (int k = 16; k >= 0; --k) {
if (fa[a][k] != fa[b][k]) {
distance[cnt++] = d1[a][k];
distance[cnt++] = d2[a][k];
distance[cnt++] = d1[b][k];
distance[cnt++] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
}
distance[cnt++] = d1[a][0];
distance[cnt++] = d1[b][0];
}
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; ++i) {
int d = distance[i];
if (d > dist1) dist2 = dist1, dist1 = d;
else if(d != dist1 && d > dist2) dist2 = d;
}
if (w > dist1) return w - dist1;
if (w > dist2) return w - dist2;
return INF;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++i) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
LL sum = kruskal();
build();
bfs();
LL res = 1e18;
for (int i = 0; i < m; ++i) {
if (!edge[i].used) {
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
res = min(res, sum + lca(a, b, w));
}
}
printf("%lld\n", res);
return 0;
}
题目4:352暗之连锁
C++代码如下,