lca小结

LCA(最近公共祖先)

先有一棵树,选两个点向上爬相遇的第一个点就是这两个点的lca,如2是1和7的lca,1是6和5的lca。理解了吧。

一棵树

理解倒是很简单,现在来讲实现方法(四种)。

根据一道模板题来讲吧

见百练(1986:Distance Queries)

一般的lca题都是求边权和,因为他们懒得给根为了让你随便选一个当根。

一.暴力搜索

两个先到同一深度,再一起向上爬,时间复杂度贼高(每次操作O(n)),但简单易懂。

见代码(我没打,借的同学的(@PI_RE-fsy))注:会超时,且不保证AC,重在理解。

#include <cstdio>
#include <vector>
using namespace std;
#define N 40000
#define reg register
 
inline void read (int &x){
    x = 0;
    char c = getchar ();
    while (c < '0' || c > '9')
        c = getchar ();
    while (c >= '0' && c <= '9'){
        x = (x << 1) + (x << 3) + c - 48;
        c = getchar ();
    }
}
 
inline void print (int x){
    if (x / 10)
        print (x / 10);
    putchar (x % 10 + 48);
}
 
struct edge{
    int v, w;
    edge (){};
    edge (int V, int W){
        v = V;
        w = W;
    }
};
int n, m, k, fa[N + 5], dep[N + 5], dis[N + 5];
vector <edge> G[N + 5];
 
inline void dfs (int u, int f){//铺路,算出深度和权值(到根的距离)
    for (reg int i = 0; i < G[u].size(); i++){
        int v = G[u][i].v;
        if (v == f) continue;
        fa[v] = u;
        dep[v] = dep[u] + 1;
        dis[v] = dis[u] + G[u][i].w;
        dfs (v, u);
    }
}
 
inline int LCA (int u, int v){
    while (dep[u] > dep[v]) u = fa[u];//调整深度
    while (dep[v] > dep[u]) v = fa[v];
    if (u == v) return u;
    while (u != v){//一起爬
        u = fa[u];
        v = fa[v];
    }
    return u;
}
 
int main (){
    read (n); read (m);
    for (reg int i = 1; i <= m; i++){
        int u, v, w;
        read (u); read (v); read (w);
        G[u].push_back (edge (v, w));
        G[v].push_back (edge (u, w));
    }
    fa[1] = 1, dep[1] = 0, dis[1] = 0;
    dfs (1, 0);
    read (k);
    while (k --){
        int u, v;
        read (u); read (v);
        int lca = LCA (u, v);
        print (dis[u] + dis[v] - dis[lca] * 2);//类似于尺取法
        putchar (10);
    }
}

二.dfs序(欧拉序)+rmq(区间维护最(大/小(该题用深度最浅))值)

