• 求最近公共祖先(LCA)的各种算法


    水一发题解。

    我只是想存一下树剖LCA的代码......

    以洛谷上的这个模板为例:P3379 【模板】最近公共祖先(LCA)

    1.朴素LCA

    就像做模拟题一样,先dfs找到基本信息:每个节点的父亲、深度。

    把深的节点先往上跳。

    深度相同了之后,一起往上跳。

    最后跳到一起了就是LCA了。

    预处理:O(n)

    每次查询:O(n)

    2.倍增LCA

    朴素LCA的一种优化。

    一点一点跳,显然太慢了。

    如果要跳x次,可以把x转换为二进制。

    每一位都是1或0,也就是跳或者不跳。

    在第i位,如果跳,就向上跳2(i-1)次。

    至于跳或者不跳,判断很简单。

    如果跳了之后还没在一起,就跳。

    预处理:算出每个点上跳2n次后的位置。(已知上跳20次的位置就是它的父亲)O(nlogn)

    每次询问:O(logn)

     1 #include<cstdio>
     2 #include<cstring>
     3 #include<algorithm>
     4 using namespace std;
     5 
     6 int n,m,s;
     7 int hd[500005],nx[1000005],to[1000005],cnt;
     8 
     9 void add(int af,int at)
    10 {
    11     to[++cnt]=at;
    12     nx[cnt]=hd[af];
    13     hd[af]=cnt;
    14 }
    15 
    16 int d[500005],f[500005][25];
    17 
    18 void pre(int p,int fa)
    19 {
    20     f[p][0]=fa;
    21     d[p]=d[fa]+1;
    22     for(int i=hd[p];i;i=nx[i])
    23     {
    24         if(to[i]!=fa)pre(to[i],p);
    25     }
    26 }
    27 
    28 int lca(int x,int y)
    29 {
    30     if(d[x]<d[y])swap(x,y);
    31     for(int i=20;i>=0;i--)
    32     {
    33         if(d[f[x][i]]>=d[y])x=f[x][i];
    34     }
    35     if(x==y)return x;
    36     for(int i=20;i>=0;i--)
    37     {
    38         if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
    39     }
    40     return f[x][0];
    41 }
    42 
    43 int main()
    44 {
    45     scanf("%d%d%d",&n,&m,&s);
    46     for(int i=1;i<n;i++)
    47     {
    48         int aa,bb;
    49         scanf("%d%d",&aa,&bb);
    50         add(aa,bb);
    51         add(bb,aa);
    52     }
    53     pre(s,0);
    54     for(int i=1;i<=20;i++)
    55     {
    56         for(int j=1;j<=n;j++)
    57         {
    58             f[j][i]=f[f[j][i-1]][i-1];
    59         }
    60     }
    61     for(int i=1;i<=m;i++)
    62     {
    63         int x,y;
    64         scanf("%d%d",&x,&y);
    65         printf("%d
    ",lca(x,y));
    66     }
    67     return 0;
    68 }
    倍增LCA

    3.欧拉序+RMQ

    欧拉序,就是dfs时,无论是进入该点的子树,还是从该点的子树中出来,都记录一遍这个点。这样得到一个序列,就是欧拉序。

    比如说点A为根,BCD为A的儿子的一颗简单的树,加上一个E作为C的儿子。

    其欧拉序就是A B A C E C A D A

    那么,任取两点,它们的LCA,就是欧拉序中,这两个点之间深度最小的点。

    如果一个点在欧拉序中出现了多次,任取一个位置就好。

    区间深度最小点,用RMQ。O(nlogn)预处理后,每次询问O(1)求出。

     1 #include<cstdio>
     2 #include<cstring>
     3 #include<algorithm>
     4 using namespace std;
     5 
     6 int m,n,ecnt,root;
     7 int head[500005],nx[1000005],to[1000005];
     8 int euler[1500005],eucnt,ps[1500005],high[1500005][25];
     9 int fa[500005],dep[500005];
    10 int log[1500005];
    11 
    12 int add(int af,int at)
    13 {
    14     to[++ecnt]=at;
    15     nx[ecnt]=head[af];
    16     head[af]=ecnt;
    17 }
    18 
    19 void dfs(int pos,int fat)
    20 {
    21     dep[pos]=dep[fat]+1;
    22     euler[++eucnt]=pos;
    23     ps[pos]=eucnt;
    24     fa[pos]=fat;
    25     for(int i=head[pos];i;i=nx[i])
    26     {
    27         if(to[i]!=fat)
    28         {
    29             dfs(to[i],pos);
    30             euler[++eucnt]=pos;
    31         }
    32     }
    33 }
    34 
    35 void prelca()
    36 {
    37     for(int i=2;i<=3*n;i++)log[i]=log[i/2]+1;
    38     for(int i=1;i<=eucnt;i++)high[i][0]=euler[i];
    39     for(int i=1;i<=27;i++)
    40     {
    41         for(int j=1;j+(1<<i)-1<=eucnt;j++)
    42         {
    43             if(dep[high[j][i-1]]>dep[high[j+(1<<(i-1))][i-1]])
    44                 high[j][i]=high[j+(1<<(i-1))][i-1];
    45             else
    46                 high[j][i]=high[j][i-1];
    47         }
    48     }
    49 }
    50 
    51 int lca(int x,int y)
    52 {
    53     int ll=ps[x];
    54     int rr=ps[y];
    55     if(ll>rr)int t=ll; ll=rr; rr=t;
    56     int len=rr-ll+1;
    57     if(dep[high[ll][log[len]]]>dep[high[rr-(1<<log[len])+1][log[len]]])
    58         return high[rr-(1<<log[len])+1][log[len]];
    59     else
    60         return high[ll][log[len]];
    61 }
    62 
    63 int main()
    64 {
    65     scanf("%d%d%d",&n,&m,&root);
    66     for(int i=1;i<n;i++)
    67     {
    68         int a,b;
    69         scanf("%d%d",&a,&b);
    70         add(a,b);
    71         add(b,a);
    72     }
    73     dfs(root,0);
    74     prelca();
    75     for(int i=1;i<=m;i++)
    76     {
    77         int q,w;
    78         scanf("%d%d",&q,&w);
    79         printf("%d
    ",lca(q,w));
    80     }
    81     return 0;
    82 }
    欧拉序+RMQ

    4.树链剖分

    把树分成轻链和重链。

    先一遍dfs找到重儿子,即子树最大的儿子。

    每个点与重儿子的连边组成重链。

    第二遍dfs记录每个点的tp值:所在重链的顶端。

    如果在轻链上,tp就是它自己。

    求LCA;类似倍增。

    让tp较深的点上跳,跳到fa[tp]。

    最后tp[x]==tp[y]的时候,二者在同一重链上,LCA即为深度较浅的那个点。

    预处理:O(n)

    每次询问:O(logn)

     1 #include<cstdio>
     2 
     3 int hd[500005],to[1000005],nx[1000005],cnt;
     4 int hs[500005],tp[500005],f[500005],d[500005],sz[500005];
     5 
     6 int n,m,s;
     7 
     8 void add(int af,int at)
     9 {
    10     to[++cnt]=at;
    11     nx[cnt]=hd[af];
    12     hd[af]=cnt;
    13 }
    14 
    15 void dfs(int p,int fa)
    16 {
    17     f[p]=fa;
    18     d[p]=d[fa]+1;
    19     sz[p]=1;
    20     for(int i=hd[p];i;i=nx[i])
    21     {
    22         if(to[i]==fa)continue;
    23         dfs(to[i],p);
    24         sz[p]+=sz[to[i]];
    25         if(sz[to[i]]>sz[hs[p]])hs[p]=to[i];
    26     }
    27 }
    28 
    29 void findtp(int p)
    30 {
    31     if(p==hs[f[p]])tp[p]=tp[f[p]];
    32     else tp[p]=p;
    33     for(int i=hd[p];i;i=nx[i])
    34         if(to[i]!=f[p])findtp(to[i]);
    35 }
    36 
    37 int lca(int a,int b)
    38 {
    39     while(tp[a]!=tp[b])d[tp[a]]>d[tp[b]]?a=f[tp[a]]:b=f[tp[b]];
    40     return d[a]<d[b]?a:b;
    41 }
    42 
    43 int main()
    44 {
    45     scanf("%d%d%d",&n,&m,&s);
    46     for(int i=1;i<n;i++)
    47     {
    48         int x,y;
    49         scanf("%d%d",&x,&y);
    50         add(x,y);
    51         add(y,x);
    52     }
    53     dfs(s,0);
    54     findtp(s);
    55     for(int i=1;i<=m;i++)
    56     {
    57         int a,b;
    58         scanf("%d%d",&a,&b);
    59         printf("%d
    ",lca(a,b));
    60     }
    61     return 0;
    62 }
    树链剖分

    5.离线tarjan

    (待填坑)

    6.欧拉序+约束RMQ

    洛谷上的玄学操作。应该是欧拉序+RMQ的优化。

    把原欧拉序分块,块内预处理,块间ST表。(我并不知道ST表是什么......)

    摘自洛谷题解:

    分块大小定为L=log(n)/2,这样共分D=n/L块,对这D个数(块内最小值)做正常ST表,建表复杂度O(Dlog(D))=O((n/L)(log(n)-log(L))=O(n)

    我们要保证每个步骤都是O(n)的,log(n)/2的块正好消去了ST建表时的log

    但在此之前,我们得处理出块内的最小值,该怎么做呢?一个正常想法就是枚举每个数,一共是O(n)复杂度

    但是,这样做虽然留下了每块的最小值以及其取到的位置,若考虑查询块的一个区间,而这个区间恰好取不到最小值,这时候只能暴力枚举,就破坏了查询O(1)了

    至此我们仍没有使用其±1的特殊性质,现在考虑一下。

    块内一共log(n)/2个数,由乘法原理可知,本质不同的块有U=2^(log(n)/2)=n^(1/2)个,我们不妨处理出每个这种块,复杂度Ulog(n)/2,这个函数增长是小于线性的,可以认为是O(n)

    这样,处理出每个块内两元素的大小关系,就可以用01唯一表示一个块了,可以用二进制存下来,作为一个块的特征,这一步复杂度O(n)

    这样有一个好处,即使查询块内一个区间,我们只需要提取这个区间对应的二进制数,就可以在预处理的数组中O(1)查询了

    (怎么做呢?把这段二进制数提出来,移到最右边,由于我们规定0表示小于,1表示大于,所以会贪心地选取前面的数,查表减去偏移量就可以了)

    查询时,类似分块,边角的块直接查表,中间部分ST表查询,查询是O(1)的。

    至此我们完成了O(n)建表,O(1)查询的约束RMQ。

    一般地,对于任何一个序列,可以在O(n)时间内建成一颗笛卡尔树,把查询该序列RMQ转化为求笛卡尔树LCA,就变成O(1)的了。

    安利一下自己博客

    找时间搞搞吧......

  • 相关阅读:
    The Tamworth Two chapter 2.4
    USACO Controlling Companies chapter 2.3 已跪
    非递归快排
    链表二路归并
    Money Systems chapter 2.3 dp
    #pragma pack与sizeof union
    快慢指针
    12
    11
    10
  • 原文地址:https://www.cnblogs.com/cervusy/p/9502559.html
Copyright © 2020-2023  润新知