严格次小生成树
Posted One_Zzz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了严格次小生成树相关的知识,希望对你有一定的参考价值。
UPD 2021.6.1.12:50 @滑翔翼
我过了!!!感动中国!!!11
1.前置算法
-
kruskal 求 最小生成树
-
倍增 求 LCA
2.定义
次小生成树,顾名思义,边权值和大于等于最小生成树的边权和且边权和最小的生成树
而严格次小生成树,就是边权值和严格大于最小生成树的边权和且边权和最小的生成树
3.正文
Part.0 一个栗子
这个图的最小生成树如下:
次小生成树如下:
这张图是不是符合一点:存在一个严格次小生成树与最小生成树只有一边只差?
Part.1 定理 & 证明
事实上我刚才说的那玩意儿就是个定理
定理:总有至少一棵严格次小生成树与最小生成树只有一边之差。
接下来是证明:然鹅我只会口胡
开始口胡((
考虑反证法:
由于这颗严格次小生成树与最小生成树不同的边最少有两条,不妨设:
边 \\(a,b\\) 为最小生成树里的与最小生成树不相同的两条边且 \\(w_a\\le w_b\\)。
边 \\(c,d\\) 为严格次小生成树里对应的两条边且 \\(w_c\\le w_d\\) 。
\\(\\textsf1.\\;\\)如果 \\(w_c\\lt w_b\\),我们可以再最小生成树中边 \\(b\\) 代替为 \\(c\\),这样会得到一棵更小的生成树,与最小生成树的定义矛盾。
\\(\\textsf2.\\;\\)如果 \\(w_c\\gt w_b\\),我们可以把最小生成树中边 \\(b\\) 代替为 \\(c\\),这样会得到一棵边权之和大于最小生成树,小于严格次小生成树的生成树,与严格次小生成树的定义矛盾。
\\(\\textsf3.\\;\\)如果 \\(w_c=w_b\\) ,我们可以最小生成树中的边 \\(a\\) 代替为 \\(c\\),就再分成情况 \\(w_c\\lt w_a\\) 和 \\(w_c\\gt w_a\\) 来讨论。和情况 \\(\\textsf{1},\\textsf{2}\\) 一样。
\\(\\textsf4.\\;\\)如果\\(w_a=w_b=w_c\\lt w_d\\),这时可以把边 \\(a\\) 代替严格次小生成树中的 \\(c\\) 边,就符合定义了。
\\(\\textsf5.\\;\\)最后还剩一种情况 \\(w_a=w_b=w_c=w_d\\) ,则与严格次小生成树定义中的严格矛盾
\\(\\textsf{Q.}\\) 为什么一定能代替呢?
\\(\\textsf{A.}\\) 因为两棵树只有两条边不同,所以如果 \\(b\\) 不能换为 \\(c\\) ,当且仅当边 \\(a\\) 和边 \\(c\\) 是重边。这时就可以用 \\(a\\) 代替 \\(c\\) 。如果边 \\(a\\) 也不能替换 \\(b\\),则存在 \\(a,b,c\\) 都是重边,与 \\(a,b\\) 存在同一个生成树矛盾。
Part.2 思路
通过证明,不难想到如下方法:
枚举每一条不在最小生成树的边 \\(\\{u,v,w\\}\\),将其加入最小生成树,这时会出现一个环。把加上边之前环上的最大边 \\(\\{x,y,z\\}\\) 丢掉。
设最小生成树的边权值和为 \\(sum\\) ,此时的新边权值和为 \\(sum-w+z\\),就可以用 \\(sum-w+z\\) 更新答案。
图文结合:考虑一个图的一棵生成树如下:
我们加上一条边 \\(\\{5,6\\}\\)
产生一个环,应该是 \\(5\\to6\\to3(lca)\\to5\\)
如果我们想求加上这条边之前的最大值,就等价于求 \\(5\\to3(lca)\\) 和 \\(6\\to3(lca)\\) 两条路径上的最大值,可以考虑倍增(具体倍增解法见 Part.3)
然后呢,你就会发现这样只能求出来非严格次小生成树
为什么呢?因为如果 \\(w\\) 正好等于 \\(z\\) ,就不满足严格条件了。
所以,在存一条最大边的同时,还要存一条严格次大边,用来处理两条边长度相等的情况。
Part.3 实现
实现分成两部分:倍增的预处理函数以及单次询问的处理
预处理
三个数组如下。
-
祖先数组 \\(fa_{i,j}\\) 是点 \\(i\\) 的 \\(2^j\\) 级祖先
-
最大值数组 \\(max1_{i,j}\\) 是点 \\(i\\) 到它的 \\(2^j\\) 级祖先这样一条路径上的权值最大的边的权值
-
最大值数组 \\(max2_{i,j}\\) 是点 \\(i\\) 到它的 \\(2^j\\) 级祖先这样一条路径上的权值严格次大的边的权值
根据倍增的更新方法:fa[i][j]=fa[fa[i,j-1]][j-1]
,同理,max1[i][j],max2[i][j]
也是由 i,fa[i][j-1]
的 \\(max1,max2\\) 更新出来的
那我们怎么更新 \\(max1_{i,j}\\) 和 \\(max2_{i,j}\\) 呢?
定义 \\(A=i,B=fa_{i,j-1},C=fa_{i,j}\\)。
为了方便看,用 \\(max(u,v)\\) 记录 \\(u\\to v\\) 的最大边,\\(mx(u,v)\\) 记录 \\(u\\to v\\) 的严格次大边
分三种情况讨论
- \\(max(a,b) = max(b,c)\\)
显然 \\(max(a,c)=max(a,b)=max(b,c)\\)
同理,\\(mx(a,c)=\\max(mx(a,b),mx(b,c))\\)
- \\(max(a,b) \\lt max(b,c)\\)
此时 \\(max(a,c)=max(a,b)\\)
而 \\(mx(a,c)=max(b,c) \\gets\\) 其实就是 \\(max\\) 不要的那个
- \\(max(a,b) \\gt max(b,c)\\)
和情况2差不多。
\\(max(a,c)=max(b,c)\\)
\\(mx(a,c)=max(a,b)\\)
\\(\\tt{Ps:}\\) 实现时把情况2和3合并起来写
\\(\\tt{Pss:}\\) 实现时 DFS 可能会超时,要用 BFS(不过似乎模板题都不会
查询
思路里讲的差不多了?
事实上,对于可以轻松写出 倍增LCA 的神仙,最大的难点就是如何更新答案(严格次小很毒瘤的
假如我想用 \\(max1_{i,j},max2_{i,j}\\) 来更新答案 \\(me1,me2\\),程序如下:(我用 \\(x\\) 代替 \\(max1_{i,j},max2_{i,j}\\))
if(x > me1) me2 = me1,me1 = x; // 注意me2也要更新
if(x > me2 && x != me1) me2 = x; // 注意要判x!=me1,不然me2会被更新为x
// Ps:me1 = first max edge,me2 = second max edge。。
注意程序第二行,有人肯定会问,写成 else if(x > me2)
行不行?
考虑一下,如果 \\(x=me1\\),这时 \\(me2\\) 也会更新为 \\(x\\) ,\\(me2\\) 的确是次大值,但不符合严格次大值。
所以还是有坑点的 qwq
\\(\\tt{Ps}\\):倍增最后求出来的 LCA 实际上是 fa[x][0]
,所以最后还要用 max1[x][0],max2[x][0],max1[y][0],max2[y][0]
更新答案。
Part.4 代码
程序中 \\(anc\\) 其实是倍增的祖先数组 \\(fa\\),因为 \\(fa\\) 与并查集求最小生成树重名了 /kk
上代码:
#include<stdio.h>
#include<string.h>
#include<queue>
#include<algorithm>
#define int long long // 不开_____见祖宗
inline int Read(){
register int x = 0,c = getchar();
for(;c < 48 || c > 57;c = getchar());
for(;c >= 48 && c <= 57;c = getchar()) x = x * 10 + (c ^ 48);
return x;
}
const int maxn = 1e5 + 1;
const int maxm = 3e5 + 1;
const int maxlg = 25;
const int inf = 1e16 + 1; // inf 要开大
int n,m;
struct SMST{ // SMST → Second MST (蒟蒻的英文太菜了
void init(){
ecnt = lg2[1] = 0;me1 = me2 = -inf;fa[1] = 1;
for(int i = 2;i <= n;++i) fa[i] = i,G[i].clear(),lg2[i] = lg2[i >> 1] + 1;
memset(dep,0,sizeof dep),memset(max1,0,sizeof max1),memset(max2,0,sizeof max2),memset(anc,0,sizeof anc);
while(!q.empty()) q.pop();
}
void add_edge(int u,int v,int w){
E[++ecnt] = (EDGE){u,v,w,false};
}
int fnd(int x){
return x == fa[x] ? x : fa[x] = fnd(fa[x]);
}
int kruskal(){
int sum = 0;
std::sort(E + 1,E + m + 1);
for(int i = 1;i <= m;++i){
int fu = fnd(E[i].u),fv = fnd(E[i].v);
if(fu != fv){
fa[fv] = fu,sum += E[i].w,E[i].vis = true;
G[E[i].u].push_back((edge){E[i].v,E[i].w}),G[E[i].v].push_back((edge){E[i].u,E[i].w});
// G是最小生成树
}
}
return sum;
}
void bfs(int st){
dep[st] = 0;
q.push(st);
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = 0;i < G[u].size();++i){
int v = G[u][i].v,w = G[u][i].w;
if(v != anc[u][0]){
dep[v] = dep[u] + 1;
anc[v][0] = u,max1[v][0] = w,max2[v][0] = -inf;
q.push(v);
for(int i = 1;i <= lg2[dep[u]];++i){
anc[v][i] = anc[anc[v][i - 1]][i - 1];
if(max1[v][i - 1] != max1[anc[v][i - 1]][i - 1]){
max1[v][i] = std::max(max1[v][i - 1],max1[anc[v][i - 1]][i - 1]);
max2[v][i] = std::min(max1[v][i - 1],max1[anc[v][i - 1]][i - 1]);
} else {
max1[v][i] = max1[v][i - 1];
max2[v][i] = std::max(max2[v][i - 1],max2[anc[v][i - 1]][i - 1]); // 分情况处理
}
}
}
}
}
}
void upd(int u,int lg){ // 把更新写成函数
int num = max1[u][lg];
if(num > me1) me2 = me1,me1 = num;
else if(num > me2 && num != me1) me2 = num;
num = max2[u][lg];
if(num > me1) me2 = me1,me1 = num;
else if(num > me2 && num != me1) me2 = num;
}
void lca(int x,int y){
me1 = me2 = -inf;
if(dep[x] < dep[y]) std::swap(x,y);
while(dep[x] > dep[y]){
int d = lg2[dep[x] - dep[y]];
upd(x,d),x = anc[x][d];
// 向上跳要更新答案
}
if(x == y) return;
for(int i = lg2[dep[x]];i >= 0;--i) if(anc[x][i] != anc[y][i]) upd(x,i),upd(y,i),x = anc[x][i],y = anc[y][i];
upd(x,0),upd(y,0);
// 由于最后求出来的LCA是x的父亲,所以还要再更新一次
}
int smst(){
int sum = kruskal(),ans = inf;
bfs(1);
for(int i = 1;i <= m;++i)
if(!E[i].vis){
lca(E[i].u,E[i].v);
if(me1 != E[i].w) ans = std::min(ans,sum - me1 + E[i].w);
else ans = std::min(ans,sum - me2 + E[i].w); // 注意判断是不是等于原来的最大边
}
return ans;
}
int ecnt,fa[maxn],dep[maxn],lg2[maxn],anc[maxn][maxlg];
int max1[maxn][maxlg],max2[maxn][maxlg],me1,me2;// me1是本次询问的最大值,me2是次大值
struct EDGE{
int u,v,w;
bool vis; // vis 表示这条边有没有在最小生成树中出现
bool operator<(const EDGE& KlsAKsshForever) const {
return w < KlsAKsshForever.w; // 。。。
}
} E[maxm];
struct edge{
int v,w;
};
std::vector<edge> G[maxn];
std::queue<int> q;
} smst; // 原谅结构体清奇马蜂
signed main(){
n = Read(),m = Read();
smst.init();
for(int i = 1;i <= m;++i){
int u,v,w;
u = Read(),v = Read(),w = Read();
smst.add_edge(u,v,w);
}
printf("%lld\\n",smst.smst());
return 0;
}
完结撒花qwq
4.总结
个人觉得严格次小生成树最毒瘤的地方就是它是严格次小,细节挺多,kruskal 挺简单的,倍增的 bfs ,查询应该也不难,最难我觉得是 \\(max1,max2\\) 的更新,以及严格次小的查询。
以上是关于严格次小生成树的主要内容,如果未能解决你的问题,请参考以下文章
洛谷 P4180 模板严格次小生成树[BJWC2010]次小生成树