算法详解(LCA&RMQ&tarjan)补坑啦!完结撒花(。◕ˇ∀ˇ◕)
首先,众所周知,求LCA共有3种算法(树剖就不说了,太高级,以后再学。。)。
1、树上倍增(ST表优化)
2、RMQ&时间戳(ST表优化)
3、tarjan(离线算法)不讲。。(后面补坑啦!)
一、树上倍增
这种方法原理是这样的:
我们可以知道,最朴素的算法就是一步一步的并查集往上找,知道找到2个点并查集相同,即为LCA
但这种算法的时间效率为O(NM)看到0<n,m<5*10^5我们就知道一定会炸。
但是,我们可以发现给出树后,每个点的LCA及走到LCA的路径一定是固定的。
所以可以ST表优化。
首先先BFS出每个点在树上的深度。。(记为depth[i])
接着我们要先让2个点的深度相同,之后2个点一起走,可以加快效率。
最后我们直接倍增上去。(if(fa[i][u]!=fa[i][v])u=fa[i][u];v=fa[i][v];)
最后只要往上走一步,就是LCA了。
重点!倍增时倍增循环在外!不能在内!否则会挂!
下面贴代码(debug了三小时,才调好。膜拜zxyer)
#include<iostream> #include<cstdio> #include<cstring> using namespace std; struct data{ int next,to; }g[500011]; int depth[500011]; int que[1000001]; bool visit[500011]; int head[500011]; int fa[30][500011]; int n,q,root,num=0; int lca(int u,int v){ if(depth[u]<depth[v])swap(u,v); int dc=depth[u]-depth[v]; for(int i=0;i<=29;i++) if((1<<i)&dc&&fa[i][u]){ u=fa[i][u]; } if(u==v)return u; for(int i=29;i>=0;i--) if(fa[i][u]!=fa[i][v]&&fa[i][u]){ u=fa[i][u]; v=fa[i][v]; } return fa[0][u]; } void bfs(int u) { memset(que,0,sizeof(que)); memset(visit,0,sizeof(visit)); visit[u]=true; que[1]=u; int h=1,l=1; while(h<=l) { int rt=que[h]; for (int i=head[rt];i;i=g[i].next) { int v=g[i].to; if ( !visit[v] ) { visit[v]=true; depth[v]=depth[rt]+1; que[++l]=v; } } h++; } } int main(){ scanf("%d%d",&n,&q); memset(g,0,sizeof(g)); memset(fa,0,sizeof(fa)); memset(depth,0,sizeof(depth)); for(int i=1;i<n;i++) { int f,t; scanf("%d%d",&f,&t); g[++num].next=head[f]; head[f]=num; g[num].to=t; fa[0][t]=f; if(fa[0][f]==0)root=f; } for(int j=1;j<=29;j++) for(int i=1;i<=n;i++) { fa[j][i]=fa[j-1][fa[j-1][i]]; } depth[root]=1; bfs(root); for(int i=1;i<=q;i++){ { int num1,num2; scanf("%d%d",&num1,&num2); printf("%d\n",lca(num1,num2)); } } }
二、RMQ+时间戳
这个算法理解了很久才懂
首先我们都知道如果把树看成一个无向图,那么LCA一定在u->v的最短路上。而且,LCA的点就是最短路上depth最小的点。
换句话说,就是LCA是2个点到根节点的路径的第一个交汇处。
接下来用dfs为点标号,用id[i]表示这个点第一次出现在顶点序列的标号。
接下来就是求id[u]<i<id[v]中depth的最小值啦!
这个过程可以用RMQ高速解决。
所以LCA=RMQ(depth)(u,v)
注意!这个算法比较难懂,可以先看看RMQ,理解之后再画画图,恩,就差不多了。
下面贴代码
#include <cstdio>
#include <cstring> #include <queue> #include <algorithm> #define MAXN 1010 #define MAXM 100000 using namespace std; struct Edge { int from, to, next; }; Edge edge[MAXM]; int head[MAXN], edgenum; int vs[MAXN<<1];//第i次DFS访问节点的编号 int depth[MAXN<<1];//第i次DFS访问节点的深度 int id[MAXN];//id[i] 记录在vs数组里面 i节点第一次出现的下标 int dfs_clock;//时间戳 int N, M, Q;//点数 边数 查询数 int dp[MAXN<<1][20];//dp[i][j]存储depth数组 以下标i开始的,长度为2^j的区间里 最小值所对应的下标 void init() { edgenum = 0; memset(head, -1, sizeof(head)); } void addEdge(int u, int v) { Edge E = {u, v, head[u]}; edge[edgenum] = E; head[u] = edgenum++; } void getMap() { int a, b; while(M--) scanf("%d%d", &a, &b), addEdge(a, b), addEdge(b, a); } void DFS(int u, int fa, int d)//当前遍历点以及它的父节点 遍历点深度 { id[u] = dfs_clock; vs[dfs_clock] = u; depth[dfs_clock++] = d; for(int i = head[u]; i != -1; i = edge[i].next) { int v = edge[i].to; if(v == fa) continue; DFS(v, u, d+1); vs[dfs_clock] = u;//类似 回溯 depth[dfs_clock++] = d; } } void find_depth() { dfs_clock = 1; memset(vs, 0, sizeof(vs)); memset(id, 0, sizeof(id)); memset(depth, 0, sizeof(depth)); DFS(1, -1, 0);//遍历 } void RMQ_init(int NN)//预处理 区间最小值 { for(int i = 1; i <= NN; i++) dp[i][0] = i; for(int j = 1; (1<<j) <= NN; j++) { for(int i = 1; i + (1<<j) - 1 <= NN; i++) { int a = dp[i][j-1]; int b = dp[i + (1<<(j-1))][j-1]; if(depth[a] <= depth[b]) dp[i][j] = a; else dp[i][j] = b; } } } int query(int L, int R) { //查询L <= i <= R 里面使得depth[i]最小的值 返回对应下标 int k = 0; while((1<<(k+1)) <= R-L+1) k++; int a = dp[L][k]; int b = dp[R - (1<<k) + 1][k]; if(depth[a] <= depth[b]) return a; else return b; } int LCA(int u, int v) { int x = id[u];//比较大小 小的当作左区间 大的当作右区间 int y = id[v]; if(x > y) return vs[query(y, x)]; else return vs[query(x, y)]; } void solve() { int a, b; while(Q--) { scanf("%d%d", &a, &b); printf("LCA(%d %d) = %d\n", a, b, LCA(a, b)); } } int main() { while(scanf("%d%d%d", &N, &M, &Q) != EOF) { init(); getMap(); find_depth();//DFS遍历整个树 求出所需要的信息 RMQ_init(dfs_clock - 1); solve(); } return 0; }
重点同上哈!倍增写外面!
三、tarjan算法求LCA
这个算法和之前的tarjan求强联通分量有一些差别(只是名字一样而已)
这个算法非常妙啊,其实就是从根节点dfs标记父节点,但是厉害的地方在于,在它标记完根节点后,恰好能够更新答案。
这个算法比较绕,网上有很多的图解,我就不贴了,我们来具体算法理解。
下面是tarjan的主体,h就是链表的头。。这个不讲,这里讲一下q数组,它用于存储询问,g[i].to表示他要查询的第二个节点。
void tarjan(int x){ for(int i=h[x];i;i=g[i].next) tarjan(g[i].to),f[g[i].to]=x; for(int i=q[x];i;i=g[i].next) ans[g[i].to]=ans[g[i].to]>0?getfa(ans[g[i].to]):x; }
很显然,在回溯后,我们计算答案,如果没有出现g[i].to的答案,我们就将他标记为x,这是什么意思呢?其实如果标记为X,就说明在dfs时先搜到了x,再搜g[i].to,显然x即为这2者的公共祖先。
但是如果我们已经更新过ans[g[i].to],其实这说明了两个点不在同一子树上,所以我们要找到后一个节点的父亲。(重点来了!)
我们在做getfather的操作的时候,由于是先序遍历,所以我们做到这个dfs时,寻找父亲只会找到他们的公共祖先然后停止。是不是很妙?
嗯对,所以是不是很简单?
#include<iostream> #include<cstdio> using namespace std; int h[500005],q[500005]; bool fa[500005]; int f[500005]; int ans[500005]; int n,m,num=0; struct edge{ int to,next; }g[1500005]; int getfa(int x){return f[x]?f[x]=getfa(f[x]):x;} void ins(int *h,int u,int v){g[++num].next=h[u];h[u]=num;g[num].to=v;} void tarjan(int x){ for(int i=h[x];i;i=g[i].next) tarjan(g[i].to),f[g[i].to]=x; for(int i=q[x];i;i=g[i].next) ans[g[i].to]=ans[g[i].to]>0?getfa(ans[g[i].to]):x; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<n;i++){ int x,y; scanf("%d%d",&x,&y); ins(h,x,y); fa[y]=1; } for(int i=0;i<m;i++) { int x,y; scanf("%d%d",&x,&y); ins(q,x,i);ins(q,y,i); } for(int i=1;i<=n;i++)if(!fa[i]){tarjan(i);break;} for(int i=0;i<m;i++)printf("%d\n",ans[i]); return 0; }