LCA专题
T1 Distance Queries 距离咨询
算是LCA这里自己的一个经典总结了吧
//Distance Queries 距离咨询 //先说一下,LCA 每一道题,都要Tarjan(离线),dfs序(RMQ)(在线),倍增三种都打一遍 //dfs序: //在线,基于dfs序(欧拉序),要用RMQ处理 //它并不强制在线 //欧拉序有两种记录方法:1.dfs到加进,dfs回加进,总共加入度遍 //2.dfs进加进,dfs最后一次回加进,总共加两遍。我举个栗子: //2是1 的儿子,在2.下:1 2 2 1 ;在1.下 1 2 1; //当然还有几种写法,各有长处,或者说根据题意,自己随便花着记 ,A了就行 //当求LCA的时候 在序1下我们要求的两个点在欧拉序中的第一个位置之间肯定包含他们的lca //因为欧拉序1上任意两点之间肯定包含从第一个点走到第二个点访问的路径上的所有点 //这个稍微在脑中模拟一下就理解了 //只需要记录他们的深度,然后从两个询问子节点x,y第一次出现的位置之间的深度最小值即可 //为什么深度最小是lca,你x到y,在序一下,哎呀,这个以我的智商是可以显然的,过了 //求深度最小显然是用st表,因为树组和线段树已经有点印象了,没有必要浪费时间,树组的区改再注意 //dfs序在序列2下不好写,或者说没有必要去写,但是处理处理树上最值/和会用它的转换,所以 //各有适用,适题选择 //Tarjan: //离线,基于dfs,运用并查集 //他好像必须离线,如果在线,你每一个询问就去dfs到询问一遍,恭喜您挂了(目前我是这么理解的) //就是在一次遍历中把所有询问一次性解决,相对稳定,时间复杂度也比较居中,也很容易理解 //呃。。我先说一下,我一直以为tarjan的意思是跟求割点一个思想 //但其实tarjan的意思是这种方法也是tarjan老爷子提出的 //时间复杂度:O((n+q) α(n))//阿尔法不知道什么意思,猜着是并查集的时间复杂度,有的没有 //这种算法基于dfs,在 dfs 的过程中,对于每个节点位置的询问做出相应的回答。 //dfs 的过程中,当一棵子树被搜索完成之后,就把他和他的父亲合并成同一集合;在搜索当前子树节点 //的询问时,如果该询问的另一个节点已经被访问过,那么该编号的询问是被标记了的,于是直接输出 //当前状态下,另一个节点所在的并查集的祖先;如果另一个节点还没有被访问过,那么就做下标记, //继续 dfs 、 //1任选一个点为根节点,从根节点开始。 //2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。 //3.若是v还有子节点,继续dfs,否则下一步。 //4.合并v到u上。 //5.寻找与当前点u有询问关系的点vv //6.若是vv已经被访问过了,则可以确认u和vv的最近公共祖先为vv被合并到的父亲节点a //并查集初始化祖先是自己 //我再理解一下,每次找到一个点,注意是dfs完回溯时合并,这个是因为在合并完之后我们 //立刻去找与u有关的所有访问,就是访问u o的Lca,此刻如果o已经被访问过了,直接输出o的最根上的 //祖先,就是他们的lca,如果没有就跳过,为什么这样是对的,因为o如果已经被访问过,那么说明 //或者这么说,u与o如果没有祖先关系,那么此刻o已经回溯完了,它的此刻的最根一定是在u没有回溯的 //状态下的最根上那个根,如果u和o是有祖先关系,那么u和o的lca就是u,如果我们在回溯前更新 //所有点的最根都是rt,我们需要这么一个递归层换的过程来恰好求出正确的lca //其实我说了这么多,在脑中模拟一下我就又可以显然了 //把它的祖先设为和自己的父亲一个祖先并标记 //因为我们是dfs,所以第一次的合并都会是自己的父亲 //倍增: //在线求 LCA //时间和空间复杂度分别是 O((n+q)logn)和 O(nlogn) //在闫立仁的指导下,通过训练指南理解了 //在Bfs下进行预处理 //定义 f[x][k]表示x的2的k次方辈祖先,即从节点x走向根走2的k次方步所到达的节点 //如果该节点不存在f[x][k]=0; f[x][0]就是x的父亲,这叫初始化 //f[x][k]=f[f[x][k-1]][k-1] 这是显然的吧,爸爸的爸爸是爷爷,爷爷的爷爷是我也不知道是啥 //先对这颗树进行广度优先遍历,按照层次顺序,在节点入队之前,入队之前!把它进行这番操作 //为什么是入队之前:(用dfs也行,反正它们都没出错,这就在dfs前预处理):就是我们的决策 //从当前x的祖先进行决策,如果出来再决策,决策溯源没有值啊 //然后是每次的询问:设d[x]表示x的深度,d[x]>=d[y],不然就交换,反正要确定谁深 //x 与 y的深度差,一定可以被二进制拆分,我们把x和y调整到同一深度,k是倒序,若x比y深 //令x=f[x][k],这样我们就可以把x和y同时调整了,若此时x==y了,说明x和y有祖先关系,输出即可 //把x和y同时向上调整,就是向上走,k是倒序,这倒序正序无所谓,无论怎么拆分,解是唯一的 //if(f[x][k]!=f[y][k])x=f[x][k],y=f[x][k];k从log(n)跑到0,此刻x,y必定只差一步就相会了 //f[x][0]即为答案,再次明确,f[x][0]是x向根走1步所到达的点,我们从来没有更新过f数组 //一直是x,y在轮换,为什么不能当f[x][k]==f[y][k]直接输出答案,因为我们求的是最近公共祖先 //三种方法得出的答案,只要样例过了,爷就1A //先用dfs序二和st表快速存储整个树的距离,在找最近公共祖先,再查距离 #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <cmath> using namespace std; const int mx=80000+1000; int n,m,K; int len,len1; struct Nodee{ int l,r; }qq[mx/4]; int dis[mx]; struct Node{ int from; int to; int next; int data; bool vi; int anss; }a[mx],b[mx];int head[mx],head1[mx];//b是tarjan的访问储存 int an1[mx],an2[mx],an3[mx],ans[mx]; int c[mx]; int fa[mx],cnn;//tarjan算法的并查集 bool vis[mx]; //int str[mx],en[mx],d[mx],tim,f[mx][20];//原图的dfs序2 总的区间和 //我一开始是想用st表求出任意两点间的距离,这个想法先放着,回头问问,就是把 //边权转点权有没有较好的方法,吴迪讲先建树再转,但又觉得不太现实,所以用 //树组记录从根到每个点的距离,这个还行,tarjan可以做的时候顺手一加 int d[mx],du[mx],f[mx][20],tim,str[mx],pos[mx][20];//原图的dfs序1 ,dfs序倍增的做法 int ff[mx][20],k1; void Insert(int u,int v,int w){ a[++len].from=u; a[len].to=v; a[len].data=w; a[len].next=head[u]; head[u]=len; } int lowbit(int x){ return x&(-x); } void updata(int i,int w){ while(i<=n){ c[i]+=w; i+=lowbit(i); } } int getsum(int i){ int res=0; while(i>0){ res+=c[i]; i-=lowbit(i); } return res; } int ask(int l,int r){//st表的查询 int kk=log((r-l+1))/log(2); // printf("l=%d r=%d kk=%d\n",l,r,kk); //我return的是区间深度最小值,而不是我要的节点编号 if(f[l][kk]<=f[r-(1<<kk)+1][kk]){ // printf("pos[%d][%d]=%d\n",l,kk,pos[l][kk]); return pos[l][kk]; } else{ // printf("1k pos[%d][%d]=%d\n",r-(1<<kk)+1,kk,pos[r-(1<<kk)+1][kk]); return pos[r-(1<<kk)+1][kk]; } // return min(f[l][kk],f[r-(1<<kk)+1][kk]); } void dfs(int u,int faa){ d[++tim]=u; str[u]=tim; for(int i=head[u];i!=0;i=a[i].next){ int v=a[i].to; if(v!=faa){ //儿子的深度就是父亲的深度加一,在递归前处理,回溯后父亲深度里没值,所以递归前 du[v]=du[u]+1; dfs(v,u); d[++tim]=u; } } // en[u]=time; } int find(int x){ if(fa[x]==x)return x; return fa[x]=find(fa[x]); } void merge(int x,int y){ int xx=find(x); int yy=find(y); fa[yy]=xx; } void Insert1(int u,int v){ b[++len1].from=u; b[len1].to=v; b[len1].next=head1[u]; head1[u]=len1;//len1写成len了 } void dfss(int u,int faa){ vis[u]=1; for(int i=head[u];i!=0;i=a[i].next){ int v=a[i].to; if(v!=faa){ dfss(v,u); merge(u,v); } } //访问与u有关的所有询问,要在遍历完u的所有子树之后,不然会有很多重复 //tarjan的询问并不是按照顺序的,所以我们要记录 ,最后遍历一遍 for(int j=head1[u];j!=0;j=b[j].next){ int vv=b[j].to; // printf("j=%d u=%d vv=%d\n",j,u,vv); if(vis[vv]==1){ int an=dis[u]+dis[vv]-2*dis[find(vv)]; // printf("dis[%d]=%d dis[%d]=%d dis[%d]=%d\n",u,dis[u],vv,dis[vv],find(vv),dis[find(vv)]); b[j].anss=an; b[j].vi=1; // an2[++cnn]=an; } } } void dfsss(int u,int faa){ for(int i=head[u];i!=0;i=a[i].next){ int v=a[i].to; if(v!=faa){ ff[v][0]=u; for(int j=1;j<=k1;++j){ ff[v][j]=ff[ff[v][j-1]][j-1];//是ff,不是f } dfsss(v,u);//这里忘写dfs了 } } } void work1(){//dfs序 du[1]=1; dfs(1,0); /* for(int i=1;i<=tim;++i){ printf("d[%d]=%d du[%d]=%d\n",i,d[i],d[i],du[d[i]]); }*/ for(int i=1;i<=tim;++i){ f[i][0]=du[d[i]];//du[d[i]]理解,不是du[i]; pos[i][0]=d[i]; // printf("i=%d pos=%d\n",i,pos[i][0]); } int nn=log(tim)/log(2); nn++; // printf("nn=%d\n",nn); // printf("tim=%d\n",tim); // i+(1<<j)-1<=tim; for(int j=1;j<=nn;++j){ for(int i=1;i+(1<<j)<=tim+2;i++){ // printf("i=%d j=%d\n",i,j); if(f[i][j-1]<=f[i+(1<<(j-1))][j-1]){ f[i][j]=f[i][j-1]; pos[i][j]=pos[i][j-1]; } else { f[i][j]=f[i+(1<<(j-1))][j-1]; pos[i][j]=pos[i+(1<<(j-1))][j-1]; } // printf("pos[%d][%d]=%d\n",i,j,pos[i][j]); //f[i][j]=min(f[i][j-1],); // pos() // printf("f[%d][%d]=%d\n",i,j,f[i][j]); } } for(int i=1;i<=K;++i){ int l=str[qq[i].l]; int r=str[qq[i].r]; // int lca=ask(min(l,r),max(l,r));//这里卡了我一个小时啊啊啊啊啊啊啊 //这个lca是区间深度最小值,不是lca的节点编号 // printf("lca=%d",lca); //这里,dis[是点的编号,不是在dfs序中的编号] //再开一个记录编号,和f同理 // printf("1k lca=%d\n",lca); int an=dis[qq[i].l]+dis[qq[i].r]-2*dis[lca]; an1[i]=an; } } void work2(){//tarjan for(int i=1;i<=n;++i){ fa[i]=i; } dfss(1,0); for(int i=1;i<=len1;i+=2){ int tot=0; for(int j=0;j<=1;++j){ // int tot=0; if(b[i+j].vi==1 && tot==0){ an2[++cnn]=b[i+j].anss; tot=1; // printf("cnn=%d \n",cnn); } } } } void work3(){//树上倍增 du[1]=1; //dfs(1); ff[1][0]=1; dfsss(1,0); for(int i=1;i<=K;++i){ //这里用的度(深度),是work1的;所以如果注掉work1,记得在这跑dfs; int x=qq[i].l; int y=qq[i].r; int an=dis[x]+dis[y]; // int dl=du[x]; // int dr=du[y]; //有好多细节遗漏错误 if(du[x]>du[y])swap(x,y); // printf("%d\n",log(0)); // int kkk=log(dr-dl)/log(2); //这里的ff全写错了 // printf("k1=%d\n",k1); for(int j=k1;j>=0;--j){ // printf("du[%d]=%d ff[%d][%d]=%d du[%d]=%d\n",x,du[x],y,j,ff[y][j],ff[y][j],du[ff[y][j]]); if(du[ff[y][j]]>=du[x])y=ff[y][j]; // y=ff[y][j]; } // printf("x=%d y=%d\n",x,y); if(x==y){ an3[i]=an-2*dis[x]; } else { // printf("k1=%d\n",k1); for(int j=k1;j>=0;--j){ if(ff[x][j]!=ff[y][j]){ x=ff[x][j]; y=ff[y][j]; } } // printf("lca=%d\n",ff[x][0]); an3[i]=an-2*dis[ff[x][0]]; } } } void df(int u,int faa){ for(int i=head[u];i!=0;i=a[i].next){ int v=a[i].to; if(v!=faa){ dis[v]=dis[u]+a[i].data; df(v,u); } } } void Solve(){ scanf("%d%d",&n,&m); k1=log(n)/log(2)+1; //k1不能在定义时赋值,那时n还是0; //我觉得这个方向没有任何作用 for(int i=1;i<=m;++i){ int u,v,w;char c; scanf("%d%d%d %c",&u,&v,&w,&c); Insert(u,v,w); Insert(v,u,w); //Inser1放错地方了,b存的是询问啊 // Insert1(u,v); // Insert1(v,u); // int uu,vv; // uu=max(u,v); // vv=min(u,v); // updata(vv,w); // updata(uu+1,-w); //我们可以直接在dfs的时候存一下根到所有点的距离,我是脑瘫谢谢 } dis[1]=0; df(1,0); /* for(int i=1;i<=n;++i){ printf("dis[%d]=%d\n",i,dis[i]); }*/ scanf("%d",&K); //我现在单方面宣布,根就是一了 for(int i=1;i<=K;++i){ int x,y; scanf("%d%d",&x,&y); qq[i].l=min(x,y); qq[i].r=max(x,y); Insert1(x,y); Insert1(y,x); } /* for(int i=1;i<=len1;++i){ printf("b[]from=%d to=%d\n",b[i].from,b[i].to); }*/ work1(); work2(); // printf("kkkkkkkkkkk\n"); work3(); /* for(int i=1;i<=K;++i){ printf("%d\n",an1[i]); }*/ for(int i=1;i<=K;++i){ if(an1[i]==an2[i] && an2[i]==an3[i]){ printf("%d\n",an1[i]); } else { printf("%d %d %d\n",an1[i],an2[i],an3[i]); printf("您挂了\n"); } } } int main(){ Solve(); return 0; }
祝您,武运昌隆