严格次小生成树

Posted One_Zzz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了严格次小生成树相关的知识,希望对你有一定的参考价值。

UPD 2021.6.1.12:50 @滑翔翼

我过了!!!感动中国!!!11

1.前置算法

  1. kruskal 求 最小生成树

  2. 倍增 求 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\\) 的严格次大边

分三种情况讨论


  1. \\(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))\\)

  1. \\(max(a,b) \\lt max(b,c)\\)

此时 \\(max(a,c)=max(a,b)\\)

\\(mx(a,c)=max(b,c) \\gets\\) 其实就是 \\(max\\) 不要的那个

  1. \\(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

原题 P4180

上代码:

#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]次小生成树

Luogu P4180 模板严格次小生成树[BJWC2010]

bzoj 1977 洛谷P4180 严格次小生成树

次小生成树

bzoj1977 [BeiJing2010组队]次小生成树 Tree——严格次小生成树