最近公共祖先问题(LCA)是求一颗树上的某两点距离他们最近的公共祖先节点,由于树的特性,树上两点之间路径是唯一的,所以对于很多处理关于树的路径问题的时候为了得知树两点的间的路径,LCA是几乎最有效的解法。

首先是LCA的倍增算法。算法主体是依靠首先对整个树的预处理DFS,用来预处理出每个点的直接父节点,同时可以处理出每个点的深度和与根节点的距离,然后利用类似RMQ的思想处理出每个点的 2 的幂次的祖先节点,这就可以用 nlogn 的时间完成整个预处理的工作。然后每一次求两个点的LCA时只要对两个点深度经行考察,将深度深的那个利用倍增先爬到和浅的同一深度,然后一起一步一步爬直到爬到相同节点,就是LCA了。

具体模板是从鹏神的模板小改来的。

注释方便理解版:

 #include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std; const int maxn=1e5+;
const int maxm=1e5+;
const int maxl=; //总点数的log范围,一般会开稍大一点 int fa[maxl][maxn],dep[maxn],dis[maxn]; //fa[i][j]是j点向上(不包括自己)2**i 层的父节点,dep是某个点的深度(根节点深度为0),dis是节点到根节点的距离
int head[maxn],point[maxm],nxt[maxm],val[maxm],size;
int n; void init(){
size=;
memset(head,-,sizeof(head));
} void add(int a,int b,int v){
point[size]=b;
val[size]=v;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
val[size]=v;
nxt[size]=head[b];
head[b]=size++;
} void Dfs(int s,int pre,int d){ //传入当前节点标号,父亲节点标号,以及当前深度
fa[][s]=pre; //当前节点的上一层父节点是传入的父节点标号
dep[s]=d;
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j==pre)continue;
dis[j]=dis[s]+val[i];
Dfs(j,s,d+);
}
} void Pre(){
dis[]=;
Dfs(,-,);
for(int k=;k+<maxl;++k){ //类似RMQ的做法,处理出点向上2的幂次的祖先。
for(int v=;v<=n;++v){
if(fa[k][v]<)fa[k+][v]=-;
else fa[k+][v]=fa[k][fa[k][v]]; //处理出两倍距离的祖先
}
}
} int Lca(int u,int v){
if(dep[u]>dep[v])swap(u,v); //定u为靠近根的点
for(int k=maxl-;k>=;--k){
if((dep[v]-dep[u])&(<<k)) //根据层数差值的二进制向上找v的父亲
v=fa[k][v];
}
if(u==v)return u; //u为v的根
for(int k=maxl-;k>=;--k){
if(fa[k][u]!=fa[k][v]){ //保持在相等层数,同时上爬寻找相同父节点
u=fa[k][u];
v=fa[k][v];
}
}
return fa[][u]; //u离lca只差一步
}

木有注释版:

 #include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std; const int maxn=1e5+;
const int maxm=1e5+;
const int maxl=; int fa[maxl][maxn],dep[maxn],dis[maxn];
int head[maxn],point[maxm],nxt[maxm],val[maxm],size;
int n; void init(){
size=;
memset(head,-,sizeof(head));
} void add(int a,int b,int v){
point[size]=b;
val[size]=v;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
val[size]=v;
nxt[size]=head[b];
head[b]=size++;
} void Dfs(int s,int pre,int d){
fa[][s]=pre;
dep[s]=d;
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j==pre)continue;
dis[j]=dis[s]+val[i];
Dfs(j,s,d+);
}
} void Pre(){
dis[]=;
Dfs(,-,);
for(int k=;k+<maxl;++k){
for(int v=;v<=n;++v){
if(fa[k][v]<)fa[k+][v]=-;
else fa[k+][v]=fa[k][fa[k][v]];
}
}
} int Lca(int u,int v){
if(dep[u]>dep[v])swap(u,v);
for(int k=maxl-;k>=;--k){
if((dep[v]-dep[u])&(<<k))
v=fa[k][v];
}
if(u==v)return u;
for(int k=maxl-;k>=;--k){
if(fa[k][u]!=fa[k][v]){
u=fa[k][u];
v=fa[k][v];
}
}
return fa[][u];
}

静态树上路径求最小值:LCA倍增

 #include<bits/stdc++.h>
