LCA和tarjan
LCA 时间复杂度(mlogn):
例题链接:
Acwing:1172祖孙询问
(https://https://www.acwing.com/problem/content/1174/)
####预处理
void bfs(int root)
{
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[root] = 1;
int hh = 0, tt = 0;
q[tt ++ ] = 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];
}
}
}
}
####查询(跳步操作)
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];
}
###tarjan() 时间复杂度(O(n + m))
Acwing:1173距离
(https://www.acwing.com/problem/content/1173/)
####预处理:
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);
}
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
####查询与合并(也可以叫做查询和路径压缩过程)
void tarjan(int u)
{
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!st[j])
{
tarjan(j);
p[j] = u;
}
}
for (auto it : q[u])
{
int a = it.first, id = it.second;
if (st[a] == 2)
{
int ans = find(a);
res[id] = dist[u] + dist[a] - dist[ans] * 2;
}
}
st[u] = 2;
}
LCA(mlogn):在线算法
void bfs(int root) // 初始化操作, root传进来的是祖先节点
{
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[root] = 1;
int hh = 0, tt = 0;
q[tt ++ ] = 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];
}
}
}
}
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];
}
###tarjan(O(n + m)):离线做法
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);
}
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void tarjan(int u)
{
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!st[j])
{
tarjan(j);
p[j] = u;
}
}
for (auto it : q[u])
{
int a = it.first, id = it.second;
if (st[a] == 2)
{
int ans = find(a);
res[id] = dist[u] + dist[a] - dist[ans] * 2;
}
}
st[u] = 2;
}
##补充:
###树剖解法:
树链剖分,正如其名,这个算法的主要思想就是 把“树”“剖分”成“链”
那怎么实现以及它的作用:
已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z
操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和
操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z
操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和
####树剖的话很明显——一跳就是一条链,对于n极大的情况就相当于是倍增的再一优化
遍历x到y的路径,我们亦容易想到LCA——两个点同时往上跳,直到某个值相同,可以一起操作。所以我们的思路就是:两个点不在同一条链就往链头的父亲节点跳,在同一条链上就直接处理。而处理方法也很简单——因为全程都在链上以连续的新节点编号来操作,所以线段树维护区间距离就很方便了,完全不受树剖影响地敲一个基本的建树、查询、区间修改+延迟标记的代码就可以了
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 40010, M = N << 1;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int fa[M],top[M],dep[M],sz[M],son[M];
long long dis[M];
void dfs1(int x, int f)
{
fa[x]=f;
dep[x]=dep[f]+1;
sz[x]=1;
son[x]=0;
for(int i = h[x]; ~i ; i = ne[i])
{
int y = e[i];
if(y == f) continue;
dis[y] = dis[x] + w[i];
dfs1(y, x);
sz[x] += sz[y];
if(sz[y] > sz[son[x]]) son[x] = y;
}
}
void dfs2(int x, int tp)
{
top[x] = tp;
if(son[x]) dfs2(son[x], tp);
for(int i = h[x]; ~i ; i = ne[i])
{
int y = e[i];
if(y == fa[x] || y == son[x]) continue;
dfs2(y, y);
}
}
int lca(int x,int y)
{
while(top[x] != top[y])
{
if(dep[top[x]] < dep[top[y]]) swap(x, y);
x = fa[top[x]];
}
if(dep[x] > dep[y]) return y;
return x;
}
int main()
{
scanf("%d %d",&n,&m);
memset(h, -1, sizeof h);
for(int i = 1; i < n; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
dfs1(1, 0);
dfs2(1, 1);
while(m --)
{
int a, b;
scanf("%d%d", &a, &b);
int p = lca(a, b);
printf("%lld\n", dis[a] + dis[b] - dis[p] * 2);
}
return 0;
}
非树边w的值域是一定≥dist1 否则在当w<dist1,则之前kruskal求最小生成树的时候把w替换dist1连接a和b
就得到一个更小的生成树(且依然能是一个生成树–原因如下图)了 与kruskal得到的是最小生成树矛盾
dist1
-c - d- -c d-
a b → a b
- -
w w
/*
解法a
AcWing 1148. 秘密的牛奶运输 暴力枚举
解法b
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]
分析条件
题目中给出的关键点,就是严格和次小.
什么是严格
就是题目强制要求严格单调性,不可以有==号的出现.
什么是次小
我们应该都知道,最小生成树,它要求边集合的边总和最小,那么次小生成树,要求边集合的边总和只比最小生成树边集合权值大.
总结性质
有至少一个(严格)次小生成树,和最小生成树之间只有一条边的差异。和真理只有一点差异,那就是出题人毒瘤
我们来粗略证明一下.(强行伪证)
我们知道最小生成树,是由n−1n−1条构成的.
那么其他的M−N+1M−N+1就是多余边.
假如说我们把一条多余边(x,y,z)(x,y,z),加入到了最小生成树中,那么一定会在(x,y)(x,y)之间的路径上形成一个环.
那么这个环上面,最大的边称之为
Val1
Val1
次大的边,称之为
Val2
Val2
而且为了保证严格这个单调性质,我们必须设
Val1>Val2最大的边一定大于次大的边
Val1>Val2最大的边一定大于次大的边
接下来,我们就需要好好分析一下这条多余边了.
我们知道多余边,替换任何一条树上的一条边,都会使得最小生成树,不再最小
为什么?
因为最小生成树上的每一条边,一定是满足贪心性质下的最小的边.为什么啊?相信你的直觉啊
这个证明,我们使用的克鲁斯卡尔算法,已经告诉我们为什么.真相只有一个,我懒了
总而言之,言而总之,我们现在知道了这条多余边的加入.,一定会产生非最小生成树.
我们不妨令
ans=最小生成树边权之和
ans=最小生成树边权之和
假如说我们将多余边,替换掉最大权值边.
Val1==>z此时我们发现当前生成树W=ans+z−Val1W=最小生成边权之和+加上多余边−最大权值边
Val1==>z此时我们发现当前生成树W=ans+z−Val1W=最小生成边权之和+加上多余边−最大权值边
这一轮替换,我们可以认为这棵生成树有潜力成为次小生成树.
然后,我们发现,换一换次大边,也是可以的.
我们将多余边,强行替换掉次大权值边.
Val2==>z此时当前生成树W=ans+z−Val2W=最小生成树之和+加入多余边−次大权值边
Val2==>z此时当前生成树W=ans+z−Val2W=最小生成树之和+加入多余边−次大权值边
#####题目链接:356. 次小生成树
(https://www.acwing.com/problem/content/description/358/)
#####题目链接:352. 闇の連鎖
(https://www.acwing.com/problem/content/description/354/)
两个例题的代码这里就不摆出来了, 感觉太长了 💢 💢 💢
另外不管是LCA算法还是tarjan算法都是书上算法, 如果想要快速了解算法 原理的话最好是参照算法书上的描述或者是提高课里面的讲解, 学算法嘛,很多时候都是要花很多精力去证明算法的正确性,
,也是可以的.
我们将多余边,强行替换掉次大权值边.
Val2==>z此时当前生成树W=ans+z−Val2W=最小生成树之和+加入多余边−次大权值边
Val2==>z此时当前生成树W=ans+z−Val2W=最小生成树之和+加入多余边−次大权值边
#####题目链接:356. 次小生成树
(https://www.acwing.com/problem/content/description/358/)
#####题目链接:352. 闇の連鎖
(https://www.acwing.com/problem/content/description/354/)
两个例题的代码这里就不摆出来了, 感觉太长了 💢 💢 💢
另外不管是LCA算法还是tarjan算法都是书上算法, 如果想要快速了解算法 原理的话最好是参照算法书上的描述或者是提高课里面的讲解, 学算法嘛,很多时候都是要花很多精力去证明算法的正确性,
虽然有句话说:不经过证明的算法都是在耍流氓, 不过我们在考试或者比赛的过程中能直接套模板就直接套模板,方便快捷,简单暴力