最近公共祖先问题

总说

一、最近公共祖先

学习了4种方法来求最近公共祖先问题,具体讲解2 3 4方法

1.1 向上标记法——O(n^2)

从一个点开始向根节点遍历,把路过的点全部标记,然后另一个点向上走的过程中,第一次遇见的被标记过的点,就是所求最近公共祖先。

这个方法用的比较少。

1.2 倍增法——O(nlogn)

我们可以先预处理每个点,对于每个点,我们先处理出来:从位置i开始,向上走2^j步所能到达的结点编号,我们规定如下:

用f[i][j]表示:从i开始,向上走2^j步所能到达的结点编号,则0 <= j <= logn

用depth[i]表示:深度

哨兵:如果从i开始跳2^j步会跳过根节点,则规定f[i][j] = 0,深度depth[0] = 0

算法思路

假设有2个点x、y,而x节点的深度更深,这里换一个图演示

1、先将2个点跳到同一层,要跳的距离就是深度之差len = depth[x] - depth[y],通过二进制从高位到低位凑出len,时间复杂度为O(logn)

2、到达同一层之后,让2个点同时往上跳,一直跳到最近公共祖先的下一层,为什么只跳到下一层,是为了方便判断是否是最近的公共祖先。

如果f[x][k] == f[y][k],我们只知道到达了一个公共祖先,无法确定是否是最近的。

但是f[x][k] != f[y][k],我们知道他们一定还没到最近公共祖先。

时间复杂度也是O(logn)

要处理n个点的,所以预处理的时间复杂度为O(nlogn),查询一组点的时间复杂度为O(logn)

来个板子题练习一下,也方便说明代码。

题目

题目链接:P3379 【模板】最近公共祖先(LCA) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路

代码模板

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

using namespace std;

const int N = 5e5 + 10;
const int M = 20;// log2 N 约等于20 

int n, m, s;
int h[N], e[2 * N], ne[2 * N], idx;
int depth[N], f[N][M];//depth存深度 f[i][j]从i开始跳2^j放步到的节点编号 
int q[N]; // 手动队列 

void add(int a, int b)
{
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
} 

void bfs(int root)
{
	memset(depth, 0x3f, sizeof depth);
	depth[0] = 0, depth[root] = 1;
	int hh = 0, tt = 0;
	q[0] = root;
	while(hh <= tt)
	{
		int t = q[hh++];
		for(int i = h[t]; ~i; i = ne[i])// 遍历相邻点 
		{
			int j = e[i];
			if(depth[j] > depth[t] + 1)// 当前点深度为0x3f,没被遍历过
			{
				depth[j] = depth[t] + 1;
				q[++tt] = j;	
				f[j][0] = t;//j往上跳1步就是t 
				for(int k = 1; k <= 15; k++)//预处理往上跳2^k步 
					f[j][k] = f[f[j][k - 1]][k - 1];
			} 
		}
	}
}

int lca(int a, int b)
{
	if(depth[a] < depth[b])//保证a在b的下面 
		swap(a, b);
	//第一步:让a和b同层 
	for(int k = 15; k >= 0; k--)//跳2^k步 
		if(depth[f[a][k]] >= depth[b])//从a跳2^k放步仍在b的下面或者同层,跳出树的话为0 
			a = f[a][k]; //更新a的位置 
	
	if(a == b) // 同层后,如果是同一节点,就是答案了 
		return a;
	
	//第二步:a、b同时向上跳到最近公共祖先下一层 
	for(int k = 15; k >= 0; k--)
	{
		if(f[a][k] != f[b][k])// 没有跳到公共祖先 
		{
			a = f[a][k];
			b = f[b][k];	
		}	
	} 
	return f[a][0]; //再跳一步就是答案 
}

int main()
{
	memset(h, -1, sizeof h);
	scanf("%d %d %d", &n, &m, &s);
	for(int i = 1; i < n; i++)
	{
		int a, b;
		scanf("%d %d",&a, &b);
			add(a, b);
			add(b, a);
	}
	
	bfs(s); // 预处理 
	
	while(m--)
	{
		int a, b;
		scanf("%d %d", &a, &b);
		printf("%d\n", lca(a, b));
	}
	return 0;
}

1.3 Tarjan算法——离线求LCA——O(n + m)

这里也简单说明一下离线和在线的区别

离线是需要把所有输入都先读进来处理,然后再一起输出

而在线是可以一遍读入一边输出。

然后用Tarjan求LCA是一种离线的做法。

在深度优先遍历DFS中,我们把遍历到的点分为3大类

(1)已经遍历过的点:所有被搜过且子树已经被搜完的点(绿色)

