倍增小总结
因为考试倍增题挂了一个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]; }
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; }
这种题就很皮了。
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混考。代码也懒得贴了,不是重点。
●例题
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; }
http://codeforces.com/problemset/problem/733/F
题解:http://www.cnblogs.com/MashiroSky/p/6027250.html
其他的题就不拿出来水了,都是很裸的。
●总结
再提一句,倍增算法其实是比较裸的了,很容易看出来。
先看数据范围吧,点数是100000附加100000组询问,估计就是nlogn预处理加查询咯。
再看步数吧。那个10^18的步数不明显是要倍增做吗?
其实倍增没什么难得,代码都可以强行记住。只要学会灵活运用,就不会怕倍增题目了。。
好吧,最后的总结其实是:注意long long!
(完)