大佬讲解链接 倍增法求Lca(最近公共祖先)_倍增求lca_姬小野的博客-CSDN博客
1.法一求最近公共祖先(在线做法)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 40010, M = N * 2;
int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], fa[N][16];
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)
{
depth[j] = depth[t] + 1;
q[ ++ tt] = j;
fa[j][0] = t;
for (int k = 1; k <= 15; k ++ )
fa[j][k] = fa[fa[j][k - 1]][k - 1];//从祖往子遍历,因此不必担心赋值的变量不存在
}
}
}
}
//也可以dfs,其中for循环内的优化也可以在bfs中使用
void dfs(int u,int fa)
{
d[u]=d[fa]+1;
p[u][0]=fa;
for(int i=1;(1<<i)<=depth[u];i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=head[u];i!=-1;i=next[i])
{
int v=e[i];
if(v!=fa)
dfs(v,u);
}
}
int lca(int a, int b)
{
if (depth[a] < depth[b]) swap(a, b);
for (int k = 15; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
a = fa[a][k];
if (a == b) return a;
for (int k = 15; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
a = fa[a][k];
b = fa[b][k];//注意要更新这个点,从最大开始是为了节约时间
}
return fa[a][0];
}
int main()
{
scanf("%d", &n);
int root = 0;
memset(h, -1, sizeof h);
for (int i = 0; i < n; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
if (b == -1) root = a;
else add(a, b), add(b, a);
}
bfs(root);
scanf("%d", &m);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
int p = lca(a, b);
if (p == a) puts("1");
else if (p == b) puts("2");
else puts("0");
}
return 0;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/154772/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.法二求最近公共祖先(离线做法)太繁琐
1
tarjan O(1)求lca
o
/ \ \
o o o
/\ /\ \
o o o o o
/\ y x 3
o o 2 1
第一类 已经遍历过,且回溯过[2]
o
/
o
/\ /
o o o
/\
o o
当前搜到了最右边这条路时
左边的分支上的点都是搜索过且回溯过的(标记为2)
第二类 正在搜索的分支[1] 标记为1
o
\
o
\
o
x
看x和第一类点中点的lca都是第一类中分支的根节点
那么可以把当前这条路上作为根节点的点中已经遍历过的分支合并到该根节点(用并查集做)
. .
/ \ \
o . .
/\ / → 回溯完到根节点后把根节点左边的子树上的点都用并查集合并到根节点
o o o
/\
o o
那么在遍历点x后 扫描所有和x相关的询问 如果询问中另一个点y已经被遍历+回溯过了
lca[x][y] = p[y]
o
/ \
o lca=p[y]
/\ /\
o o o o
/\ y x
o o
这样 枚举每个点1次 所有点合并1次 查询每个点1次 并查集O(1)
第三类 还未搜索过[0]
o
\
o
\
o
2
求树上两个点的距离
o
1/ \
lca o
2/\4 /\
o o o o
3/\ y
o o
x
d[node] 代表根节点到node的距离
其中d[x] = 1+2+3
d[y] = 1+4
则
x到y的距离 = d[x]+d[y] - 2*d[lca]
(3)Tarjan-离线求LCA O(m+n)
在深度优先遍历时,将所有点分成三大类
[1] 已经遍历过,且回溯过
[2] 正在搜索的分支
[3] 还未搜索到的点
作者:仅存老实人
链接:https://www.acwing.com/solution/content/24569/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 10010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];//每个点和1号点的距离
int p[N];
int res[M];
int st[N];
vector<PII> query[N];//把询问存下来
// query[i][first][second] first存查询距离i的另外一个点j,second存查询编号idx
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(p[x]!=x)p[x] = find(p[x]);
return p[x];
}
void dfs(int u,int 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)
{
st[u]=1;//当前路径点标记为1
// u这条路上的根节点的左下的点用并查集合并到根节点
for(int i = h[u];~i;i=ne[i])
{
int j = e[i];
if(!st[j])
{
tarjan(j);//往左下搜
p[j] = u;//从左下回溯后把左下的点合并到根节点
}
}
// 对于当前点u 搜索所有和u
for(auto item:query[u])
{
int y = item.first,id = item.second;
if(st[y]==2)//如果查询的这个点已经是左下的点(已经搜索过且回溯过,标记为2)
{
int anc = find(y);//y的根节点
// x到y的距离 = d[x]+d[y] - 2*d[lca]
res[id] = dist[u]+dist[y] - dist[anc]*2;//第idx次查询的结果 res[idx]
}
}
//点u已经搜索完且要回溯了 就把st[u]标记为2
st[u] = 2;
}
int main()
{
cin >> n >> m;
// 建图
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++)
{
int a,b,c;
cin >> a >> b >> c;
add(a,b,c),add(b,a,c);
}
// 存下询问
for(int i=0;i<m;i++)
{
int a,b;
cin >> a >> b;
if(a!=b)
{
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);
tarjan(1);
for(int i=0;i<m;i++)cout << res[i] << '\n';//把每次询问的答案输出
return 0;
}
作者:仅存老实人
链接:https://www.acwing.com/solution/content/24569/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3.优化求次短生成树
wzc dalao nb
⭐非树边w的值域是一定≥dist1 否则在当w<dist1,则之前kruskal求最小生成树的时候把w替换dist1连接a和b
就得到一个更小的生成树(且依然能是一个生成树--原因如下图)了 与kruskal得到的是最小生成树矛盾
dist1
-c - d- -c d-
a b → a b
- -
w w
解法0
AcWing 1148. 秘密的牛奶运输 暴力枚举
解法1
lca倍增O(logn)求新加入非树边边的两点a,b间的最大边和最小边
定理:对于一张无向图,如果存在最小生成树和次小生成树,那么对于任何一颗最小生成树都存在一颗次小生成树
使得这两棵树只有一条边不同
o-o
/ \
o o
\ /
o-o
那么我们就是要在现有的最小生成树中找一条非树边w去替换w[i]
则对于每一条非树边w,变换后的边权和=sum+w-w[i] 则我们想要边权和越小,则对应替换的w[i]越大
即我们要做的就是找这条路径上的最大边权替换
为了防止w == max(w[i] for i in path) 替换后w[i]依然==w 导致不是严格最小生成树
在这种情况下 我们用w替换这条路上的次大边
所以用d1 d2存最大边和次大边
2 预处理每个点i跳2^j后的父节点f[i][j]
lca
o
/ \
o o
/ \
o o
/ y
o
x
在x和y向 lca[x][y]跳的过程中维护各自路径中的最大值d1[x→lca]\d1[y→lca] 和次大值d2[x→lca]\d2[y→lca]
路径
最大值d1 o o o
| | |
次大值d2 o o o
| | |
假设最大值为. 则次大值从ci中找
ci . ci
| | |
o ci o
| | |
[x→y]的最大值 = max(d1[i] for i in path)
次大值 = 次大(max(d1[i],d2[i]) for i in path)
3 预处理:
每个点i跳2^j路径上的最大边权d1[i][j]
每个点i跳2^j路径上的次大边权d2[i][j]
d1[i][j]跳2^j次
→ →
o---o---o
i anc j
d1[i,j-1],d2[i,j-1] d1[anc,j-1],d2[anc,j-1]
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010, INF = 0x3f3f3f3f;
int n, m;
struct Edge
{
int a, b, w;
bool used;
bool operator< (const Edge &t) const
{
return w < t.w;
}
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];//log2(1e5)=16 d1最大边 d2次大边
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];
}
LL kruskal()
{
for (int i = 1; i <= n; i ++ ) p[i] = i;
sort(edge, edge + m);
LL res = 0;
for (int i = 0; i < m; i ++ )
{
int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
if (a != b)
{
p[a] = b;
res += w;
edge[i].used = true;
}
}
return res;
}
void build()
{
memset(h,-1,sizeof h);
for(int i = 0;i<m;i++)
{
if(edge[i].used)
{
int a = edge[i].a,b = edge[i].b,w = edge[i].w;
add(a,b,w),add(b,a,w);
}
}
}
void bfs()
{
memset(depth,0x3f,sizeof depth);
depth[0] = 0,depth[1] = 1;//哨兵0 根节点1
queue<int> q;
q.push(1);
while(q.size())
{
int t = q.front();
q.pop();//日常漏
for(int i = h[t];~i;i=ne[i])
{
int j = e[i];
// j没有被遍历过
if(depth[j]>depth[t]+1)
{
depth[j] = depth[t]+1;
q.push(j);
fa[j][0] = t;
d1[j][0] = w[i],d2[j][0] = -INF;
for(int k = 1;k<=16;k++)
{
/*
→ →
o---o---o
j anc
d1[i,k-1],d2[i,k-1] d1[anc,k-1],d2[anc,k-1]
*/
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
//初始化d1[j][k]和d2[j][k]
d1[j][k] = d2[j][k] = -INF;
for (int u = 0; u < 4; u ++ )
{
int d = distance[u];
// 更新最大值d1和次大值d2
if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
// 严格次大值
else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
// lca求出a, b之间的最大边权与次大边权
int lca(int a,int b,int w)
{
static int distance[N * 2];
int cnt = 0;
// a和b中取深度更深的作为a先跳
if (depth[a] < depth[b]) swap(a, b);
for (int k = 16; k >= 0; k -- )
// 如果a 跳2^k后的深度比b深度大 则a继续跳
// 直到两者深度相同 depth[a] == depth[b]
if (depth[fa[a][k]] >= depth[b])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
a = fa[a][k];
}
// 如果a和b深度相同 但此时不是同一个点 两个同时继续向上跳
if (a != b)
{
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
distance[cnt ++ ] = d1[b][k];
distance[cnt ++ ] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
// 此时a和b到lca下同一层 所以还要各跳1步=跳2^0步
distance[cnt ++ ] = d1[a][0];
distance[cnt ++ ] = d1[b][0];
}
// 找a,b两点距离的最大值dist1和次大值dist2
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; i ++ )
{
int d = distance[i];
if (d > dist1) dist2 = dist1, dist1 = d;
else if (d != dist1 && d > dist2) dist2 = d;
}
// ⭐ dist1和dist2是a和b之间的最大边权和次大边权 所以可以用w替换而仍然保持生成树(包含所有节点)
// 因为加入w这条边 原来的树会形成环
// 删除环中边权最大的边(如果最大的边和加入的边相等,那么删去次大边)。
// 如果w>这条路的最大边 w替换dist1
if (w > dist1) return w - dist1;
// 否则w==dist1 w替换dist2
if (w > dist2) return w - dist2;
// 不加这个return INF也是可以的
// ⭐因为非树边w的值域是一定≥dist1 否则在之前kruskal求最小生成树的时候把w替换dist1连接a和b就得到一个更小的生成树了 矛盾
// 所以最坏情况是w==dist1
// return INF;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
// kruskal建最小树(把用到的边标记)
LL sum = kruskal();
// 对标记的边建图
build();
bfs();
LL res = 1e18;
//从前往后枚举非树边
for (int i = 0; i < m; i ++ )
if (!edge[i].used)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
// lca(a,b,w) 返回用w替换w[i] 的差值 = w-w[i]
res = min(res, sum + lca(a, b, w));
}
printf("%lld\n", res);
return 0;
}
作者:仅存老实人
链接:https://www.acwing.com/solution/content/24609/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
4.砍边成两个
在没有附加边的情况下,我们发现这是一颗树,那么再添加条附加边(x,y)后,会造成(x,y)之间产生一个环
如果我们第一步截断了(x,y)之间的一条路,那么我们第二次只能截掉(x,y)之间的附加边,才能使其不连通;
我们将每条附加边(x,y)称为将(x,y)之间的路径覆盖了一遍;
因此我们只需要统计出每条主要边被覆盖了几次即可;
对于只被覆盖一次的边,第二次我们只能切断(x,y)边,方法唯一;
如果我们第一步切断了被覆盖0次的边,那么我们已经将其分为两部分,那么第二部只需要在m条附加边中任选一条即可,如果第一步截到被覆盖超过两次的边,将无法将其分为两部分;
运用乘法原理,我们累加答案;
那么怎么标记我们的边(x,y)被覆盖了几次呢,那么我们可以使用树上差分,是解决此类问题的经典套路;
我们想,对于一条边(x,y),我们添加一条边;
那么只会对x到lca(x,y)到y上的边产生影响,对于(x,y)我们将x节点的权值+1,y节点的权值+1,另lca(x,y)的权值-2,画图很好理解,那么我们进行一遍dfs求出每个节点权值,那么这个值就是节点父节点连边被覆盖的次数,按上述方法累加答案即可;
作者:Tyouchie
链接:https://www.acwing.com/solution/content/1280/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<iostream>
#include<queue>
using namespace std;
const int N = 100010, M = N * 2;
int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], fa[N][17];
int d[N];//存储每个点差分数
int ans;
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
// 求lca
void bfs()
{
memset(depth,0x3f,sizeof depth);
queue<int> q;
depth[0] = 0,depth[1] = 1;
q.push(1);
while(q.size())
{
int t = q.front();
q.pop();
for(int i = h[t];~i;i=ne[i])
{
int j = e[i];
if(depth[j]>depth[t]+1)
{
depth[j] = depth[t]+1;
q.push(j);
fa[j][0] = t;
// +2^k-1 +2^k-1
// j → fa[j][k-1] → fa[j][k]
for(int k = 1;k<=16;k++)
{
fa[j][k] = fa[fa[j][k-1]][k-1];
}
}
}
}
}
int lca(int a, int b)
{
// 从更低的a开始
if (depth[a] < depth[b]) swap(a, b);
// 把a和b提到同一个高度
for (int k = 16; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
a = fa[a][k];
if (a == b) return a;
// 如果 a,b不是同一个点 两个一起往上跳
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
a = fa[a][k];
b = fa[b][k];
}
// lca 倍增最终停下来的位置是lca下一层 所以还要跳一步
return fa[a][0];
}
// dfs 返回每一棵子树的和
int dfs(int u, int father)
{
// 遍历以u为根节点的子树j的和
int res = d[u];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j != father)
{
// 边t→j 砍掉后的方案 s
int s = dfs(j, u);
// 如果s=0 则随便砍
if (s == 0) ans += m;
// 如果s=1 则只能砍对应的非树边
else if (s == 1) ans ++ ;
// 子节点j的差分向上加给/传给 节点u
res += s;
}
}
// 如果没有子节点 即叶子节点 直接返回d[node]
return res;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
bfs();
// 读入附加边==非树边
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
int p = lca(a, b);
d[a] ++, d[b] ++, d[p] -= 2;
}
dfs(1, -1);
printf("%d\n", ans);
return 0;
}
作者:仅存老实人
链接:https://www.acwing.com/solution/content/24653/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。