(2)正在搜索的分支,包含当前搜索过程路径上的点(红色)

(3)还没有搜索过的点(蓝色)

对于我们当前搜索的点,不妨设为节点x,我们可以看一下后面所有与x点有关的询问,所有的 x 和 已经遍历过的点 的最近公共祖先,都在当前搜索x的路径上

所以,我们可以把所有遍历过的点,合并到它的根节点中。可以用并查集来实现。后面在询问的时候,可以直接输出并查集的头部。

合并是在DFS搜索完一个分支,回溯的时候合并。

所有点只会被遍历一次,时间复杂度为O(n),所有查询可以在O(1)时间下输出,时间复杂度为O(m),总时间复杂度为O(n + m)

还是用一道例题演示

题目

题目链接:1171. 距离 - AcWing题库

思路

这道题问的是2个点的距离,但是我们也可以转化成最近公共祖先问题

我们只需要用一个dist[N]数组,记录每个点到根节点的距离

假如我们要求x,y两个节点之间的距离,x,y最近公共祖先为节点p,如图:

通过公式:

d[x] + d[y] - 2 * d[p]

就能算出2点间距离。

代码模板

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e4 + 10;

int n, m;
int h[N], e[2 * N], ne[2 * N], w[2 * N], idx;
int dist[N]; //存所有点到根节点距离,这里默认1为根节点 
int p[N]; //并查集
int res[2 * N]; //用来存询问的结果
int st[N];

vector<PII> query[2 * N];//用来记录查询
//first记录要查询的另一个点是谁, second用来记录 查询编号 

void add(int a, int b, int c)
{
	e[idx] = b;
	ne[idx] = h[a];
	w[idx] = c;
	h[a] = idx++;
}

int find(int x)// 
{
	if(x != p[x])
		p[x] = find(p[x]);
	return p[x];
}

void dfs(int u, int fa)//当前节点u,父节点fa
{
	for(int i = h[u]; ~i; i = ne[i])
	{
		int j = e[i];
		if(j == fa)
			continue;
		dist[j] = dist[u] + w[i];//更新距离
		dfs(j, u);	
	}	
} 

void tarjan(int u)
{
	//0还没走过,1这次遍历走过的,2已经被遍历过的 
	st[u] = 1;
	for(int i = h[u]; ~i; i = ne[i])
	{
		int j = e[i];
		if(!st[j])//当前点没被走过 
		{
			tarjan(j);// 递归继续
			p[j] = u; //j合并到u中	
		}	
	} 
	
	// 遍历所有和u相关的查询
	for(auto it : query[u])
	{
		int y = it.first; //相关的点 
		int id = it.second; //查询的编号
		
		if(st[y] == 2)//已经被遍历过的
		{
			//2点的最近公共祖先就为 y在并查集中的祖宗节点 
			int anc = find(y);	
			res[id] = dist[u] + dist[y] - 2 * dist[anc]; //记录当前答案 
		}	
	} 
	st[u] = 2; //当前节点已经被遍历过了 
}

int main()
{
	memset(h, -1, sizeof h);
	scanf("%d %d", &n, &m);
	for(int i = 0; i < n - 1; i++)// 读入边 
	{
		int a, b, c;
		scanf("%d %d %d", &a, &b, &c);
		add(a, b, c);
		add(b, a, c);
	}
	
	for(int i = 0; i < m; i++)
	{
		int a, b;
		scanf("%d %d", &a, &b);// 查询的是a b两点的距离
		if(a != b)// a = b 不用存 答案是0 
		{
			query[a].push_back({b, i});	
			query[b].push_back({a, i});	
		} 
	}
	
	for(int i = 1; i <= n; i++)// 初始化并查集 
		p[i] = i;
	
	dfs(1, -1); //处理所有点到根节点1的距离 
	tarjan(1);//从1号点开始 
	
	for(int i = 0; i < m; i++)
		printf("%d\n", res[i]);
	
	return 0;
}

1.4 RMQ法——ST表求区间最小值

待更新~~

二、题目练习

2.1 次小生成树——待更新

题目描述

 题目链接:356. 次小生成树 - AcWing题库

思路

定理:对于一张无向图,如果存在最小生成树和(严格)次小生成树,那么对于任何一棵最小生成树,都存在一棵(严格)次小生成树,使得这两棵树只有一条边不同。 
给定一张N个点M条边的无向图。

用depth[i]表示:深度

用f[i][j]表示:从i开始,向上走2^j步所能到达的结点编号,则0 <= j <= logn

用d1[i][j]表示:从i开始,向上走2^j步所经过的最小边权

用d2[i][j]表示:从i开始,向上走2^j步所经过的(严格)次小边权

代码模板

  • 27
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值