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

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

原理

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

向上标记法

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

倍增法

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

1. \(f(i, j)\)

表示从 \(i\) 开始,向上走 \(2^j\) 步能到达的节点,\(0\leq j\leq logn\)
那么怎么更新?

  1. \(j=0\), \(f(i,j)\)\(i\) 的父节点
  2. \(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\) 中的最大边与次大边作比较(两种替换的可能性),则次小生成树权值之和可能为:

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

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

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

然后,

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

树上差分:\(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;
}
posted @ 2022-08-15 00:10  Sakana~  阅读(47)  评论(0编辑  收藏  举报