浅谈倍增法求解LCA

博客 动态
0 236
羽尘
羽尘 2022-06-09 22:00:19
悬赏:0 积分 收藏

浅谈倍增法求解LCA

Luogu P3379 最近公共祖先

原题展现

题目描述

如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

输入格式

第一行包含三个正整数 \(N,M,S\),分别表示树的结点个数、询问的个数和树根结点的序号。

接下来 \(N-1\) 行每行包含两个正整数 \(x, y\),表示 \(x\) 结点和 \(y\) 结点之间有一条直接连接的边(数据保证可以构成树)。

接下来 \(M\) 行每行包含两个正整数 \(a, b\),表示询问 \(a\) 结点和 \(b\) 结点的最近公共祖先。

输出格式

输出包含 \(M\) 行,每行包含一个正整数,依次为每一个询问的结果。

样例输入 #1

5 5 43 12 45 11 42 43 23 51 24 5

样例输出 #1

44144

提示

对于 \(30\%\) 的数据,\(N\leq 10\)\(M\leq 10\)

对于 \(70\%\) 的数据,\(N\leq 10000\)\(M\leq 10000\)

对于 \(100\%\) 的数据,\(N\leq 500000\)\(M\leq 500000\)

样例说明:

该树结构如下:

第一次询问:\(2, 4\) 的最近公共祖先,故为 \(4\)

第二次询问:\(3, 2\) 的最近公共祖先,故为 \(4\)

第三次询问:\(3, 5\) 的最近公共祖先,故为 \(1\)

第四次询问:\(1, 2\) 的最近公共祖先,故为 \(4\)

第五次询问:\(4, 5\) 的最近公共祖先,故为 \(4\)

故输出依次为 \(4, 4, 1, 4, 4\)

解析

本题是 LCA 的模板

LCA 的做法很多,比如暴力跳,倍增

暴力跳

让深度大的一点不断向上跳,直到两点深度相等

如果两点深度相同但是并不相等,可以两点一起跳

在随机数据下表现优异,因为树会比较平衡,所以近似\(O(\log n)\)

通常会被卡成单次\(O(n)\),其实不难构造,可以构造一个深度大的树(比如链)

本人出的一道题思想类似这样,不过这道题保证了平衡

倍增法

考虑一次跳多一点

\(fa_{u,k}\)表示距离\(u\)的边数为\(2^k\)的祖先节点则\(fa_{u,k}=fa_{fa_{u,k-1},k-1}\)可以通过dfs求出\(fa\)

如果求LCA,我们可以很快让两点来到相同的深度

考虑求两点深度差,将差二进制拆分,每次跳一个\(2\)的幂,时间复杂度\(O(\log n)\)

当然,没必要真的二进制拆分,因为我们要知道是\(2\)的几次幂,所以用cmathlog2更加方便

这里有一个优化:用\(O(n)\)的时间复杂度递推求出log2的值

然后,如果两点深度相同不相等,有一个自认为巧妙的方法求解

一个性质:如果两点跳到LCA了,继续向上跳依然相等(易证)

如果两点向上跳不相等,那么一定可以继续跳

于是想到一个办法:尝试枚举\(i\)\(31\)\(0\),表示尝试跳\(2^i\)

如果向上跳不相同的话,就向上跳,这样,枚举完,LCA就是\(fa_{x,0}\)

核心代码如下,首先是预处理

void dfs(long long x,long long fa){    f[x][0]=fa;    dep[x]=dep[fa]+1;    for(int i=1;i<=31;i++)    {        f[x][i]=f[f[x][i-1]][i-1];    }    for(int i=h[x];i;i=a[i].next)    {        if(a[i].to!=fa)        {            dfs(a[i].to,x);        }    }}

然后是求解

if(dep[x]<dep[y]){    swap(x,y);}   while(dep[x] > dep[y]){    x = f[x][lg[dep[x]-dep[y]] - 1];}if(x==y){    cout<<x<<endl;    continue;}for(int k = lg[dep[x]] - 1; k >= 0;k--) {    if(f[x][k] != f[y][k])     {        x = f[x][k], y = f[y][k];    }}

于是,我们得到了一个严格的\(O(\log n)\)算法

Luogu P1967 [NOIP2013 提高组] 货车运输

原题展现

题目描述

A 国有 \(n\) 座城市,编号从 \(1\)\(n\),城市之间有 \(m\) 条双向道路。每一条道路对车辆都有重量限制,简称限重。

现在有 \(q\) 辆货车在运输货物, 司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。

输入格式

第一行有两个用一个空格隔开的整数 $ n,m$,表示 \(A\) 国有 $ n$ 座城市和 \(m\) 条道路。

