【倍增小总结】

倍增小总结

  因为考试倍增题挂了一个long long,被迫写总结QAQ

  ●何谓倍增法

  简单的说就是每次步数是原来的两倍,一般用数组f[i][k]表示在i这个点走了2^k步之后的结果。

  ●倍增法的用处?

  倍增法多用于数列或者树上,进行一段较大的区间查询,把每次查询的时间由O(n)降到O(logn)。

  ●倍增法的常见应用

  一、RMQ 算法

  这个算法其实可以说是个dp了。

  1.简单介绍

  大致是在一段很长序列上进行区间查询,用st[i][k]表示从i开始后2^k个数中的最大/小值或者和。这个数组叫做st表,建立的话是O(nlogn)的时间,但查询只需要O(1)。

  2.对比数据结构

  相比于线段树来说,不仅代码更好写,并且时间更优,因为线段树常数大且每次操作都是logn。但线段树可以支持很多操作,还可以修改之类的,就很屌了。

  比上树状数组么。。

  树状数组常数也很小,并且支持修改操作,但它有一个致命的BUG,那就是它只能查询与前缀相关的东西,譬如前缀和之类的。

如果查询区间和,直接用前缀和减去就好了,但是如果查询区间最大值,那么它就没有任何办法了。

  3.简单建立st表

  确实很简单,大致参照一个dp思路

  i~i+2^k的区间信息可由i+2^(k-1)~i+2^(k-1)+2^(k-1)和i~i+2^(k-1)合并得到。st数组定义参照介绍。

for(int k=1;(1<<k)<=n;k++)

for(int i=1;i+(1<<j)-1<=n;i++)

st[i][k]=min(st[i][k-1],st[i+(1<<(k-1))][k-1]);

  

  值得注意的是,由于处理2^k区间长度时要用到i和i+2^(k-1)点的信息,所以先枚举k。

  4.查询没什么好提的,根据定义来就行

int rmq(int a,int b){

if(a>b)swap(a,b);

int t=(int)log2(b-a+1);

return min(st[a][t],st[b-(1<<t)+1][t]);

}

  

 

  5.简单的小优化

  由于上述的2^i和log2(i)都需要计算,而log2函数的常数是比较大的,所以可以根据幂和log的性质预处理出mi[]和lg[]数组。

此外,RMQ算法在后缀数组中用的还是比较多的。

  二、树上倍增

  1.寻找爸爸

  树上倍增这个东西呢,一般是用来求lca的,单次查询时间logn,具体的描述有点长,就不写了。

  还是大致提一下。关键是建立fa[i][k]表示i点向上数2^k个父亲是谁。查询lca(x,y)的时候得到x,y的深度,帮它们两个提到同一深度,再同时向上提找爸爸就好了。具体有些细节不再赘述。

  挂一波代码。

void dfs(int u,int dad,){

    p[u][0]=dad;
    
    d[u]=d[dad]+1;

    for(int i=1;(1<<i)<=d[u];i++)

    p[u][i]=p[p[u][i-1]][i-1];

   for(int i=hd[u];i;i=e[i].next)

    if(e[i].v!=dad)dfs(e[i].v,u,e[i].w);

}

 

