LCA

LCA

基本概念:

给定一棵有根树,若节点z既是节点x的祖先,也是节点y的祖先,则称z是x,y的公共祖先。在x,y的所有公共祖先中,深度最大的一个称为x,y的最近公共祖先,记为LCA(x,y)。
  LCA(x,y)是x到根的路径与y到根的路径的交汇点。它也是x与y之间的路径上深度最小的节点。求最近公共祖先的方法通常有五种。

法一:(暴力)O(nm)

  dfs求出每个点深度

  从x向上走到根节点,并标记所有经过的节点。
  从y向上走到根节点,当第一次遇到已标记的节点时,就找到了LCA(x,y)。
  对于每个询问,向上标记法的时间复杂度最坏为O(n)

法二:倍增 O( (n+m)logn )在线

设$ f[x][k]$表示x的\(2^k\)辈祖先,即从x向根节点走\(2^k\)步到达的节点
特别的,若该节点不存在,则令$ f[x][k]=0 \(; \) f[x][0] $是该节点的父节点;
除此之外,\(1<=k<=logn,f[x][k]=f[ f[x][k-1] ][ k-1 ].\) ———— $ 2k=2*2^{k-1} $

这类似于一个动态规划的过程,阶段就是节点的深度。

因此,我们可以对树进行广度优先遍历,得到f[x][0],再计算f数组中所有的值。

以上预处理 复杂度O(nlogn)

之后可以多次对不同x,y计算lca 每次询问复杂度O(logn)

#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 500010
using namespace std;
inline void read(int &x){
    x=0;int f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0',ch=getchar();}x*=f;
}

int n,m,s,cnt,tot,ans=0,hd[N],dis[N];
int dep[N],f[N][25],log[N];
bool vis[N];
struct edge{
    int nxt,v;
}e[N<<1];

void add(int u,int v){
    e[++tot].v=v;
    e[tot].nxt=hd[u];
    hd[u]=tot;
}

void dfs(int now,int fa){
    f[now][0]=fa;
    dep[now]=dep[fa]+1;
    for(int i=1;i<=log[dep[now]];i++)
        f[now][i]=f[f[now][i-1]][i-1];//now的2^i祖先等于now的2^(i-1)祖先的2^(i-1)祖先
    for(int i=hd[now];i;i=e[i].nxt)
        if(e[i].v!=fa) dfs(e[i].v,now);
}

int lca(int x,int y){
    if(dep[x]<dep[y]) swap(x,y);//不妨设dep[x]≥dep[y]
    while(dep[x]>dep[y])
        x=f[x][log[dep[x]-dep[y]]-1];//用二进制拆分思想,把x向上调整到于y同一深度
    if(x==y) return x;//说明已经找到了LCA,LCA就是x
    for(int k=log[dep[x]]-1;k>=0;k--)
        if(f[x][k]!=f[y][k])
            x=f[x][k],y=f[y][k];//用二进制拆分思想,把x,y同时向上调整,并保持深度一致且二者不相汇
    return f[x][0];//此时x,y必定只差一步就相会了,他们的父节点f[x][0]就是LCA
}

int main(){
    read(n),read(m),read(s);
    for(int i=1;i<n;i++){
        int x,y;
        read(x),read(y);
        add(x,y);add(y,x);
    }
    for(int i=1;i<=n;i++)
        log[i]=log[i>>1]+1;//
    dfs(s,0);
    for(int i=1;i<=m;i++){
        int x,y;
        read(x),read(y);
        printf("%d\n",lca(x,y));
    }
    return 0;
}

法三:Tarjan O(n+m) 离线

这里有篇超详尽的blog

本质是用并查集对暴力向上标记进行优化

基本思路:

      1.任选一个点为根节点,从根节点开始。

      2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。

      3.若是v还有子节点,返回2,否则下一步。

      4.合并v到u上。

      5.寻找与当前点u有询问关系的点v。

      6.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。

在深度优先遍历的任意时刻,树中节点分为三类:
  (1)----已经访问完毕并且回溯的节点。在这些节点上标记一个整数2。
  (2)----已经开始递归,但尚未回溯的节点。这些节点就是当前正在访问的节点x以及x的祖先。在这些节点上标记一个整数1。
  (3)----尚未访问的节点。这些节点没有标记。

