Lca的两种做法
昨晚西山居第一场的比赛中的最后一题,很明显的Lca,不过发现自己居然没有模板- -,以前都没做过。。。最后网上搞了个模板各种修改,wa了n把终于过了。。。
今天来总结下以便下次碰到不至于这么坑。。。
在线算法,Lca+Rmq:
/**************** *西山居第一场LCA* (1) / \ (2) (7) / \ \ (3) (4) (8) / \ (5) (6) 一个nlogn 预处理,O(1)查询的算法. Step 1: 按先序遍历整棵树,记下两个信息:结点访问顺序和结点深度. 如上图: 结点访问顺序是: 1 2 3 2 4 5 4 6 4 2 1 7 8 7 1 //共2n-1个值 结点对应深度是: 0 1 2 1 2 3 2 3 2 1 0 1 2 1 0 Step 2: 如果查询结点3与结点6的公共祖先,则考虑在访问顺序中 3第一次出现,到6第一次出现的子序列: 3 2 4 5 4 6. 这显然是由结点3到结点6的一条路径. 在这条路径中,深度最小的就是最近公共祖先(LCA). 即 结点2是3和6的LCA. ****************/ #include<cstdio> #include<cstring> #include<string> #include<algorithm> #include<map> #include<cmath> using namespace std; #define N 100009 struct edge{ int v; int next; }e[N*2];//由上可知为双向边 int ecnt; int head[N]; int vis[N]; int n; int R[N];//在rmq中的位置 int p[N*2];//原标号 int dis[N];//点到根的距离 int dep[N*2];//深度点标号重新标过,p[num]指向原来的标号 int dp[19][N*2];//Rmq int num; map<string,int> mpp; void init(){ memset(head,-1,sizeof(head)); memset(R,-1,sizeof(R)); memset(vis,0,sizeof(vis)); memset(dis,0,sizeof(dis)); mpp.clear(); ecnt=0; } void add(int u,int v){ e[ecnt].v = v; e[ecnt].next = head[u]; head[u] = ecnt++; e[ecnt].v = u; e[ecnt].next = head[v]; head[v] = ecnt++; } void dfs(int u,int depth){ vis[u] = 1; p[++num] = u; dep[num] = depth; dis[u] = depth; for(int i=head[u];i!=-1;i=e[i].next){ int v = e[i].v; if(!vis[v]){ dfs(v,depth+1); p[++num] = u; dep[num] = depth; } } } void init_rmq(){ int i,j; for(i=1;i<=num;i++){ if(R[p[i]] == -1){ R[p[i]] = i; } } for(i=1;i<=num;i++){ dp[0][i] = i; } int t = (int)(log(num*1.0)/log(2.0)); for(i=1;i<=t;i++){ for(j=1;j+(1<<(i-1))<=num;j++){ int a = dp[i-1][j],b = dp[i-1][j+(1<<(i-1))]; if(dep[a]<=dep[b]){ dp[i][j] = a; } else dp[i][j] = b; } } } int rmq(int u,int v){ int s = R[u],t = R[v]; if(s>t)swap(s,t); int k = (int)(log((t-s+1)*1.0)/log(2.0)); int a = dp[k][s],b = dp[k][t-(1<<k)+1]; if(dep[a]<=dep[b])return p[a]; else return p[b]; } int du[N]; char sta[55],End[55]; int main(){ int t,m; scanf("%d",&t); while(t--){ scanf("%d%d",&n,&m); int i,j; int nn = 1; init(); for(i=0;i<=n;i++)du[i] = 0; for(i=1;i<n;i++){ scanf("%s%s",sta,End); string A = sta; string B = End; if(mpp[A] == NULL) mpp[A] = nn++; if(mpp[B] == NULL) mpp[B] = nn++; du[mpp[A]] = 1; add(mpp[B],mpp[A]); } int root=1; for(i=1;i<=n;i++){ if(!du[i]){ root = i; break; } } num = 0; dep[root] = 0; dfs(root,0); init_rmq(); for(i = 0; i < m; i++) { scanf("%s%s",sta,End); string A = sta; string B = End; if(A == B) { printf("0\n"); continue; } int root1 = rmq(mpp[A],mpp[B]); if(mpp[B] == root1) printf("%d\n",dis[mpp[A]]-dis[root1]); else printf("%d\n",dis[mpp[A]]-dis[root1]+1); } } return 0; }
离线算法,要把所有问题保存起来:
/********************************************** *hdu 2586 *在求解最近公共祖先为问题上,用到的是Tarjan的思想 *从根结点开始形成一棵深搜树,非常好的处理技巧就是 *在回溯到结点u的时候,u的子树已经遍历,这时候才把 *u结点放入合并集合中,这样u结点和所有u的子树中的结 *点的最近公共祖先就是u了,u和还未遍历的所有u的兄弟 *结点及子树中的最近公共祖先就是u的父亲结点。以此类推 *这样我们在对树深度遍历的时候就很自然的将树中的结点 *分成若干的集合,两个集合中的所属不同集合的任意一 *对顶点的公共祖先都是相同的,也就是说这两个集合的最 *近公共最先只有一个。对于每个集合而言可以用并查集来 *优化,时间复杂度就大大降低了,为O(n + q),n为总 *结点数,q为询问结点对数。 ***********************************************/ #include<cstdio> #include<cstring> #include<string> #include<algorithm> #include<map> #include<cmath> using namespace std; #define N 40009 #define M 209 struct edge{ int v; int c; int next; }e[N*2],E[M*2]; int k1,k2; int head1[N],head[N]; int vis[N]; int dis[N]; int fa[N]; int in[N]; int res[M][3];//保存答案 void init(){ memset(head1,-1,sizeof(head1)); memset(head,-1,sizeof(head)); memset(vis,0,sizeof(vis)); memset(dis,0,sizeof(dis)); memset(in,0,sizeof(in)); k1 = k2 = 0; } void add1(int u,int v,int c){ e[k1].v = v; e[k1].next = head1[u]; e[k1].c = c; head1[u] = k1++; e[k1].v = u; e[k1].next = head1[v]; e[k1].c = c; head1[v] = k1++; } void add2(int u,int v,int c){ E[k2].v = v; E[k2].c = c; E[k2].next = head[u]; head[u] = k2++; E[k2].v = u; E[k2].c = c; E[k2].next = head[v]; head[v] = k2++; } int find(int x){ if(x != fa[x]){ return fa[x] = find(fa[x]); } return x; } void tarjan(int u) { vis[u] = 1; fa[u] = u; for(int i = head[u]; i != -1; i = E[i].next) { int v = E[i].v; if(vis[v]) { res[E[i].c][2] = find(v); } } for(int i = head1[u]; i != -1; i = e[i].next) { int v = e[i].v; if(!vis[v]) { dis[v] = dis[u] + e[i].c; tarjan(v); fa[v] = u;//递归出来的时候把子节点指向根节点 } } } int main() { int T; scanf("%d",&T); while(T--) { init(); int n,m; scanf("%d%d",&n,&m); for(int i = 0; i < n-1; i++) { int u,v,c; scanf("%d%d%d",&u,&v,&c); in[v]++; add1(u,v,c); } for(int i = 0; i < m; i++) { int u,v; scanf("%d%d",&u,&v); res[i][0] = u; res[i][1] = v; add2(u,v,i); } for(int i = 1; i <= n; i++) { if(in[i] == 0) { tarjan(i); break; } } for(int i = 0; i < m; i++) { printf("%d\n",dis[res[i][0]] + dis[res[i][1]] - 2*dis[res[i][2]]); } } return 0; }