咸鱼了很多天,开始补题,进入了递归学习模式:
生成树专题->最小生成树专题->MST + LCA-> LCA -> LCA(tarjan实现)
LCA的tarjan实现
和tarjan找强连通分量类似,在DFS增加一些骚操作使得在搜索的过程中完成公共祖先的预处理。所以这是一种离线算法,复杂度O(n + q)。
思路
假设a点和b点的最近公共祖先是x,那么在DFS时访问的这三个节点的顺序必然是
到达节点x,继续搜索子树,到达a,返回附近节点,返回x,继续搜索子树,到达b。(a,b访问顺序可互换)
所以我们用并查集,在访问完一颗子树now后,把now归并到其父亲节点的集合中去。这样在访问到b时,x到a这段路径上的fa数组均已更新,所以find(a) = x,a和b的最近公共祖先就可以表示为find(a)。
伪代码
void tarjan(int now){
vis[now] = 1;
fa[now] = now;
for(each {x,y}){//枚举所有要查询最近公共祖先的点对
//如果当前点和有另一个已经访问过的点要求lca
if(x == now && vis[y])z[i] = find(y);
if(y == now && vis[x])z[i] = find(x);
//用z数组代表答案
}
for(int i = 0 ; i < G[now].size() ; i++){
//继续tarjan
edge e = G[now][i];
if(!vis[e.to]){
dis[e.to] = dis[now] + e.val;
tarjan(e.to);
fa[e.to] = now;
}
}
}
例题
hdu2586
AC代码
这里用邻接表存的查询数组,感觉复杂度会到O(n*q)
求树上最短路可以转换成dis[x] + dis[y] - 2 * dis[lca(x,y)],妙啊
#include<iostream>
#include<vector>
#include<cstdio>
using namespace std;
const int maxn = 4e4 + 5;
const int maxm = 205;
struct edge{
int to, val;
};
vector<edge>G[maxn];
int x[maxm],y[maxm],z[maxm];
int dis[maxn],fa[maxn],vis[maxn];
int n, m;
int find(int now){
if(fa[now] == now)return now;
return fa[now] = find(fa[now]);
}
void tarjan(int now){
vis[now] = 1;
fa[now] = now;
for(int i = 1 ; i <= m ; i++){
if(x[i] == now && vis[y[i]]) z[i] = find(y[i]);
if(y[i] == now && vis[x[i]]) z[i] = find(x[i]);
}
for(int i = 0 ; i < G[now].size() ; i++){
edge e = G[now][i];
if(!vis[e.to]){
dis[e.to] = dis[now] + e.val;
tarjan(e.to);
fa[e.to] = now;
}
}
}
int main(){
int T;
scanf("%d",&T);
while(T--){
scanf("%d %d",&n,&m);
for(int i = 1 ; i <= n ; i++){
vis[i] = 0;
G[i].clear();
}
int u, v, c;
for(int i = 1 ; i < n ; i++){
scanf("%d%d%d",&u,&v,&c);
G[u].push_back({v,c});
G[v].push_back({u,c});
}
for(int i = 1 ; i <= m ; i++){
scanf("%d%d",&x[i],&y[i]);
}
dis[1] = 0;
tarjan(1);
for(int i = 1 ; i <= m ; i++){
printf("%d\n",dis[x[i]] + dis[y[i]] - 2 * dis[z[i]]);
}
}
}