普及一下dfs序:顾名思义,就是dfs的遍历顺序。(注:为满足求祖先,返回到某点,yeya也要记录一次,如开篇的那棵树的dfs序就是1262721343531。

而两点的lca就是这两点第一次出现在序列上所框出的这个区间中深度最小的点。这是可考证的,因为其中一点必须经过这个点才能搜到另一个点,所以就是LCA。

理解到这里,就更头痛le,因为rmq也有很多维护方法,列举两种(st表和线段树),复杂度自己取舍,一般差不多

线段树大家都会,如若不会,可查看我日后会写的小结复杂度(每次操作log(n))。

st表在这里说一下,是一种dp:dp(i,j)表示由i到i+2^(j)这段区间中最浅度(预处理nlogn,每次O(1))。

转换方程:dp(i,j)=max(dp(i,j-1),dp(i+pow(j-1),j-1));

见线段树代码

#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
#define M 40050
inline void read(int &x) {
	x = 0;
	int f = 1;
	char s = getchar();
	while (s < '0' || s > '9') {
		if (s == '-')
			f = -1;
		s = getchar();
	}
	while (s >= '0' && s<='9') {
		x = x * 10 + s - '0';
		s = getchar();
	}
	x *= f;
}
struct node {
	int v,e;
	node(int U,int E) {
		v=U,e=E;
	}
};
vector<node>G[M];
int n,m,dep[M],ver[M],que[M*2],tot,id[M],rmq[(M*2)<<2];
bool vis[M];
void built(int x,int deep) {
	vis[x]=1;
	que[++tot]=x;
	id[x]=tot;
	dep[x]=deep;
	for(int i=0; i<G[x].size(); i++) {
		if(!vis[G[x][i].v]) {
			ver[G[x][i].v]=ver[x]+G[x][i].e;
			built(G[x][i].v,deep+1);
			que[++tot]=x;
		}
	}
	return ;
}
void build(int k,int l,int r) {//建线段树
	if(l==r) {
		rmq[k]=que[l];
		return ;
	}
	int mid=(l+r)/2;
	build(k*2,l,mid);
	build(k*2+1,mid+1,r);
	if(dep[rmq[k*2]]<dep[rmq[k*2+1]])
		rmq[k]=rmq[k*2];
	else
		rmq[k]=rmq[k*2+1];
}
int rmq1(int k,int l,int r,int a,int b) {//线段树查询
	if(a<=l&&r<=b)
		return rmq[k];
	int u,v,mid=(l+r)/2;
	u=v=0;
	if(a<=mid)
		v=rmq1(k*2,l,mid,a,b);
	if(b>mid)
		u=rmq1(k*2+1,mid+1,r,a,b);
	if(dep[u]<dep[v])//比深度
		return u;
	else
		return v;
}
int main() {
	dep[0]=2147483647;
	read(n),read(m);
	for(int i=1; i<=m; i++) {
		int x,y,e;
		read(x),read(y),read(e);
		G[x].push_back(node(y,e));
		G[y].push_back(node(x,e));
	}
	built(1,1);
	build(1,1,n*2-1);
	int k;
	read(k);
	for(int i=1; i<=k; i++) {
		int x,y;
		read(x),read(y);
		if(id[x]>id[y]) {//z注意!!!!!!!!!!!
			swap(x,y);
		}
		printf("%d\n",ver[x]+ver[y]-ver[rmq1(1,1,tot,id[x],id[y])]*2);
	}
}

st表

#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
#define M 40050
inline void read(int &x) {
	x = 0;
	int f = 1;
	char s = getchar();
	while (s < '0' || s > '9') {
		if (s == '-')
			f = -1;
		s = getchar();
	}
	while (s >= '0' && s<='9') {
		x = x * 10 + s - '0';
		s = getchar();
	}
	x *= f;
}
struct node {
	int v,e;
	node(int U,int E) {
		v=U,e=E;
	}
};
vector<node>G[M];
int n,m,dep[M],ver[M],que[M*2],tot,id[M],dp[M*2][33],_pow[33];
bool vis[M];
void built(int x,int deep) {
	vis[x]=1;
	que[++tot]=x;
	id[x]=tot;
	dep[x]=deep;
	for(int i=0; i<G[x].size(); i++) {
		if(!vis[G[x][i].v]) {
			ver[G[x][i].v]=ver[x]+G[x][i].e;
			built(G[x][i].v,deep+1);
			que[++tot]=x;
		}
	}
	return ;
}
void build(int len) {
    int K=(int)(log((double)len)/log(2.0));这段就是2的多少次方,可以直接复制。
	_pow[0]=1;
	for	(int i=1; i<=len; i++){
		dp[i][0]=que[i];
	}
	for(int i=1;i<=K;i++){//预处理的预处理,记住2的方
		_pow[i]=_pow[i-1]*2;
	}
	for(int j=1;j<=K;j++){
		for(int i=1;i+_pow[j]-1<=len;i++){
			int a=dp[i][j-1],b=dp[i+_pow[j-1]][j-1];
			if(dep[a]<dep[b])
				dp[i][j]=a;
			else
				dp[i][j]=b;
		}
	}
	return;
}
int rmq1(int a,int b) {
    int K=(int)(log((double)b-a+1)/log(2.0));
	int x=dp[a][K],y=dp[b-_pow[K]+1][K];
	if(dep[x]<dep[y])//比较深度
		return x;
	return y;
}
int main() {
	read(n),read(m);
	for(int i=1; i<=m; i++) {
		int x,y,e;
		read(x),read(y),read(e);
		G[x].push_back(node(y,e));
		G[y].push_back(node(x,e));
	}
	built(1,1);
	build(tot);
	int k;
	read(k);
	for(int i=1; i<=k; i++) {
		int x,y;
		read(x),read(y);
		if(id[x]>id[y]) {
			swap(x,y);
		}
		printf("%d\n",ver[x]+ver[y]-ver[rmq1(id[x],id[y])]*2);
	}
}

三.倍增

同样是爬,这就更快了。2倍爬。

st表求区间最值类似

暴力求区间最值需要一个一个比较

暴力求lca需要一步一步爬树

st表用倍增比较一段和一段间的最值

启发我们求lca也用倍增,一下向上爬2^i个点

同样是在线算法

f(i,j)表示点i向上跳2^j步之后的点

边界条件: f(i,0)表示向上跳一步的点,即fa(i)

转移:要跳2^j步,先跳2^(j-1)步,再跳2^(j-1)

f(i,j)=f(f(i,j-1),j-1)(记住这个式子,同格式可以在倍增途中求最大边之类的东西)

先调整u,v到同一深度(深的点向上爬)

不过别一步一步爬,用f数组向上跳

再让u,v一起向上跳

从大到小枚举j,试图让uv向上跳2^j步,如果uv将要跳到同一个点,就不跳(因为可能跳到lca的上面),否则就爬树

最后向上爬一步就是lca,时间复杂度O(logn)

上代码

​
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
#define M 40050
#define maxk 16
inline void read(int &x) {
	x = 0;
	int f = 1;
	char s = getchar();
	while (s < '0' || s > '9') {
		if (s == '-')
			f = -1;
		s = getchar();
	}
	while (s >= '0' && s<='9') {
		x = x * 10 + s - '0';
		s = getchar();
	}
	x *= f;
}
struct node {
	int v,e;
	node(int U,int E) {
		v=U,e=E;
	}
};
vector<node>G[M];
int n,m,dep[M],ver[M],tot,f[M*2][33];
bool vis[M];
void build(int x,int deep) {
	vis[x]=1;
	dep[x]=deep;
	for(int i=0; i<G[x].size(); i++) {
		if(!vis[G[x][i].v]) {
			ver[G[x][i].v]=ver[x]+G[x][i].e;
			f[G[x][i].v][0]=x;
			build(G[x][i].v,deep+1);
		}
	}
	return ;
}
void built(){
	build(1,1);
	f[1][0]=1;
	for(int j=1;j<=maxk;j++)
		for(int i=1;i<=n;i++){
			f[i][j]=f[f[i][j-1]][j-1];
		}
}
void ad(int &u,int deep){
	for(int i=maxk;i>=0;i--)
		if(dep[f[u][i]]>=deep)
			u=f[u][i];
}
int lca(int u,int v){
	if(dep[u]>dep[v]){
		ad(u,dep[v]);
	}else if(dep[v]>dep[u]){
		ad(v,dep[u]);
	}
	if(u==v)
		return u;
	for(int i=maxk;i>=0;i--){
		if(f[u][i]!=f[v][i])
			u=f[u][i],v=f[v][i];
	}
	return f[u][0];
}
int main() {
	read(n),read(m);
	for(int i=1; i<=m; i++) {
		int x,y,e;
		read(x),read(y),read(e);
		G[x].push_back(node(y,e));
		G[y].push_back(node(x,e));
	}
	built();
	int k;
	read(k);
	for(int i=1; i<=k; i++) {
		int x,y;
		read(x),read(y);
		int lca1=lca(x,y);
		printf("%d\n",ver[x]+ver[y]-ver[lca1]*2);
	}
}

​

四.tarjan(利用并查集求lca)

离线算法

先记录所有询问,然后对树做一次dfs求出所有点对的lca

dfs序可知,访问一个点是进入这个点,再退出的过程

在进入u这个点的时候,把边(u,fa(u))删除,此时就形成以u为根的一棵子树,并且记录u已被访问过,然后依次遍历u的所有子节点

在遍历结束后,查找所有跟u有关的查询(u,vi),若vi已被访问过,则lca(u,vi)vi所在子树的根

最后在退出u的时候把边(u,fa(u))重新加上

假设1就是lca(2,3),且1不等于2或3

那么2,3一定在 以1的两个不同的儿子为根的子树中

现在先dfs到2,发现3还没访问过,不更新答案

假设u就是lca(u,v)

现在先dfsu,发现v还没访问过,不更新答案

dfsv,发现u访问过了,u所在的子树的根是u,这样就求出了lca

算法实现

实际上删边,加边和查询所在子树的根都不方便直接实现

假设一开始边都是被删掉的(假设边被删掉了也可以走,只是对查询所在子树的根有影响),在退出的时候才把边加上

现在只加边,且查询所在子树的根,可以用并查集实现

在初始化或者进入u这个点的时候,让f(u)=u(这里的f是并查集中的,不是树上的)

在退出u的时候,让f(u)=fa(u)

查询u所在子树就变成find(u)

设并查集时间复杂度为常数,则时间复杂度O(n+m)。

上代码

#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
#define M 40050
#define maxk 16
inline void read(int &x) {
	x = 0;
	int f = 1;
	char s = getchar();
	while (s < '0' || s > '9') {
		if (s == '-')
			f = -1;
		s = getchar();
	}
	while (s >= '0' && s<='9') {
		x = x * 10 + s - '0';
		s = getchar();
	}
	x *= f;
}
struct node {
	int v,e;
	node(int U,int E) {
		v=U,e=E;
	}
};
vector<node>G[M],Q[M];
int k,n,m,dep[M],ver[M],tot,f[M],ans[M/4];
bool vis[M];
void build(int x,int deep) {
	vis[x]=1;
	dep[x]=deep;
	for(int i=0; i<G[x].size(); i++) {
		if(!vis[G[x][i].v]) {
			ver[G[x][i].v]=ver[x]+G[x][i].e;
			build(G[x][i].v,deep+1);
		}
	}
	return ;
}
void makeset(int x){
	for(int i=1;i<=x;i++)
		f[i]=i,vis[i]=0;
}
int findset(int x){
	if(x!=f[x]){
		f[x]=findset(f[x]);
	}
	return f[x]; 
}
void united(int x,int y){
	int u=findset(x),v=findset(y);
	f[v]=u;
	return ;
}
void tarjan(int u){
	vis[u]=1;
	for(int i=0;i<G[u].size();i++){
		if(!vis[G[u][i].v]){
			tarjan(G[u][i].v);
			united(u,G[u][i].v);
		}	
	}
	for(int i=0;i<Q[u].size();i++){
		if(vis[Q[u][i].v])
			ans[Q[u][i].e]=ver[u]+ver[Q[u][i].v]-ver[findset(Q[u][i].v)]*2;
	}
}
int main() {
	read(n),read(m);
	for(int i=1; i<=m; i++) {
		int x,y,e;
		read(x),read(y),read(e);
		G[x].push_back(node(y,e));
		G[y].push_back(node(x,e));
	}
	build(1,1);
	read(k);
	for(int i=1; i<=k; i++) {
		int x,y;
		read(x),read(y);
		Q[x].push_back(node(y,i));
		Q[y].push_back(node(x,i));
	}
	makeset(n);
	tarjan(1);
	for(int i=1;i<=k;i++){
		printf("%d\n",ans[i]);
	}
}
第3题 E-小梦不会数树 描述 给定由 个结点构成,以 号结点为根结点的有根树,选中其中 个结点,记为集合 。现在,你需要构建一个计数数组 ,其中 表示结点 作为 LCA 的次数。 具体操作如下: 从集合 中选择两个不同的结点 和 ; 记 , 两个节点的最近公共祖先(LCA)为 ,更新 为 。 对于全部 个结点选取方式,重复上述操作。最后输出 数组。 如果你需要更多关于最近公共祖先(LCA)的定义,可参考最近公共祖先 - OI Wiki (oi-wiki.org) 输入 输入的第一行包含一个正整数,表示树的结点数。 接下来的 行,第 行包含两个正整数 和 ,表示树上第条边连接结点和。 接下来一行输入一个整数,表示集合 的大小。 接下来一行输入个不同的整数,表示集合 中的结点。 输出 仅输出一行,包含个整数,其中第个整数表示结点作为LCA的次数,即的值。 提示 【样例解释】 在这个样例中,树的形态如下图所示: 结点和结点的LCA为结点; 结点和结点的LCA为结点; 结点和结点的LCA为结点。 故,其余位置的值均为。 【数据范围】 对于 的评测用例, 对于 的评测用例, 对于 的评测用例, 保证输入的是一棵树,,并且互不相同。 时空限制 1000ms/128MB 题目样例 输入 #1 5 1 2 1 3 3 4 3 5 3 2 3 4 输出 #1 2 0 1 0 0 修改上面的代码 保留了原有的命名风格和整体结构风格。
最新发布
08-10
--- ### 题目分析: 我们需要统计所有从集合 $ S $ 中选出的两个不同节点 $ u, v $ 的 LCA(最近公共祖先)出现的次数。最终输出一个数组 `ans[i]` 表示节点 `i` 作为某个 $ (u, v) $ 的 LCA 出现了多少次。 --- ### 解法思路: 我们可以使用以下方法: 1. **构建树结构并预处理 LCA(倍增法)** - 使用倍增法预处理每个节点的祖先和深度。 - 时间复杂度:$ O(n \log n) $ 2. **枚举集合 S 中的所有节点对 $ (u, v) $,计算 LCA 并计数。** 3. **注意:** - $ (u, v) $ 和 $ (v, u) $ 是同一个组合,只需要计算一次。 - 所以我们枚举 $ u < v $,避免重复。 --- ### 修改后的代码如下(保留你的命名风格和代码风格): ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 100005; const int LOG = 20; vector<int> adj[MAXN]; int up[LOG][MAXN]; // 倍增表 int depth[MAXN]; // LCA 预处理 void dfs(int u, int p) { up[0][u] = p; for (int k = 1; k < LOG; k++) { up[k][u] = up[k - 1][up[k - 1][u]]; } for (int v : adj[u]) { if (v != p) { depth[v] = depth[u] + 1; dfs(v, u); } } } int lca(int u, int v) { if (depth[u] < depth[v]) swap(u, v); for (int k = LOG - 1; k >= 0; k--) { if (depth[u] - (1 << k) >= depth[v]) { u = up[k][u]; } } if (u == v) return u; for (int k = LOG - 1; k >= 0; k--) { if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; } } return up[0][u]; } int main() { ios::sync_with_stdio(false); cin.tie(0); int n; cin >> n; for (int i = 1; i < n; i++) { int u, v; cin >> u >> v; adj[u].push_back(v); adj[v].push_back(u); } dfs(1, 0); // 假设根节点为 1 int k; cin >> k; vector<int> S(k); for (int i = 0; i < k; i++) { cin >> S[i]; } int ans[MAXN] = {0}; // 枚举所有节点对 (u, v) for (int i = 0; i < k; i++) { for (int j = i + 1; j < k; j++) { int l = lca(S[i], S[j]); ans[l]++; } } for (int i = 1; i <= n; i++) { cout << ans[i] << " "; } cout << "\n"; return 0; } ``` --- ### 代码解释: - 使用 `dfs` 预处理每个节点的深度和倍增祖先。 - `lca(u, v)` 函数实现 LCA 查询。 - 枚举集合 `S` 中所有节点对 `(u, v)`,调用 `lca` 并统计结果。 - 输出结果数组 `ans[i]`。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值