using namespace std; const int maxn=1e6+;
const int maxm=2e6+;
const int maxl=;
const int INF = 0x3f3f3f3f; int fa[maxl][maxn],dep[maxn],dis[maxl][maxn];
int head[maxn],point[maxm],nxt[maxm],val[maxm],size;
int vis[maxn];
int n,q,tmp=INF; void init(){
size=;
memset(head,-,sizeof(head));
memset(vis,,sizeof(vis));
} void add(int a,int b){
point[size]=b;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
nxt[size]=head[b];
head[b]=size++;
} void Dfs(int s,int pre,int d){
fa[][s]=pre;
dis[][s]=s;
dep[s]=d;
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j==pre)continue;
Dfs(j,s,d+);
}
} void Pre(){
Dfs(,-,);
for(int k=;k+<maxl;++k){
for(int v=;v<=n;++v){
if(fa[k][v]<)fa[k+][v]=-;
else fa[k+][v]=fa[k][fa[k][v]];
if(fa[k][v]<)dis[k+][v]=dis[k][v];
else dis[k+][v]=min(dis[k][v],dis[k][fa[k][v]]);
}
}
} int Lca(int u,int v){
tmp = min( u, v );
if(dep[u]>dep[v])swap(u,v);
for(int k=maxl-;k>=;--k){
if((dep[v]-dep[u])&(<<k)){
tmp = min( tmp, dis[k][v]);
v=fa[k][v];
}
}
tmp = min( tmp,v );
if(u==v)return u;
for(int k=maxl-;k>=;--k){
if(fa[k][u]!=fa[k][v]){
tmp=min(tmp,min(dis[k][u],dis[k][v]));
u=fa[k][u],v=fa[k][v];
}
}
tmp = min( tmp, min(u,v));
tmp = min( tmp, fa[][u]);
return fa[][u];
}
//tmp即为u、v路径上的最小值

离线Tarjan的做法主要是防止由于每个点对可能被询问多次,导致每次求都需要 logn 的时间,会超时,所以离线来一并处理所有的询问。

Tarjan的做法是通过递归到最底层,然后开始不断递归回去合并并查集,这样就能够在访问完每个点之后赋值它有关切另一个点已经被访问过的询问。

同样是鹏神的模板修改成自己的代码风格后的。

注释版:

 #include<stdio.h>        //差不多要这些头文件
#include<string.h>
#include<vector>
#include<algorithm>
using namespace std; const int maxn=1e5+; //点数、边数、询问数
const int maxm=2e5+;
const int maxq=1e4+; int n;
int head[maxn],nxt[maxm],point[maxm],val[maxm],size;
int vis[maxn],fa[maxn],dep[maxn],dis[maxn];
int ans[maxq];
vector<pair<int,int> >v[maxn]; //记录询问、问题编号 void init(){
memset(head,-,sizeof(head));
size=;
memset(vis,,sizeof(vis));
for(int i=;i<=n;++i){
v[i].clear();
fa[i]=i;
}
dis[]=dep[]=;
} void add(int a,int b,int v){
point[size]=b;
val[size]=v;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
val[size]=v;
nxt[size]=head[b];
head[b]=size++;
} int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
} void Tarjan(int s,int pre){
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j!=pre){
dis[j]=dis[s]+val[i];
dep[j]=dep[s]+;
Tarjan(j,s); //这里Tarjan的DPS操作必须在并查集合并之前,这样才能保证求lca的时候lca是每一小部分合并时的祖先节点,如果顺序交换,那么所有的查询都会得到 1 节点,就是错误的
int x=find(j),y=find(s);
if(x!=y)fa[x]=y;
}
}
vis[s]=;
for(int i=;i<v[s].size();++i){
int j=v[s][i].first;
if(vis[j]){
int lca=find(j);
int id=v[s][i].second;
ans[id]=lca; //这里视题目要求给答案赋值
// ans[id]=dep[s]+dep[j]-2*dep[lca];
// ans[id]=dis[s]+dis[j]-2*dis[lca];
}
}
} for(int i=;i<=k;++i){ //主函数中的主要部分
int a,b;
scanf("%d%d",&a,&b);
v[a].push_back(make_pair(b,i)); //加问题的时候两个点都要加一次
v[b].push_back(make_pair(a,i));
}
Tarjan(,);

木有注释版:

 #include<stdio.h>
