总说
一、最近公共祖先
学习了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 次小生成树——待更新
题目描述
思路
定理:对于一张无向图,如果存在最小生成树和(严格)次小生成树,那么对于任何一棵最小生成树,都存在一棵(严格)次小生成树,使得这两棵树只有一条边不同。
给定一张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步所经过的(严格)次小边权