先来看一个例题:Forsaken喜欢独一无二的树
题意:
现在给定一个
n
n
n 个点,
m
m
m 条边的图,每条边
e
i
e_{i}
ei 都有一个权值
w
i
w_{i}
wi 。
刚开始最小生成树可能不唯一,现在可以删除一些边,使得剩下的边的最小生成树大小不变并且唯一。
求删除的边的权值和最小是多少?
分析:
什么样的边会影响到最小生成树的唯一性呢?
kruskal 求最小生成树 是将所有边权从小到大排序,然后判断当前边的两个端点所在连通块是否连通。如果没有连通,那么这条边就需要拿。
而此时如果有另外一条边,虽然也可以将这两个连通块合并,但是其权值比较大,那么这条边是不会影响到最小生成树的。
所以,只有两个边的权值相同,并且都能将端点的两个连通块合并,这么这两个边选择哪个都行,那么最小生成树就不唯一了。
所以,为了保证最小生成树唯一,那么就是要去掉若干条 权值相同并且能够合并相同连通块的边,只剩一个这样的边就行。
如何实现呢?
我们像 kruskal 一样将所有的边按照权值排序,从小到大遍历所有的边。
对于当前边来说,将所有的和当前边权相等的边都拿过来(双指针)。对于这些权值相同的边,我们要去掉一些能够合并相同连通块的边。
我们可以先将所有的权值相同且能够合并连通块的边都删除,然后再留下最小生成树中的边。
这样,对于权值相同的能够合并连通块的多余边就被删除了。
- 先遍历所有边,判断其是否可选,也就是判断其端点连接的两个连通块是否已经连通。如果没有连通,说明可选,删除的权值和
ans += wi
;
这时,我们把能够合并连通块的所有边都删除了。 - 然后,再遍历一遍,用这些边求最小生成树权值和
sum
。
最终,删除的多余边权之和就为 ans - sum
。
int sum = 0;
for(int i=1;i<=m;i++)
{
int r = i;
while(r <= m && a[r].w == a[i].w) r++;
r--;
for(int j=i;j<=r;j++)
{
int x = a[j].x, y = a[j].y, w = a[j].w;
if(find(x) != find(y)) ans += w;
}
for(int j=i;j<=r;j++)
{
int x = a[j].x, y = a[j].y, w = a[j].w;
if(find(x) != find(y)) pre[find(x)] = find(y), sum += w;
}
i = r;
}
ans -= sum;
完整Code:
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
map<PII,int> mp;
/**/
const int N = 200010, mod = 1e9+7;
int T, n, m;
struct node{
int x, y, w;
}a[N];
int ans, pre[N];
bool cmp(node a, node b){
return a.w < b.w;
}
int find(int x){
if(pre[x] != x) pre[x] = find(pre[x]);
return pre[x];
}
void kruskal()
{
sort(a+1, a+m+1, cmp);
int sum = 0;
for(int i=1;i<=m;i++)
{
int r = i;
while(r <= m && a[r].w == a[i].w) r++;
r--;
for(int j=i;j<=r;j++)
{
int x = a[j].x, y = a[j].y, w = a[j].w;
if(find(x) != find(y)) ans += w; //能拿的都删去
}
for(int j=i;j<=r;j++)
{
int x = a[j].x, y = a[j].y, w = a[j].w;
if(find(x) != find(y)) pre[find(x)] = find(y), sum += w; //求最小生成树
}
i = r;
}
ans -= sum; //把最小生成树的权值留下,删去的就是多余的了
}
signed main(){
Ios;
cin>>n>>m;
for(int i=1;i<=n;i++) pre[i] = i;
for(int i=1;i<=m;i++)
{
int x, y, w;cin>>x>>y>>w;
a[i] = {x, y, w};
}
kruskal();
cout << ans;
return 0;
}
这样,阻碍最小生成树唯一的边就都被删去了。
那么,如果需要判断一个图中最小生成树是否唯一,那么就可以用这种方法,看最终的删去边的权值是否为0,如果为0就是唯一的。
或者,也可以对于每一种权值来说,判断这其中的 可拿的边(端点所在连通块不连通) 是不是都在最小生成树中,如果有的不在,则说明最小生成树不唯一。
因为既然一个边可拿,也就是端点所在连通块不连通,那么其就应该出现在最小生成树中。而现在第二遍遍历跑最小生成树之后,发现之前可拿边的个数和可拿边的个数不同,那么也就是有多余的边,最小生成树不唯一。
Code:
#include<iostream>
#include<algorithm>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
/**/
const int N = 200010, mod = 1e9+7;
int T, n, m;
struct node{
int x, y, w;
}a[N];
int ans, pre[N];
int sum;
bool cmp(node a, node b){
return a.w < b.w;
}
int find(int x){
if(pre[x] != x) pre[x] = find(pre[x]);
return pre[x];
}
int kruskal()
{
sort(a+1, a+m+1, cmp);
for(int i=1;i<=m;i++)
{
int r = i;
while(r <= m && a[r].w == a[i].w) r++;
r--;
int cnt = 0; //记录可拿边的个数
for(int j=i;j<=r;j++)
{
int x = a[j].x, y = a[j].y, w = a[j].w;
if(find(x) != find(y)) cnt++; //可拿边个数++
}
for(int j=i;j<=r;j++)
{
int x = a[j].x, y = a[j].y, w = a[j].w;
if(find(x) != find(y)) pre[find(x)] = find(y), sum += w, cnt --; //用掉了,cnt--
}
i = r;
if(cnt) return 0; //最后还剩余可拿边,最小生成树不唯一
}
return 1;
}
signed main(){
Ios;
cin>>T;
while(T--)
{
cin>>n>>m;
ans = 0, sum = 0;
for(int i=1;i<=n;i++) pre[i] = i;
for(int i=1;i<=m;i++)
{
int x, y, w;cin>>x>>y>>w;
a[i] = {x, y, w};
}
if(!kruskal()) cout<<"Not Unique!\n";
else cout << sum <<endl;
}
return 0;
}
这种做法时间复杂度和求最小生成树复杂度相同,O(n+m)。
感觉比求次小生成树简单呢~