对于正在访问的节点x,它到根节点的路径已经标记为1。若y是已经访问完毕并且回溯的节点,则LCA(x,y)就是从y向上走到根,第一个遇到的标记为1的节点。

可以利用并查集进行优化,当一个节点获得整数2的标记时,把它所在的集合合并到它的父节点所在的集合中(合并时它的父节点标记一定为1,且单独构成一个集合)。

这相当于每个完成回溯的节点都有一个指针指向它的父节点,只需查询y所在集合的代表元素(并查集的get操作),就等价于从y向上一直走到一个开始递归但尚未回溯的节点(具有标记1),即LCA(x,y)。

此时扫描与x相关的所有询问,若询问当中的另一个点y的标记为2,就知道了该询问的回答应该是y在并查集中的代表元素(get(y)函数的结果)。

#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
inline int read() {
    int x=0;char ch=getchar();
    while(!isdigit(ch)) ch=getchar();
    while(isdigit(ch)) {x=x*10+ch-'0';ch=getchar();}
    return x;
}
const int N=500005;
int n,m,root;
int hd[N<<1],nxt[N<<1],to[N<<1],tot;
inline void add(int x,int y) {
    to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
vector< pair<int,int> >ask[N];
int fa[N];
inline int find(int x) {
    return x==fa[x]?x:fa[x]=find(fa[x]);
}
int vis[N],ans[N];
void tarjan(int x) {
    vis[x]=1;
    for(int i=hd[x];i;i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        tarjan(y);
        fa[y]=x;
    }
    for(int i=0;i<ask[x].size();i++) {
        int y=ask[x][i].first,id=ask[x][i].second;
        if(vis[y]==2) ans[id]=find(y);
    }
    vis[x]=2;
}
int main() {
    n=read();m=read();root=read();
    for(int i=1,x,y;i<n;i++) {
        x=read();y=read();
        add(x,y),add(y,x);
    }
    for(int i=1,x,y;i<=m;i++) {
        x=read();y=read();
        ask[x].push_back(make_pair(y,i));
        ask[y].push_back(make_pair(x,i));
    }
    for(int i=1;i<=n;i++) fa[i]=i;
    tarjan(root);
    for(int i=1;i<=m;i++)
        printf("%d\n",ans[i]);
    return 0;
}

法四:欧拉序转化为RMQ问题,复杂度 O(m+nlog⁡n) ,每次询问的复杂度为O(1), 在线

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
typedef long long ll;
const int N=1000006;
inline int read() {
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return f*x;
}
int n,m,rt;
int hd[N],nxt[N<<1],to[N<<1],tot;
inline void add(int x,int y) {
	to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
	to[++tot]=x;nxt[tot]=hd[y];hd[y]=tot;
}
int st[N][20],lg[N],dfn_cnt,dfn[N],dep[N],euler[N];
inline void dfs(int x,int fa,int d) {
	dep[x]=d;
	dfn[x]=++dfn_cnt;euler[dfn_cnt]=x;
	for(int i=hd[x];i;i=nxt[i]) 
		if(to[i]!=fa) {
			dfs(to[i],x,d+1);
			euler[++dfn_cnt]=x;
		}
}
inline int Min(int x,int y) {
	return dep[x]<dep[y]?x:y; 
}
inline int LCA(int x,int y) {
	int L=dfn[x]<dfn[y]?dfn[x]:dfn[y],R=dfn[x]>dfn[y]?dfn[x]:dfn[y];
	int k=lg[R-L+1];
	return Min(st[L][k],st[R-(1<<k)+1][k]);
}
int main() {
	n=read();m=read();rt=read();
	for(int i=1;i<n;i++)	
		add(read(),read());
	dfs(rt,0,1);
	lg[0]=-1;
	for(int i=1;i<=dfn_cnt;i++) lg[i]=lg[i>>1]+1;
	for(int i=1;i<=dfn_cnt;i++) st[i][0]=euler[i];
	for(int j=1;(1<<j)<=dfn_cnt;j++)
		for(int i=1;i+(1<<j)-1<=dfn_cnt;i++)
			st[i][j]=Min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
	for(int i=1,a,b;i<=m;i++) {
		a=read();b=read();
		printf("%d\n",LCA(a,b));
	}
	return 0;
}

法五:树链剖分,O(2n+mlogn) 在线

和倍增想法类似,其实就是往上跳

参考blog

树剖要用到两次dfs,都是O(n)的复杂度

然后如果图是满二叉树的话是最坏情况,但此时查询也是O(logn)的

【显然最坏情况每次一步步取f[top[x]]走,走下来是一个树的深度,也就是一个logn】

所以理论复杂度上界就是O(2n+mlogn)

而且关键的是树剖常数还比倍增要小。。。

树剖就是把树剖分成若干条不相交的链,目前常用做法是剖成轻重链

所以我们定义siz[x]为以x为根结点的子树的结点个数

对于每个结点x,在它的所有子结点中寻找一个结点y

使得对于y的兄弟节点z,都有siz[y]≥siz[z]

此时x就有一条重边连向y,有若干条轻边连向他的其他子结点【比如z】

这样的话,树上的不在重链上的边的数量就会大大减少

然后我们每次求LCA(x,y)的时候就可以判断两点是否在同一链上

如果两点在同一条链上我们只要找到这两点中深度较小的点输出就行了

如果两点不在同一条链上

那就找到深度较大的点令它等于它所在的重链链端的父节点即为x=f[top[x]]

直到两点到达同一条链上,输出两点中深度较小的点

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N=1000002;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
int n,m,root,ans[N];
struct edge{
    int to,nxt;
}e[N];
int hd[N],tot;
inline void add(int x,int y){
    e[++tot].to=y;e[tot].nxt=hd[x];hd[x]=tot;
}   

int son[N],top[N],dep[N],siz[N],fa[N];
void dfs_son(int x,int f){
    siz[x]=1;fa[x]=f;dep[x]=dep[f]+1;
    for(int i=hd[x];i;i=e[i].nxt){
        int y=e[i].to;
        if(y==f) continue;
        dfs_son(y,x);
        siz[x]+=siz[y];
        if(siz[y]>siz[son[x]]) son[x]=y;
    }
}

void dfs_chain(int x,int tp){
    top[x]=tp;
    if(son[x]) dfs_chain(son[x],tp);
    for(int i=hd[x];i;i=e[i].nxt){
        int y=e[i].to;
        if(y==fa[x]||y==son[x])continue;
        dfs_chain(y,y);
    }
}

int main(){
    n=read();m=read();root=read();
    for(int i=1,x,y;i<n;i++){
        x=read();y=read();
        add(x,y);add(y,x);
    }
    dfs_son(root,0);
    dfs_chain(root,root);
    for(int i=1,x,y;i<=m;i++){
        x=read();y=read();
        while(top[x]!=top[y]){
            if(dep[top[x]]>dep[top[y]]) x=fa[top[x]];
            else y=fa[top[y]];
        }
         printf("%d\n",dep[x]<dep[y]?x:y);
    }
    return 0;
}

例题:异象石

按时间戳从小到大,有异象石的相邻两点路径累加就是答案的两倍,所以我们只需LCA求树上两点间距离,然后用个set维护时间戳,每次+操作就 在set里查找dfn[x]的前驱和后继,然后ans 减去原来两点间路径,加上新的 L 和 x,x和R,‘-’ 操作同理

‘?’直接输出ans即可

#include <set>
#include <queue>
#include <vector>
#include <cstdio>
#include <utility>
#include <cstring>
#include <iostream>
#include <algorithm>
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return f*x;
}
using namespace std;
const int N=100005; 
const int M=200005; 
long long ans;
int n,m;
int hd[M],nxt[M],to[M],tot,w[M];
inline void add(int x,int y,int z) {
    to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
int fa[N],siz[N],son[N],dep[N];
long long dis[N];
void dfs_son(int x,int f) {
    siz[x]=1;fa[x]=f;dep[x]=dep[f]+1;
    for(int i=hd[x];i;i=nxt[i]) {
        int y=to[i];
        if(y==f) continue;
        dis[y]=dis[x]+w[i];
        dfs_son(y,x);
        siz[x]+=siz[y];
        if(siz[y]>siz[son[x]]) son[x]=y;
    }
}
int dfn[N],rev[N],top[N],dfn_cnt;
void dfs_chain(int x,int tp) {
    dfn[x]=++dfn_cnt;rev[dfn_cnt]=x;
    top[x]=tp;
    if(son[x]) dfs_chain(son[x],tp);
    for(int i=hd[x];i;i=nxt[i]) {
        int y=to[i];
        if(dfn[y]) continue;
        dfs_chain(y,y);
    }
}
long long path(int x,int y) {
    long long ans=dis[x]+dis[y];
    while(top[x]!=top[y]) {
        if(dep[top[x]]<dep[top[y]]) swap(x,y);
        x=fa[top[x]];
    }
    return ans-(dep[x]<dep[y]?dis[x]:dis[y])*2;
}
set<int>s;

char op;
int main() {
    n=read();
    for(int i=1,x,y,z;i<n;i++) {
        x=read();y=read();z=read();
        add(x,y,z);
        add(y,x,z);
    }
    dfs_son(1,0);dfs_chain(1,1);
    m=read();
    int x;
    for(int i=1;i<=m;i++) {
        cin>>op;
        if(op=='+') {
            x=read();
            if(s.empty()) {s.insert(dfn[x]);continue;}
            set<int>::iterator l=--s.lower_bound(dfn[x]);
            set<int>::iterator r=s.lower_bound(dfn[x]);
            if(r==s.end()) r=s.begin();
            if(r==s.begin()) l=--s.end();
            ans-=path(rev[*l],rev[*r]);
            ans+=path(rev[*l],x);
            ans+=path(x,rev[*r]);
            s.insert(dfn[x]);
        } else if(op=='-') {
            x=read();
            s.erase(dfn[x]);
            if(s.empty()) continue;
            set<int>::iterator l=--s.lower_bound(dfn[x]);
            set<int>::iterator r=s.lower_bound(dfn[x]);
            if(r==s.end()) r=s.begin();
            if(r==s.begin()) l=--s.end();
            ans+=path(rev[*l],rev[*r]);
            ans-=path(rev[*l],x);
            ans-=path(x,rev[*r]);
        } else 
            printf("%lld\n",ans>>1);
    }
    return 0;
}

Cow Politics

可以证明(或者感性理解)最远的距离的一端一定是最深的属于这个a[i]的点

证明:

https://www.luogu.com.cn/blog/stevenmeng/solution-p2971

#include <queue>
#include <cmath>
#include <cstdio>
#include <vector>
#include <cstring>
#include <utility>
#include <iostream>
#include <algorithm>

using namespace std;
const int N=200005;
inline int read() {
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return f*x;
}
int n,m;
int fa[N];
int hd[N],nxt[N<<1],to[N<<1],tot;
inline void add(int x,int y) {
	to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
int siz[N],dep[N],son[N];
void dfs_son(int x) {
	siz[x]=1;
	dep[x]=dep[fa[x]]+1;
	for(int i=hd[x];i;i=nxt[i]) {
		int y=to[i];
		if(y==fa[x]) continue;
		dfs_son(y);
		siz[x]+=siz[y];
		if(siz[y]>siz[son[x]]) son[x]=y;
	}
}
int dfn[N],dfn_cnt,top[N];
void dfs_chain(int x,int tp) {
	top[x]=tp;
	dfn[x]=++dfn_cnt;
	if(son[x]) dfs_chain(son[x],tp);
	for(int i=hd[x];i;i=nxt[i]) {
		int y=to[i];
		if(y==fa[x]||y==son[x]) continue;
		dfs_chain(y,y);
	}
}
inline int LCA(int x,int y) {
	while(top[x]!=top[y]) {
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	return dfn[x]<dfn[y]?x:y;
}
inline int dist(int x,int y) {
	return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
vector<int>v[N];
int root;
inline void Max(int &x,int y){if(x<y)x=y;}
int main() {
	n=read();m=read();
	for(int i=1;i<=n;i++) {
		v[read()].push_back(i);fa[i]=read();
		if(fa[i]) add(fa[i],i);
		else root=i;
	}
	dfs_son(root);
	dfs_chain(root,root);
	for(int i=1;i<=m;i++) {
		int ans=0,mxdeep=0;
		for(auto x:v[i]) 
			if(dep[x]>dep[mxdeep])
				mxdeep=x;
		for(auto x:v[i]) 
			Max(ans,dist(x,mxdeep));
		printf("%d\n",ans);
	}
	return 0;
}


posted @ 2020-10-09 10:28  ke_xin  阅读(32)  评论(0编辑  收藏  举报