Luogu p3379(LCA)
传送门
题意:
求一颗个节点的树的
题目分析:
复习+学习一下三种不同的求法(特别是根据欧拉序+表求)的方法。
下面简单总结(借鉴)一下的三种求法
代码:
- 树上倍增算法(在线),预处理时间复杂度,每次询问的时间复杂度为
该算法的核心为构建出一个倍增数组,表示第个结点的第个祖先。同时存在一个递推式:
因此我们可以先预处理出来所有的数组,最后对于每个询问,都根据进行跳转,就可以用的时间复杂度求出两个结点的
#include <bits/stdc++.h>
#define maxn 1000005
using namespace std;
const int LOG=20;
struct Node{
int to,next;
}q[maxn];
int head[maxn],cnt=0;
void add_edge(int from,int to){
q[cnt].to=to;
q[cnt].next=head[from];
head[from]=cnt++;
}
struct LCA_ST{
int anc[maxn][LOG],depth[maxn];
void dfs(int x,int fa,int dis){
anc[x][0]=fa;depth[x]=dis;
for(int i=head[x];i!=-1;i=q[i].next){
int to=q[i].to;
if(to==fa) continue;
dfs(to,x,dis+1);
}
}
void init(int root,int n){
dfs(root,-1,1);
for(int j=1;j<LOG;j++){
for(int i=1;i<=n;i++){
anc[i][j]=anc[ anc[i][j-1] ][j-1];
}
}
}
void swim(int &x,int h){
for(int i=0;h>0;i++){
if(h&1)
x=anc[x][i];
h>>=1;
}
}
int query(int x,int y){
if(depth[x]<depth[y]) swap(x,y);
swim(x,depth[x]-depth[y]);
if(x==y) return x;
for(int i=LOG-1;i>=0;i--){
if(anc[x][i]!=anc[y][i]){
x=anc[x][i];
y=anc[y][i];
}
}
return anc[x][0];
}
}lca;
int main()
{
memset(head,-1,sizeof(head));
cnt=0;
int n,m,root;
scanf("%d%d%d",&n,&m,&root);
for(int i=0;i<n-1;i++){
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
lca.init(root,n);
while(m--){
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",lca.query(a,b));
}
return 0;
}
dfs会爆栈?我们也可以用bfs进行预处理
#include <bits/stdc++.h>
#define maxn 1000005
using namespace std;
const int LOG=20;
struct Node{
int to,next;
}q[maxn];
int head[maxn],cnt=0;
void add_edge(int from,int to){
q[cnt].to=to;
q[cnt].next=head[from];
head[from]=cnt++;
}
struct LCA_ST{
int anc[maxn][LOG],depth[maxn];
void bfs(int root){
queue<int>que;
depth[root]=0;
anc[root][0]=root;
que.push(root);
while(!que.empty()){
int x=que.front();
que.pop();
for(int i=1;i<LOG;i++){
anc[x][i]=anc[ anc[x][i-1] ][i-1];
}
for(int i=head[x];i!=-1;i=q[i].next){
int to=q[i].to;
if(to==anc[x][0]) continue;
depth[to]=depth[x]+1;
anc[to][0]=x;
que.push(to);
}
}
}
void swim(int &x,int h){
for(int i=0;h>0;i++){
if(h&1)
x=anc[x][i];
h>>=1;
}
}
int query(int x,int y){
if(depth[x]<depth[y]) swap(x,y);
swim(x,depth[x]-depth[y]);
if(x==y) return x;
for(int i=LOG-1;i>=0;i--){
if(anc[x][i]!=anc[y][i]){
x=anc[x][i];
y=anc[y][i];
}
}
return anc[x][0];
}
}lca;
int main()
{
memset(head,-1,sizeof(head));
cnt=0;
int n,m,root;
scanf("%d%d%d",&n,&m,&root);
for(int i=0;i<n-1;i++){
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
lca.bfs(root);
while(m--){
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",lca.query(a,b));
}
return 0;
}
- 欧拉序+表(在线),预处理时间复杂度为,每次询问的时间复杂度为
个人认为,该算法是三个求解算法中最浅显易懂的算法了。
首先我们需要知道,欧拉序即为:对有根树进行深度优先遍历,无论是递归还是回溯,每次到达一个节点就把编号记录下来,得到一个长度为的序列,成为树 的欧拉序列 。
之后我们再记录一下出现的每个结点第一次出现的位置,最后我们可以发现,等价于在欧拉序列中区间在到之间的深度最小的结点。
因此,此时我们就可以将求解转化成一个问题,继而,我们就可以用表进行维护。
继而预处理的时间复杂度为,而单次的查询可以达到
#include <bits/stdc++.h>
#define maxn 1000005
using namespace std;
const int LOG=20;
struct Node{
int to,next;
}q[maxn<<1];
int head[maxn],cnt=0;
void add_edge(int from,int to){
q[cnt].to=to;
q[cnt].next=head[from];
head[from]=cnt++;
}
struct LCA_ST{
int ST[maxn<<1][LOG];
int value[maxn],depth[maxn<<1],first[maxn],len;
int cal(int x,int y){
return depth[x]<depth[y]?x:y;
}
void dfs(int x,int fa,int dis){
value[++len]=x,depth[len]=dis;
first[x]=len;
for(int i=head[x];i!=-1;i=q[i].next){
int to=q[i].to;
if(to==fa) continue;
dfs(to,x,dis+1);
value[++len]=x;
depth[len]=dis;
}
}
void init(int root){
len=0;
dfs(root,-1,1);
for(int i=1;i<=len;i++) ST[i][0]=i;
for(int j=1;(1<<j)<=len;j++){
for(int i=1;i+(1<<j)-1<=len;i++){
ST[i][j]=cal(ST[i][j-1],ST[i+(1<<(j-1))][j-1]);
}
}
}
int query(int x,int y){
int l=first[x],r=first[y];
if(l>r) swap(l,r);
int k=log2(r-l+1);
return value[cal(ST[l][k],ST[r-(1<<k)+1][k])];
}
}lca;
int main()
{
memset(head,-1,sizeof(head));
cnt=0;
int n,m,root;
scanf("%d%d%d",&n,&m,&root);
for(int i=0;i<n-1;i++){
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
lca.init(root);
while(m--){
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",lca.query(a,b));
}
return 0;
}
- 算法有待总结……