【算法学习笔记】04 最近公共祖先LCA
【算法学习笔记】04 最近公共祖先LCA
原理
顾名思义,就是求两点的最近公共祖先(自己也是自己的祖先)。
也就是两点在走到根节点的路径上最先遇到的共同的点。
向上标记法
比较贴定义的原始方法。
一点先向 \(root\) 走,走过的点标记一下;然后另一点也往 \(root\) 走,走到的第一个被标记的点为二者的 \(LCA\)。时间复杂度为 \(O(n)\)
倍增法
1. 首先需要记录两个变量
1. \(f(i, j)\)
表示从 \(i\) 开始,向上走 \(2^j\) 步能到达的节点,\(0\leq j\leq logn\)
那么怎么更新?
- \(j=0\), \(f(i,j)\) 为 \(i\) 的父节点
- \(j>0\), \(f(i,j)=f(f(i,j-1),j-1)\),相当于跳了两个 \(2^{j-1}\)
这一过程可用 \(bfs/dfs\) 求
2. \(depth(i)\)
表示 \(i\) 的深度
哨兵
\(depth(0)=0\)
如果从 \(i\) 开始跳 \(2^j\) 步会跳过根节点,那么 \(f(i,j)=0\)。
2. 将两点跳到同一层
具体操作如下:
相当于用我们之前处理好的二进制数来拼凑 \(dx = depth(x)-depth(y)\)(保证 \(x\) 在上面),即从后往前找到第一个满足 \(2^k \leq dx\) 的 \(k\), 然后令 \(dx -= 2^k\), 重复这个找的过程,直至 \(dx=0\)。
找的过程不用算具体值,只需判断是否满足 \(depth(f(x,k))\geq depth(y)\)即可。
3. 让两个点一直往上跳,直到跳到 \(LCA\) 的下一层(儿子那层)
为什么要到下一层?
因为到本层(\(LCA\))时无法判断是否为 \(LCA\),为避免歧义才到下一层。
跳的过程也是二进制拼凑,从大到小枚举 \(k\), 直至 \(f(a,k)=f(b,k)\), 此时到了点\(x, y\),在往上一层,也就是他们的父亲就是 \(LCA\),即 \(LCA=f(x,0)=f(y,0)\)。
\(Tarjan\) —— 离线求 \(LCA\)
优化向上标记法
\(dfs\) 时,把点分为三大类:
0. 还没搜索的
1. 正在搜索的
2. 已经完全搜完的(子树也搜完了)
如图,可以发现已经搜过的点的祖先是固定的,那么可以把他合并到祖先上
用并查集,复杂度为 \(O(n+m)\)
回溯完之后,合并进父结点
求 \(x\) 到 \(y\) 的最短距离:
预处理每个点到根节点的距离,可以发现 \(d_x+d_y-2d_p\) 即为所求
基于 \(RMQ\)
遍历一遍,记录 \(DFS\)序列,求\(x,y\) 的 \(LCA\) 就等价于在这个 \(DFS\) 序列上求区间 \([x,y]\) 的 深度最小的点。转化为区间最值问题,可用 \(RMQ\) 或者 线段树求
板子
倍增法
void bfs (int root) {
memset (depth, 0x3f, sizeof depth); //记得
queue <int> q;
q.push (root);
depth[root] = 1, depth[0] = 0;
while (!q.empty()) {
int t = q.front();
q.pop ();
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.push (j);
f[j][0] = t;
for (int k = 1; k <= 15; k++) {
f[j][k] = f[f[j][k-1]][k-1];
}
}
}
}
}
int lca (int a, int b) {
if (depth[a] < depth[b]) swap (a, b); //确保a往上跳
for (int k = 15; k >= 0; k--) {
if (depth[f[a][k]] >= depth[b]) { //注意是>=
a = f[a][k];
}
}
if (a == b) return a;
for (int k = 15; k >= 0; k--) {
if (f[a][k] != f[b][k]) {
a = f[a][k], b = f[b][k];
}
}
return f[a][0];
}
\(Tarjan\) 离线做法
void dfs (int v, int u) { //1, -1
for (int i = h[v]; ~i; i = ne[i]) {
int j = e[i];
if (j == u) continue;
d[j] = d[v] + w[i];
dfs (j, v);
}
}
int find (int x) {
if (x != fa[x])
fa[x] = find (fa[x]);
return fa[x];
}
void tarjan (int u) { //1
vis[u] = 1; //正在搜
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (vis[j]) continue;
tarjan(j);
fa[j] = u; //合并
}
for (auto i : q[u]) {
int v = i.first, id = i.second;
if (vis[v] == 2) {
int p = find(v);
ans[id] = d[u] + d[v] - 2*d[p];
}
}
vis[u] = 2; //搜过了
}
例题
前面两个给过板子的题就不放了
严格次小生成树
https://www.luogu.com.cn/problem/P4180
好耶,是紫题
定理
对于一个无向图,如果存在最小生成树和(严格)次小生成树,那么对于任何一颗最小生成树,都存在一颗(严格)次小生成树,使得这两棵树只有一条边不同
原理
在最小生成树的基础上加一条非树边,记为 \((u, v, w)\), 能提供 \(dx\) 的增量。
设路径 \(u->v\) 上的最大值为 \(mx_1\), 严格次大值为 \(mx_2\),(\(mx_2 < mx_1\)),
若 \(w>mx_1\), 则 \(dx = min (dx, w-mx_1)\);
若 \(w=mx_1\), 则 \(dx = min (dx, w-mx_2)\)。
即替换后增量最小的边为答案
就相当于是在最小生成树上面加一个多余的边,然后把这条边和原 \(MST\) 中的最大边与次大边作比较(两种替换的可能性),则次小生成树权值之和可能为:
- \(MST\) + 多余边 - 最大权值边
- \(MST\) + 多余边 - 次大权值边
如何快速计算路径上的最大边和次大边
预处理:
\(f(i,j)\): 从 \(i\) 开始,向上走 \(2^j\) 步能到达的节点
\(d1(i,j)\): 从 \(i\) 开始,向上走 \(2^j\) 步的路径上的最小边权
\(d2(i,j)\): 从 \(i\) 开始,向上走 \(2^j\) 步的路径上的次小边权
然后,
- 最大值就是全局最大值
- 次大值可在圈起来的点中选:
(竖线为路径上的每一条边,边上打点的依次表示为该路径上的最大值、次大值,那么全局次大值就在图示范围中找)
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5, M = 3e5 + 5, inf = 0x3f3f3f3f;
int fa[N], dist[N*2], n, m;
int h[N], e[M], ne[M], w[M], idx;
int depth[N], f[N][17], d1[N][17], d2[N][17]; //最大,次大
struct Node {
int a, b, w;
bool used;
bool operator<(const Node &t) const {
return w < t.w;
}
}E[M];
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 (x != fa[x])
fa[x] = find (fa[x]);
return fa[x];
}
int kruscal () {
for (int i = 1; i <= n; i++) fa[i] = i;
sort (E, E + m);
int ans = 0;
for (int i = 0; i < m; i++) {
int a = E[i].a, b = E[i].b, w = E[i].w;
int pa = find (a), pb = find (b);
if (pa != pb) {
fa[pa] = pb;
ans += w;
E[i].used = true; //标记为树边
}
}
return ans;
}
void build () {
memset (h, -1, sizeof h);
for (int i = 0; i < m; i++) {
if (!E[i].used) continue;
int a = E[i].a, b = E[i].b, w = E[i].w;
add (a, b, w), add (b, a, w);
}
//build MST
}
void bfs () {
memset (depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
queue <int> q;
q.push (1);
while (!q.empty()) {
int t = q.front();
q.pop();
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.push (j);
f[j][0] = t;
d1[j][0] = w[i], d2[j][0] = -inf;
for (int k = 1; k <= 16; k++) {
int anc = f[j][k-1];
f[j][k] = f[anc][k-1];
//j跳到anc,anc接着跳
int dis[] = {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 = dis[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; //严格次大值
}
}
}
}
}
//LCA预处理
}
int lca (int a, int b, int w) {
int cnt = 0;
if (depth[a] < depth[b]) swap (a, b);
for (int k = 16; k >= 0; k--) {
if (depth[f[a][k]] >= depth[b]) {
dist[cnt++] = d1[a][k];
dist[cnt++] = d2[a][k];
a = f[a][k]; //jmp
}
}
if (a != b) {
for (int k = 16; k >= 0; k--) {
if (f[a][k] == f[b][k]) continue;
dist[cnt++] = d1[a][k];
dist[cnt++] = d2[a][k];
dist[cnt++] = d1[b][k];
dist[cnt++] = d2[b][k];
a = f[a][k], b = f[b][k];
}
dist[cnt++] = d1[a][0];
dist[cnt++] = d1[b][0];
}
int dist1 = -inf, dist2 = -inf;
for (int i = 0; i < cnt; i++) {
int d = dist[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;
}
signed main () {
ios::sync_with_stdio (0);cin.tie(0);
cin >> n >> m;
for (int i = 0; i < m; i++) {
int a, b, c;
cin >> a >> b >> c;
E[i] = {a, b, c};
}
int sum = kruscal();
build(), bfs();
int ans = 1e18;
for (int i = 0; i < m; i++) {
if (E[i].used) continue;
int a = E[i].a, b = E[i].b, w = E[i].w;
ans = min (ans, sum + lca (a, b, w));
}
cout << ans;
}
彩蛋:
对于我这种菜鸟来说,这码量还是蛮大的
闇の連鎖
https://www.acwing.com/problem/content/description/354/
题意:砍两刀,第一次斩树边,第二次斩非树边。砍成两半
如图,对于环上的每一条树边来说,如果想要使得它砍完之后,图不连通,那么还要砍掉这个环上的非树边
枚举所有非树边,然后把环上经过的树边+1。树边上的权值就表示砍完这个树边后还需要看多少条非树边
树上差分:\(d_x+=c, d_y+=c, d_p-=2*c\)
#include <bits/stdc++.h>
#define endl "\n"
using namespace std;
const int N = 100000 + 5, M = N*2;
int h[N], e[M], ne[M], idx;
int depth[N], f[N][17];
int n, m;
int d[N], ans;
void add (int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void bfs (int root) {
memset (depth, 0x3f, sizeof depth);
queue <int> q;
q.push (root);
depth[root] = 1, depth[0] = 0;
while (!q.empty()) {
int t = q.front();
q.pop ();
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.push (j);
f[j][0] = t;
for (int k = 1; k <= 16; k++) {
f[j][k] = f[f[j][k-1]][k-1];
}
}
}
}
}
int lca (int a, int b) {
if (depth[a] < depth[b]) swap (a, b); //确保a往上跳
for (int k = 16; k >= 0; k--) {
if (depth[f[a][k]] >= depth[b]) { //注意是>=
a = f[a][k];
}
}
if (a == b) return a;
for (int k = 16; k >= 0; k--) {
if (f[a][k] != f[b][k]) {
a = f[a][k], b = f[b][k];
}
}
return f[a][0];
}
int dfs (int u, int fa) {
int res = d[u];
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j != fa) {
int s = dfs (j, u);
if (!s) ans += m;
else if (s == 1) ans ++;
res += s;
}
}
return res;
}
int main () {
ios::sync_with_stdio (0);cin.tie(0);cout.tie(0);
memset (h, -1, sizeof h);
cin >> n >> m;
int root;
for (int i = 0; i < n-1; i++) {
int a, b;
cin >> a >> b;
add (a, b), add (b, a);
}
bfs (1); //预处理两个数组
for (int i = 0; i < m; i ++ ) {
int a, b;
cin >> a >> b;
int p = lca (a, b);
d[a] ++, d[b] ++, d[p] -= 2;
}
dfs (1, -1);
cout << ans;
}