图论--最近公共祖先问题(LCA)模板
最近公共祖先问题(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)模板的更多相关文章
- 洛谷P3379 【模板】最近公共祖先(LCA)
P3379 [模板]最近公共祖先(LCA) 152通过 532提交 题目提供者HansBug 标签 难度普及+/提高 提交 讨论 题解 最新讨论 为什么还是超时.... 倍增怎么70!!题解好像有 ...
- P3379 【模板】最近公共祖先(LCA)
P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询 ...
- 洛谷P3379 【模板】最近公共祖先(LCA)(dfs序+倍增)
P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询 ...
- 「LuoguP3379」 【模板】最近公共祖先(LCA)
题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询问的个数和树根结点的序号. 接下来N-1行每 ...
- 洛谷——P3379 【模板】最近公共祖先(LCA)
P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询 ...
- LCA 最近公共祖先 (笔记、模板)
求lca的方法大体有三种: 1.dfs+RMQ(线段树 ST表什么的) 在线 2.倍增 在线 3.tarjan 离线 ps:离线:所有查询全输入后一次解决 在线:有一个查询输出一次 以下模板题为 洛谷 ...
- luogo p3379 【模板】最近公共祖先(LCA)
[模板]最近公共祖先(LCA) 题意 给一个树,然后多次询问(a,b)的LCA 模板(主要参考一些大佬的模板) #include<bits/stdc++.h> //自己的2点:树的邻接链表 ...
- 【原创】洛谷 LUOGU P3379 【模板】最近公共祖先(LCA) -> 倍增
P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询 ...
- P3379 【模板】最近公共祖先(LCA)(欧拉序+rmq)
P3379 [模板]最近公共祖先(LCA) 用欧拉序$+rmq$维护的$lca$可以做到$O(nlogn)$预处理,$O(1)$查询 从这里剻个图 #include<iostream> # ...
- 最近公共祖先(LCA)学习笔记 | P3379 【模板】最近公共祖先(LCA)题解
研究了LCA,写篇笔记记录一下. 讲解使用例题 P3379 [模板]最近公共祖先(LCA). 什么是LCA 最近公共祖先简称 LCA(Lowest Common Ancestor).两个节点的最近公共 ...
随机推荐
- 深入浅出设计模式——解释器模式(Interpreter Pattern)
模式动机 如果在系统中某一特定类型的问题发生的频率很高,此时可以考虑将这些问题的实例表述为一个语言中的句子,因此可以构建一个解释器,该解释器通过解释这些句子来解决这些问题.解释器模式描述了如何构成一个 ...
- nodejs搭建http-server
很多时候我们都需要搭建一个简单的服务器,部署在IIS,阿帕奇,或者用nodejs,网上很多关于nodejs搭建server的文章,但都是要创建server.js,很麻烦, 在这里我分享一个创建ht ...
- TCP连接的状态与关闭方式及其对Server与Client的影响
TCP连接的状态与关闭方式及其对Server与Client的影响 1. TCP连接的状态 首先介绍一下TCP连接建立与关闭过程中的状态.TCP连接过程是状态的转换,促使状态发生转换的因素包括用户调用. ...
- html/CSS基础知识回顾
html部分 块级元素: 一般用来搭建网站架构,布局,装载内容...像这些大体力的活都属于块级元素.它包括以下标签: address,blockquote,center,dir, div, dl, d ...
- Vim入门教程
尽管网上有成打的Vim在线教程,但是要么艰深晦涩,要么太过肤浅.本教程的目标让每个阶段都有斩获,从理解它的哲学(将和你终身相伴)到超越现在编辑技巧,成为其中的牛人. 简单来说,本教程的学习方式将使你终 ...
- 【转载】协同过滤 & Spark机器学习实战
因为协同过滤内容比较多,就新开一篇文章啦~~ 聚类和线性回归的实战,可以看:http://www.cnblogs.com/charlesblc/p/6159187.html 协同过滤实战,仍然参考:h ...
- html,css,js加载顺序
1.js放在head中会立即执行,阻塞后续的资源下载与执行.因为js有可能会修改dom,如果不阻塞后续的资源下载,dom的操作顺序不可控. 正常的网页加载流程是这样的. 浏览器一边下载HTML网页,一 ...
- D3.js 用层画条形图
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- js中面向对象
1.对象的表示方法,以下是对象的两种方法:第二种方法是使用函数构造器来创建一个对象. 2.对象的一种表达方式,这种方式更像Java中对象的创建,就是用一个new来创建一个对象实例.面向对象的封装.样式 ...
- Android DisplayMetrics类获取屏幕大小
DisplayMetrics public class DisplayMetrics extends Object java.lang.Object ↳ android.util.Disp ...