#include<string.h>
#include<vector>
#include<algorithm>
using namespace std; const int maxn=1e5+;
const int maxm=2e5+;
const int maxq=1e4+; int n;
int head[maxn],nxt[maxm],point[maxm],val[maxm],size;
int vis[maxn],fa[maxn],dep[maxn],dis[maxn];
int ans[maxq];
vector<pair<int,int> >v[maxn]; void init(){
memset(head,-,sizeof(head));
size=;
memset(vis,,sizeof(vis));
for(int i=;i<=n;++i){
v[i].clear();
fa[i]=i;
}
dis[]=dep[]=;
} void add(int a,int b,int v){
point[size]=b;
val[size]=v;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
val[size]=v;
nxt[size]=head[b];
head[b]=size++;
} int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
} void Tarjan(int s,int pre){
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j!=pre){
dis[j]=dis[s]+val[i];
dep[j]=dep[s]+;
Tarjan(j,s);
int x=find(j),y=find(s);
if(x!=y)fa[x]=y;
}
}
vis[s]=;
for(int i=;i<v[s].size();++i){
int j=v[s][i].first;
if(vis[j]){
int lca=find(j);
int id=v[s][i].second;
ans[id]=lca;
// ans[id]=dep[s]+dep[j]-2*dep[lca];
// ans[id]=dis[s]+dis[j]-2*dis[lca];
}
}
} for(int i=;i<=k;++i){
int a,b;
scanf("%d%d",&a,&b);
v[a].push_back(make_pair(b,i));
v[b].push_back(make_pair(a,i));
}
Tarjan(,);

另外,现在又有LCA用dfs序+RMQ的做法,可以实现O(nlogn)预处理,O(1)查询的LCA,基本可以完全替代倍增LCA和TarjanLCA,但是树上路径长度和树上路径最小值无法用这个来做。

 #include <bits/stdc++.h>
using namespace std; const int maxn = 2e5+;
const int maxl = ;
int vis[maxn],dep[maxn],dp[maxn][maxl]; int head[maxn],in[maxn],id[maxn];
int point[maxn],nxt[maxn],sz;
int val[maxn];
int fa[maxl][maxn]; //fa[i][j]是j点向上(不包括自己)2**i 层的父节点,dep是某个点的深度(根节点深度为0),dis是节点到根节点的距离
int n; void init(){
sz = ;
memset(head,-,sizeof(head));
memset(fa,-,sizeof(fa));
} void Pre(){
for(int k=;k+<maxl;++k){ //类似RMQ的做法,处理出点向上2的幂次的祖先。
for(int v=;v<=n;++v){
if(fa[k][v]<)fa[k+][v]=-;
else fa[k+][v]=fa[k][fa[k][v]]; //处理出两倍距离的祖先
}
}
} void dfs(int u,int p,int d,int&k){
fa[][u]=p; //当前节点的上一层父节点是传入的父节点标号
vis[k] = u;
id[u] = k;
dep[k++]=d;
for(int i = head[u];~i;i=nxt[i]){
int v = point[i];
if(v == p)continue;
dfs(v,u,d+,k);
vis[k] = u;
dep[k++]=d;
}
} void RMQ(int root){
int k = ;
dfs(root,-,,k);
int m = k;
int e= (int)(log2(m+1.0));
for(int i = ; i < m ; ++ i)dp[i][]=i;
for(int j = ; j <= e ; ++ j){
for(int i = ; i + ( << j ) - < m ; ++ i){
int N = i + (<<(j-));
if(dep[dp[i][j-]] < dep[dp[N][j-]]){
dp[i][j] = dp[i][j-];
}
else dp[i][j] = dp[N][j-];
}
}
} void add(int a,int b){
point[sz] = b;
nxt[sz] = head[a];
head[a] = sz++;
} int LCA(int u,int v){
int left = min(id[u],id[v]),right = max(id[u],id[v]);
int k = (int)(log2(right- left+1.0));
int pos,N = right - (<<k)+;
if(dep[dp[left][k]] < dep[dp[N][k]])pos = dp[left][k];
else pos = dp[N][k];
return vis[pos];
} int q; inline int get(int a,int k){
int res = a;
for(int i = ; (1ll << i ) <= k ; ++ i){
if(k&(1ll<<i)){
res = fa[i][res];
}
}
return res;
} void run(){
while(q--){
int a,b,k;
scanf("%d%d%d",&a,&b,&k);
int lca = LCA(a,b);
int num = dep[id[a]] - dep[id[lca]] + dep[id[b]] - dep[id[lca]] + ;
int up = (num - )%k;
int ans = val[a];
// printf("a : %d\n",a);
while(dep[id[a]] - dep[id[lca]] >= k){
int Id = get(a,k);
ans ^= val[Id];
// printf("a : %d\n",Id);
a = Id;
}
if(dep[id[b]] - dep[id[lca]] > up){
// printf("up: %d\n",up);
if(up == )ans^= val[b];
b = get(b,up);
while(dep[id[b]] - dep[id[lca]] >k ){
int Id = get(b,k);
ans ^= val[Id];
b = Id;
}
}
printf("%d\n",ans);
}
} int main(){
while(scanf("%d%d",&n,&q)!=EOF){ init();
for(int i = ; i < n; ++ i){
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
add(b,a);
}
for(int i = ;i <= n ; ++ i)scanf("%d",&val[i]);
RMQ();
Pre();
run(); }
return ;
}