接下来 \(m\) 行每行三个整数 \(x, y, z\),每两个整数之间用一个空格隔开,表示从 $x $ 号城市到 $ y $ 号城市有一条限重为 \(z\) 的道路。
注意: \(x \neq y\),两座城市之间可能有多条道路 。

接下来一行有一个整数 \(q\),表示有 \(q\) 辆货车需要运货。

接下来 \(q\) 行,每行两个整数 \(x,y\),之间用一个空格隔开,表示一辆货车需要从 \(x\) 城市运输货物到 \(y\) 城市,保证 \(x \neq y\)

输出格式

共有 \(q\) 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。
如果货车不能到达目的地,输出 \(-1\)

样例输入 #1

4 31 2 42 3 33 1 131 31 41 3

样例输出 #1

3-13

提示

对于 \(30\%\) 的数据,\(1 \le n < 1000\)\(1 \le m < 10,000\)\(1\le q< 1000\)

对于 \(60\%\) 的数据,\(1 \le n < 1000\)\(1 \le m < 5\times 10^4\)\(1 \le q< 1000\)

对于 \(100\%\) 的数据,\(1 \le n < 10^4\)\(1 \le m < 5\times 10^4\),$1 \le q< 3\times 10^4 $,\(0 \le z \le 10^5\)

解析

因为我们想要经过的最小边最大,那么不妨构造一个最大生成树(建议使用克鲁斯卡尔算 法),这样每条边都能尽可能大

然后问题转换为树上查询,同样利用倍增法求\(x->LCA,y->LCA\)路径中的最小边,也是可以预处理的

不过问题不保证树联通,需要判断是否有解

克鲁斯卡尔的优势就体现出来了,我们已经处理了并查集,如果两点祖先不同就直接判断为无解

核心代码如下(码风十分奇怪)

#include<bits/stdc++.h>#define ll long longusing namespace std;struct road{    ll s,t,w;}r[200005];struct node{    ll to,next,w;}a[200005];ll n,m,t,k,x,y,fa2[100005],h[100005],fa[100005][33],f[100005][33],dep[100005],lg[100005];bool cmp(road x,road y){    return x.w>y.w;}void add(int x,int y,int z){    t++;    a[t].to=y;    a[t].w=z;    a[t].next=h[x];    h[x]=t;}int find(int x){    if(fa2[x]==x)return x;    return fa2[x]=find(fa2[x]);}void dfs(long long x,long long fn){    fa[x][0]=fn;    dep[x]=dep[fn]+1;     for(int i=1;i<=31;i++)    {        fa[x][i]=fa[fa[x][i-1]][i-1];        f[x][i]=min(f[x][i-1],f[fa[x][i-1]][i-1]);//f数组表示x到fa[x][i]路径的最小值    }    for(int i=h[x];i;i=a[i].next)    {        if(a[i].to!=fn)        {               f[a[i].to][0]=a[i].w;            dfs(a[i].to,x);        }    }}int lca(int x,int y){    if(dep[x]<dep[y])    {        swap(x,y);    }       while(dep[x] > dep[y])    {        x = fa[x][lg[dep[x]-dep[y]] - 1];    }    if(x==y)    {        return x;    }    for(int k = lg[dep[x]] - 1; k >= 0;k--)     {        if(fa[x][k] != fa[y][k])         {            x = fa[x][k], y = fa[y][k];        }    }        return fa[x][0];}int work(int x,int y)//求解x到y路径的最小值,保证y是x祖先{    ll ans=1e9,deph=dep[x]-dep[y];    while(deph!=0)    {        ll t=lg[deph]-1;        ans=min(ans,f[x][t]);        x=fa[x][t];        deph=dep[x]-dep[y];    }    return ans;}int main(){    cin>>n>>m;    for(int i=1;i<=n;i++)    {        fa2[i]=i;        lg[i] = lg[i-1] + (1 << lg[i-1] == i);    }    for(int i=1;i<=m;i++)    {        cin>>r[i].s>>r[i].t>>r[i].w;    }    sort(r+1,r+m+1,cmp);//克鲁斯卡尔    int k=n-1;    for(int i=1;i<=m;i++)    {        if(k==0)break;        if(find(r[i].s)!=find(r[i].t))        {            add(r[i].s,r[i].t,r[i].w);            add(r[i].t,r[i].s,r[i].w);            fa2[find(r[i].s)]=find(r[i].t);            k--;        }    }    for(int i=1;i<=n;i++)    {        if(find(i)==i)        {            dfs(i,0);        }    }    cin>>k;    for(int i=1;i<=k;i++)    {        cin>>x>>y;        if(find(x)!=find(y))        {            cout<<-1<<endl;            continue;        }        int lcah=lca(x,y);        cout<<min(work(x,lcah),work(y,lcah))<<endl;    }}

Duck006[DuckOI]Kill the Duck

原题展现

温馨提示

Duck非常不要脸,单推自己的题

