题目描述
算法
本题是一个裸的严格次小生成树问题。所谓严格就是该次小生成树的总边权严格大于最小生成树(以下用MGT称呼)。非严格的就是边权和MGT相同
算法1:
自然的想先求MGT,再每次删去MGT中的一条边,在该图上做MGT求解(新图的MGT就是原图的次小生成树)。做一遍kruskal后面就不用再排序了,删边次数n - 1,每次求MGT是m,故 O ( m l o g m + n m ) O(mlogm + nm) O(mlogm+nm)。
- 这种方法求不出 严格次小生成树,因为如果有非严格次小生成树,删边之后再找肯定会找到非严。到底小不小是按边权和排序,非严是小于严的
虽然不能求严格,但也还是有用。做法的正确性证明如下:
- 将所有权值大于MGT的生成树集合S,按照不包含最小生成树中的第1条边,第2条边…第n - 1条边分类,能把S不漏的分完。故每类取min就能得到次小生成树。我们删边之后做MGT就是在取每一个类的最小值。
算法2:
.先求最小生成树,然后依次枚举非树边,然后将该边加入树中,同时从树中去掉一条边,使得最终图仍然是一棵树,统计最小值。
这个方法一定可以找到非严。证明如下
- 定义1:设T为图G的一棵生成树,对于非树边a和树边b,插入边a,并删除边b的操作记为(+a, -b)。如果T+a-b之后,仍然是一棵生成树,称(+a,-b)是T的一个可行交换。
- 定义2:称由T进行一次可行变换所得到的新的生成树集合称为T的邻集。
- 做法中的枚举外部边,删除树中边就是枚举MGT的邻集。
- 定理:次小生成树一定在最小生成树的邻集中
- 定理证明:
- 反证,如果有次小生成树与最小生成树不相同的有k条边,排序做kruskal,找到第一条和次小不同的边,在树中的边t连接ab。
- 我们将次小中连接ab中的一条边p去掉换成t,那么次小就会变得更小(因为排序kruskal)这样一个可行交换我们让次小生成树和最小生成树的不相同的边数k-1。这种操作可以持续做下去直到只差1条边。所以次小生成树一定在最小生成树的邻集中。
- 故该算法能找到非严格次小生成树。
该算法的详细步骤如下
-
求最小生成树,统计标记每条边是否是树边;同时把最小生成树建立,权值之和为 s u m sum sum
-
预处理生成树中任意两点间的边权最大值 d i s t 1 [ a ] [ b ] dist1[a][b] dist1[a][b]和长度次大 d i s t 2 [ a ] [ b ] dist2[a][b] dist2[a][b] (树中两点路径唯一,dfs)
-
依次枚举所有非MGT边t,边t连接a,b,权为w。显然a,b在MGT中。
-
尝试用t替换a-b的路径中最大的一条边A,A权为a。w > a。(若w < a,t边是外部边,直接换边就能得到更小的生成树,矛盾了)
-
如果w > a,替换后总权值是 s u m + w − d i s t 1 [ a ] [ b ] sum + w - dist1[a][b] sum+w−dist1[a][b]
-
否则 w = a ,不能替换A边,会得到非严格次小生成树(权值和MGT相等) 所以该做法也能得到非严
-
w = a,w > 次大值b 则可以替换b的边,替换后总权值是$sum + w - dist2[a][b] $
终于分析完了,代码+注释如下
/*
* @Author: 爱学习的图灵机
* @Date: 2022-02-19 19:56:58
* @LastEditTime: 2022-02-19 21:35:34
* Bilibili:https://space.bilibili.com/7469540
* 题目地址:https://www.acwing.com/activity/content/problem/content/1529/
* @keywords: 最小生成树次小生成树
将最小生成树称为为MGT
次小生成树在最小生成树的邻集中。(最小生成树变一个边)
求最小生成树,统计标记每条边是否是树边;同时把最小生成树建立,权值之和为sum
预处理生成树中任意两点间的边权最大值dist1[a][b]和长度次大dist2[a][b] (树中两点路径唯一,dfs)
依次枚举所有非MGT边t,边t连接a,b,权为w。显然a,b在MGT中。
尝试用t替换a-b的路径中最大的一条边A。t的权w >= A。(如果w < A,直接换边就能得到更小的生成树,矛盾了)
如果w > A,替换后总权值是sum + w - dist1[a][b]
否则 w = A ,不能替换,会得到非严格次小生成树(权值和MGT相等)
w = A,w > 次大值B 替换后总权值是sum + w - dist2[a][b]
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
typedef long long LL;
struct Edge{
int a, b, w;
bool f = false;
bool operator < (const Edge & A) const{
return w < A.w;
}
}edge[M];
int h[N], e[N * 2], ne[N * 2], w[N * 2], idx; // 无向树 2 * N
int cnt;
int n, m;
int dist1[N][N], dist2[N][N];// 最小和次小
int p[N];
void add(int a,int b,int c){
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx ++;
}
int find(int x){
return x == p[x] ? x : p[x] = find(p[x]);
}
//dfs无向图技巧:记录父节点防止回走
void dfs(int u, int fa, int maxd1, int maxd2, int d1[], int d2[]){
d1[u] = maxd1, d2[u] = maxd2;
for (int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if (j != fa){ //不往回搜
int td1 = maxd1, td2 = maxd2;
if (w[i] > td1) td2 = td1, td1 = w[i];
else if (w[i] < td1 && w[i] > td2) td2 = w[i];
dfs(j, u, td1, td2, d1, d2);
}
}
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
;
for(int i = 0; i < m; ++ i){
int a, b, w;
cin >> a >> b >> w;
edge[i] = {a, b, w};
}
sort(edge, edge + m);
for(int i = 1; i <= n; ++ i)p[i] = i;
LL sum = 0;
for(int i = 0; i < m; ++ i){
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int pa = find(a), pb = find(b);
if (pa != pb)
{
p[pa] = pb;
sum += w;
add(a, b, w), add(b, a, w);//! 合并集合,但加边是节点之间加边
edge[i].f = true;
}
}
for(int i = 1; i <= n; ++ i)dfs(i, -1, -1e9,-1e9, dist1[i],dist2[i]);// 生成树内搜索最长路
LL res = 1e18;
for(int i = 0; i < m; ++ i){
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
if(!edge[i].f){// 遍历每条外部边尝试替换
LL t = 1e18;
if(w > dist1[a][b]){
t = sum + w - dist1[a][b];
}else if( w > dist2[a][b]){ // w不是大于就是等于
t = sum + w - dist2[a][b];
}
res = min(res, t);
}
}
cout << res << endl;
return 0;
}