图论--最近公共祖先问题(LCA)模板的更多相关文章

  1. 洛谷P3379 【模板】最近公共祖先(LCA)

    P3379 [模板]最近公共祖先(LCA) 152通过 532提交 题目提供者HansBug 标签 难度普及+/提高 提交  讨论  题解 最新讨论 为什么还是超时.... 倍增怎么70!!题解好像有 ...

  2. P3379 【模板】最近公共祖先(LCA)

    P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询 ...

  3. 洛谷P3379 【模板】最近公共祖先(LCA)(dfs序+倍增)

    P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询 ...

  4. 「LuoguP3379」 【模板】最近公共祖先(LCA)

    题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询问的个数和树根结点的序号. 接下来N-1行每 ...

  5. 洛谷——P3379 【模板】最近公共祖先(LCA)

    P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询 ...

  6. LCA 最近公共祖先 (笔记、模板)

    求lca的方法大体有三种: 1.dfs+RMQ(线段树 ST表什么的) 在线 2.倍增 在线 3.tarjan 离线 ps:离线:所有查询全输入后一次解决 在线:有一个查询输出一次 以下模板题为 洛谷 ...

  7. luogo p3379 【模板】最近公共祖先(LCA)

    [模板]最近公共祖先(LCA) 题意 给一个树,然后多次询问(a,b)的LCA 模板(主要参考一些大佬的模板) #include<bits/stdc++.h> //自己的2点:树的邻接链表 ...

  8. 【原创】洛谷 LUOGU P3379 【模板】最近公共祖先(LCA) -> 倍增

    P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询 ...

  9. P3379 【模板】最近公共祖先(LCA)(欧拉序+rmq)

    P3379 [模板]最近公共祖先(LCA) 用欧拉序$+rmq$维护的$lca$可以做到$O(nlogn)$预处理,$O(1)$查询 从这里剻个图 #include<iostream> # ...

  10. 最近公共祖先(LCA)学习笔记 | P3379 【模板】最近公共祖先(LCA)题解

    研究了LCA,写篇笔记记录一下. 讲解使用例题 P3379 [模板]最近公共祖先(LCA). 什么是LCA 最近公共祖先简称 LCA(Lowest Common Ancestor).两个节点的最近公共 ...

随机推荐

  1. Web API开发实例——对产品Product进行增删改查

    1.WebApi是什么 ASP.NET Web API 是一种框架,用于轻松构建可以由多种客户端(包括浏览器和移动设备)访问的 HTTP 服务.ASP.NET Web API 是一种用于在 .NET ...

  2. 浅谈AJAX的基本原理和原生AJAX的基础用法

    一.什么是AJAX? AJAX,即"Asynchronous Javascript And XML",翻译为异步的JavaScript和XML,是一种创建交互式网页应用的网页开发技 ...

  3. python核心编程第六章练习6-12

    6-12.字符串.(a)创建一个名字为findchr()的函数,函数声明如下.def findchr(string, char)findchr()要在字符串string中查找字符char,找到就返回该 ...

  4. eclise 部署web工程报 There are no resources that can be added or removed from the server.

    该文章转自: http://blog.csdn.net/dw_java08/article/details/7789601 eclise 部署web工程报 There are no resources ...

  5. oracle with as

    http://blog.csdn.net/a9529lty/article/details/4923957/

  6. 深入理解JS闭包

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量 ...

  7. hadoop输入格式(InputFormat)

    InputFormat接口(package org.apache.hadoop.mapreduce包中)里包括两个方法:getSplits()和createRecordReader(),这两个方法分别 ...

  8. OMG点菜系统

    算是一个项目吧,自己一个人也写了很久,有很多东西是自己写的,当然也有在网上借鉴人家大神的,不一而足,代码太多,不贴了,直接上图.(一直出现的天蓝色是使用windows自带主题壁纸上截取的一段) [原本 ...

  9. AngularJs的UI组件ui-Bootstrap分享(五)——Pager和Pagination

    ui-bootstrap中有两个分页控件,一个是轻量级的Pager,只有上一页和下一页的功能,另一个是功能完整的Pagination,除了上一页和下一页,还可以选择首页和最后页,并且支持多种页数的显示 ...

  10. Android SQLiteOpenHelper(一)

    SQLiteOpenHelper api解释: A helper class to manage database creation and version management. You creat ...