后来发现其实有好多一样的题

  • 贪玩的小孩
  • HDU 2586 How far away?

题目描述

XCR是世界名列前茅的OIer,今天在打模拟赛。

他已经AC了前四道题,准备暴切第五题,看着这个题面,突然发现不太对....

他一看五道题的名字

\[\mathtt{\color{red}{X}\color{black}{or}}\\\mathtt{\color{red}{C}\color{black}{ount\;the\;Number\;of\;Dance\;Schemes}}\\\mathtt{\color{red}{R}\color{black}{elaxing\;Time }}\\\mathtt{\color{red}{A}\color{black}{n\; Easy\;Problem}}\\\mathtt{\color{red}{K}\color{black}{ill\;the\;Duck}}\\\mathtt{\huge{\color{red}{XCRAK}}}\]

XCR十分生气,想要杀了DengDuck

DengDuck跑到了一个有\(n\)个结点,\(n-1\)条边的树上

这个树的每个边都是无向的,都有边权

XCR现在有\(m\)次询问,第\(i(1 \leq i \leq m)\)次给出两个正整数\(x_i\)\(y_i\),含义如下

DengDuck 在点 \(x_i(1 \leq x_i \leq n)\) 上,XCR在点 \(y_i(1 \leq y_i \leq n)\)

对于每次询问,请问XCR离DengDuck的距离是多少?

输入格式

第一行一个整数\(n\)

接下来\(n-1\)行每行三个正整数分别表示一条边的起点,终点,边权

\(n+1\)行一个正整数\(m\)

接下来\(m\)行每行两个正整数\(x_i\)\(y_i\)

输出格式

\(m\)行,每行一个正整数,表示DengDuck和XCR的距离

样例输入 #1

31 2 32 3 421 21 3

样例输出 #1

37

样例输入 #2

31 3 101 2 1351 12 23 12 11 3

样例输出 #2

00101310

样例输入 #3

145 7 127 11 155 14 1214 3 177 1 1914 4 141 12 161 6 1612 9 199 10 107 2 114 8 102 13 14176 1114 1413 116 1012 68 79 910 1113 101 42 1213 42 72 112 210 114 7

样例输出 #3

50040613248079895746631130467938

提示

对于一定的数据\(n,m\)的范围特殊限制
\(5\%\)的数据\(1~20\)
\(20\%\)的数据\(1~3000\)
另外的\(5\%\)的数据\(1~3000\)\(m=1\)
所有数据\(1~100000\)

解析

预处理出\(dis_i\)表示点\(i\)到根\(1\)的距离,答案是\(dis_x+dis_y-2dis_{lca(x,y)}\)

非常容易证明

代码如下

#include <bits/stdc++.h>using namespace std;int n, k, b[1000005], x, y, z, tot, h[500005], len[500005], fa[500005][33], dep[500005], lg[500005],    f[1000005], ans;struct node {    int to, next, w;} a[1000005];void dfs(long long x, long long fn, long long l) {    fa[x][0] = fn;    dep[x] = dep[fn] + 1;    len[x] = l;    for (int i = 1; i <= 31; i++) {        fa[x][i] = fa[fa[x][i - 1]][i - 1];    }    for (int i = h[x]; i; i = a[i].next) {        if (a[i].to != fn) {            dfs(a[i].to, x, l + a[i].w);        }    }}int lca(int x, int y) {    if (dep[x] < dep[y]) {        swap(x, y);    }    while (dep[x] > dep[y]) {        x = fa[x][lg[dep[x] - dep[y]] - 1];    }    if (x == y) {        return x;    }    for (int k = lg[dep[x]] - 1; k >= 0; k--) {        if (fa[x][k] != fa[y][k]) {            x = fa[x][k], y = fa[y][k];        }    }    return fa[x][0];}void add(int x, int y, int z) {    ++tot;    a[tot].to = y;    a[tot].next = h[x];    a[tot].w = z;    h[x] = tot;}void answer(int x, int fn) {    for (int i = h[x]; i; i = a[i].next) {        if (a[i].to != fn) {            answer(a[i].to, x);            f[x] += f[a[i].to];        }    }    ans = max(ans, f[x]);}int main() {    cin >> n;    for (int i = 1; i <= n; ++i) {        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);    }    for (int i = 1; i <= n - 1; i++) {        cin >> x >> y >> z;        add(x, y, z);        add(y, x, z);    }    dfs(1, 0, 0);    cin >> k;    for (int i = 1; i <= k; i++) {        cin >> x >> y;        int t = lca(x, y);        cout << len[x] + len[y] - 2 * len[t] << endl;    }}
posted @ 2022-06-09 21:05 DengDuck 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    羽尘

    羽尘 (王者 段位)

    2335 积分 (2)粉丝 (11)源码

     

    温馨提示

    亦奇源码

    最新会员