int lca(int x,int y){

    if(d[x]>d[y])swap(x,y);

    for(int i=20;~i;i--)

    if(d[x]<=d[y]-(1<<i))y=p[y][i];

    if(x==y)return x;

    for(int i=20;~i;i--){

    if(p[x][i]==p[y][i])continue;

    x=p[x][i];y=p[y][i];

    }

    return p[x][0];

}
View Code

  2.找爸爸的优化版

  预处理还是nlogn的,只不过把查询变成了RMQ O(1)算法。

  怎么做呢?

  很简单,中序遍历一遍树,记录欧拉序和每个点在欧拉序中最早出现的位置fir[i]。根据深度最小建立st表,查询lca(x,y)的时候只用查询rmq(fir[x],fir[y])就好了。证明略。

  这样的话,我们把查询时间变成了O(1),避免和数据结构一起用的时候出题人卡双log的情况。下面给出一道卡双log的例题。

  http://begin.lydsy.com/JudgeOnline/problem.php?id=2100

  题解的话看这个http://blog.csdn.net/rzo_kqp_orz/article/details/52280811

  代码给在这里

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<vector>
#include<cstring>
#define ll long long
#define N 100005
using namespace std;
int ol[N<<2],dfn[N],dep[N],rmq[N<<2][25];
int st[N],ed[N],fir[N],lg[N<<2];
int n,q,ont,dnt;vector<int>s[N];
struct node{int x,y,len;}t[N<<2];
void dfs(int u,int pre){
    ol[++ont]=u;dfn[++dnt]=u;
    dep[u]=dep[pre]+1;st[u]=dnt;
    fir[u]=ont;
    for(int i=0;i<s[u].size();i++){
        int v=s[u][i];
        if(v==pre)continue;
        dfs(v,u);ol[++ont]=u;
    }
    ed[u]=dnt;
}

void rmqpre(){
    lg[1]=0;rmq[1][0]=ol[1];
    for(int i=2;i<=ont;i++)rmq[i][0]=ol[i],lg[i]=lg[i>>1]+1;
    for(int j=1;j<=18;j++)
    for(int i=1;i+(1<<j)<=ont;i++){
        rmq[i][j]=rmq[i][j-1];
        if(dep[rmq[i+(1<<(j-1))][j-1]]<dep[rmq[i][j]])
        rmq[i][j]=rmq[i+(1<<(j-1))][j-1];
    }
}



int lca(int x,int y){
    x=fir[x];y=fir[y];if(x>y)swap(x,y);int t=lg[y-x+1];
    return dep[rmq[x][t]]<dep[rmq[y-(1<<t)+1][t]]?rmq[x][t]:rmq[y-(1<<t)+1][t];
}
int dis(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}

node merge(node a,node b){
    if(a.len==-1)return b;
    int x1=a.x,x2=b.x;
    int y1=a.y,y2=b.y;
    node t=a.len>b.len?a:b;
    if(dis(x1,x2)>t.len)t=(node){x1,x2,dis(x1,x2)};
    if(dis(x1,y2)>t.len)t=(node){x1,y2,dis(x1,y2)};
    if(dis(y1,x2)>t.len)t=(node){y1,x2,dis(y1,x2)};
    if(dis(y1,y2)>t.len)t=(node){y1,y2,dis(y1,y2)};
    return t;
}

void build(int u,int l,int r){
    if(l==r){
        t[u].x=t[u].y=dfn[l];
        t[u].len=0;return;
    }
    int mid=(l+r)>>1;
    build(u<<1,l,mid);
    build(u<<1|1,mid+1,r);
    t[u]=merge(t[u<<1],t[u<<1|1]);
}

node query(int u,int l,int r,int x,int y){
    if(x<=l&&r<=y)return t[u];
    int mid=(l+r)>>1;
    node t=(node){0,0,-1};
    if(x<=mid)t=merge(t,query(u<<1,l,mid,x,y));
    if(y>mid)t=merge(t,query(u<<1|1,mid+1,r,x,y));
    return t;
}

