最近公共祖先问题(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. 《BI那点儿事》数据流转换——字符映射表

    特征映射转换如图展示数据流中列的特征,它的编辑界面很简单,只有一个标签界面,点击要进行映射的列,可以选择需要添加新列或需要更新的列.可以在Output Alias列中给原来的列一个别名,选择要进行的操 ...

  2. JavaScript中的Math对象

    Math对象中提供的计算功能执行起来比你在代码中写的js要快得多,这是它的一个很好的优点.   属性: 属性 说明 Math.E 即常量e的值 Math.LN10 ln10 Math.LN2 ln2 ...

  3. alu features menu

    可以把所有的custom menu和menu link移植到新的环境下,并且增加新的menu link不会吧module变成overridden, 只需到时recreate生成一个新的module包, ...

  4. Openvpn 本地密码验证

    1.修改配置文件.(添加下列配置) auth-user-pass-verify /etc/openvpn/checkpsw.sh via-env #开启用户密码脚本 client-cert-not-r ...

  5. CAD打开文件总是弹出要求选择字体怎么办

    CAD打开文件总是弹出要求选择字体怎么办1.在C:\Documents and Settings\下搜索acad.fmp文件,双击用记事本打开acad.fmp文件,在最后添加内容,上面几行本来就有,不 ...

  6. VIM 代码折叠

    VIM 代码折叠 VIM代码折叠方式可以用"foldmethod"选项来设置,如: set foldmethod=indent 有6种方式来折叠代码 1. manual //手工定 ...

  7. Ubuntu 16.04 更新源

    1/ 在修改source.list前,最好先备份一份 执行备份命令 sudo cp /etc/apt/sources.list /etc/apt/sources.list.old 2/ 执行命令打开s ...

  8. 向JS对象添加和删除事件

    this.removeEventListener = function (obj, ename, func) { var store = obj[this.addEventListener.pre + ...

  9. JVM类加载机制

    一.概述 JVM把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是JVM的类加载机制 二.类加载过程 加载—(验证—准备— ...

  10. 使用Markdown+Pandoc+LaTex+Beamer制作幻灯片

    概述 为什么使用markdown? mardown是一种轻量级的标记语言,语法简单,可读性好,并且容易转化成其他格式的文档, 在技术文档撰写中得到越来越广泛的应用.相信大家对markdown都有一定了 ...