【算法学习笔记】04 最近公共祖先LCA

【算法学习笔记】04 最近公共祖先LCA

原理

顾名思义,就是求两点的最近公共祖先(自己也是自己的祖先)。
也就是两点在走到根节点的路径上最先遇到的共同的点。

向上标记法

比较贴定义的原始方法。
一点先向 root 走,走过的点标记一下;然后另一点也往 root 走,走到的第一个被标记的点为二者的 LCA。时间复杂度为 O(n)

倍增法

1. 首先需要记录两个变量

1. f(i,j)

表示从 i 开始,向上走 2j 步能到达的节点,0jlogn
那么怎么更新?

  1. j=0, f(i,j)i 的父节点
  2. j>0, f(i,j)=f(f(i,j1),j1),相当于跳了两个 2j1
    这一过程可用 bfs/dfs
2. depth(i)

表示 i 的深度

哨兵

depth(0)=0
如果从 i 开始跳 2j 步会跳过根节点,那么 f(i,j)=0

2. 将两点跳到同一层

具体操作如下:
相当于用我们之前处理好的二进制数来拼凑 dx=depth(x)depth(y)(保证 x 在上面),即从后往前找到第一个满足 2kdxk, 然后令 dx=2k, 重复这个找的过程,直至 dx=0
的过程不用算具体值,只需判断是否满足 depth(f(x,k))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)

回溯完之后,合并进父结点

xy 的最短距离:
预处理每个点到根节点的距离,可以发现 dx+dy2dp 即为所求

基于 RMQ

遍历一遍,记录 DFS序列,求x,yLCA 就等价于在这个 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 上的最大值为 mx1, 严格次大值mx2,(mx2<mx1),
w>mx1, 则 dx=min(dx,wmx1);
w=mx1, 则 dx=min(dx,wmx2)

替换后增量最小的边为答案

就相当于是在最小生成树上面加一个多余的边,然后把这条边和原 MST 中的最大边与次大边作比较(两种替换的可能性),则次小生成树权值之和可能为:

  1. MST + 多余边 - 最大权值边
  2. MST + 多余边 - 次大权值边

如何快速计算路径上的最大边和次大边

预处理:
f(i,j): 从 i 开始,向上走 2j 步能到达的节点
d1(i,j): 从 i 开始,向上走 2j 步的路径上的最小边权
d2(i,j): 从 i 开始,向上走 2j 步的路径上的次小边权

然后,

  1. 最大值就是全局最大值
  2. 次大值可在圈起来的点中选:

    (竖线为路径上的每一条边,边上打点的依次表示为该路径上的最大值、次大值,那么全局次大值就在图示范围中找)

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。树边上的权值就表示砍完这个树边后还需要看多少条非树边

树上差分:dx+=c,dy+=c,dp=2c

#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;
}
posted @   Sakana~  阅读(48)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示