int main(){
//    freopen("1.in","r",stdin);
  //  freopen("wa.out","w",stdout);
    scanf("%d%d",&n,&q);
    for(int i=1;i<n;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        s[u].push_back(v);
        s[v].push_back(u);
    }
    dfs(1,0);rmqpre();
    build(1,1,dnt);
    for(int i=1;i<=q;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        if(st[x]>st[y])swap(x,y);
        node ans=(node){0,0,-1};
        if(st[x]>1)ans=merge(ans,query(1,1,dnt,1,st[x]-1));
        if(ed[x]+1<st[y])ans=merge(ans,query(1,1,dnt,ed[x]+1,st[y]-1));
        int end=max(ed[x],ed[y]);
        if(end<dnt)ans=merge(ans,query(1,1,dnt,end+1,dnt));
        printf("%d
",ans.len==-1?0:ans.len);
    }
    
    return 0;
}
View Code

  这种题就很皮了。

 

  3.树上倍增找儿子

  这种题见得到是不多啊,但是noip见过一道,开车旅行的什么,烦的一匹,这种东西建边还要卡你,逼着你用nlogn的stl或数结构,非常的皮。

  有一篇写的很好的博客,贴在这里,只不过他的代码很丑陋。

  http://www.cnblogs.com/Paul-Guderian/p/6853123.html

  哎这个人就把倍增的精髓领会到了,不错不错。

  4.序列诡异倍增

  http://www.cnblogs.com/oijzh/archive/2012/08/20/2648023.html

  这个就很皮了。

 

  三、倍增法求后缀数组sa[]

  哇这个讲起来不是一般滴麻烦,提一下表示自己知道。

  还经常和RMQ混考。代码也懒得贴了,不是重点。

 

  ●例题

  https://vijos.org/p/1843

  Noip2013的货车运输,值得一做,它把RMQ算法求最小距离也混进了lca里面,还不错。

  顺便也贴了代码

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#define N 50005
using namespace std;
int n,m,q,fa[N],hd[N],p[N][25],d[N],rmq[N][25],cnt=1,tot;
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}struct edge{int v,next,w;}e[N]; 
struct edge1{int u,v,w;bool operator <(const edge1 &b)const{return w>b.w;}}e1[N];

void dfs(int u,int dad,int val){
    p[u][0]=dad;
    d[u]=d[dad]+1;
    rmq[u][0]=val;
    for(int i=1;(1<<i)<=d[u];i++){
        p[u][i]=p[p[u][i-1]][i-1];
        rmq[u][i]=min(rmq[u][i-1],rmq[p[u][i-1]][i-1]);
    }
    for(int i=hd[u];i;i=e[i].next)
    if(e[i].v!=dad)dfs(e[i].v,u,e[i].w);    
}

int lca(int x,int y){
    int ans=99999999;
    if(d[x]>d[y])swap(x,y);
    for(int i=20;~i;i--)
    if(d[x]<=d[y]-(1<<i))
    {ans=min(ans,rmq[y][i]);y=p[y][i];}
    if(x==y)return ans;
    for(int i=20;~i;i--){
        if(p[x][i]==p[y][i])continue;
        ans=min(rmq[x][i],ans);
        ans=min(rmq[y][i],ans);
        x=p[x][i];y=p[y][i];
    }
    return min(ans,min(rmq[x][0],rmq[y][0]));
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        e1[i]=(edge1){u,v,w};
    }
    for(int i=1;i<=n;i++)fa[i]=i;
    sort(e1+1,e1+1+m);
    for(int i=1;i<=m;i++){
        int u=find(e1[i].u),v=find(e1[i].v);
        if(fa[u]==fa[v])continue;fa[v]=u;
        e[++tot]=(edge){e1[i].v,hd[e1[i].u],e1[i].w};hd[e1[i].u]=tot;
        e[++tot]=(edge){e1[i].u,hd[e1[i].v],e1[i].w};hd[e1[i].v]=tot;
        cnt++;if(cnt==n)break;
    }
    dfs(1,0,0);
    scanf("%d",&q);
    for(int i=1;i<=q;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        int u=find(x),v=find(y);
        if(u!=v)puts("-1");
        else printf("%d
",lca(x,y));
    }
    return 0;
}
View Code

  http://codeforces.com/problemset/problem/733/F

  题解:http://www.cnblogs.com/MashiroSky/p/6027250.html

 

  其他的题就不拿出来水了,都是很裸的。

 

  ●总结

  再提一句,倍增算法其实是比较裸的了,很容易看出来。

  先看数据范围吧,点数是100000附加100000组询问,估计就是nlogn预处理加查询咯。

  再看步数吧。那个10^18的步数不明显是要倍增做吗?

  其实倍增没什么难得,代码都可以强行记住。只要学会灵活运用,就不会怕倍增题目了。。

   

 

  好吧,最后的总结其实是:注意long long!

 

                        (完)

 

 

原文地址:https://www.cnblogs.com/wsy01/p/7571386.html