题目大意:给一个连通图,n个点,m个边,然后求出次小生成树和最小生成树的权值是否相等,也就是判断最小生成树是否唯一,如果唯一就输出最小生成树的权值,不唯一输出Not Unique!
一下讲解内容转自:MATO IS NO.1
以下的T全部指G的最小生成树。
(1)Prim:
设立数组F,F[x][y]表示T中从x到y路径上的最大边的权值。F数组可以在用Prim算法求最小生成树的过程中得出。每次将边(i, j)加入后(j是新加入树的边,i=c[j]),枚举树中原有的每个点k(包括i,但不包括j),则F[k][j]=max{F[k][i], (i, j)边权值},又由于F数组是对称的,可以得到F[j][k]=F[k][j]。然后千万记住将图G中的边(i, j)删除(就是将邻接矩阵中(i, j)边权值改为∞)!因为T中的边是不能被加入的。等T被求出后,所有的F值也求出了,然后,枚举点i、j,若邻接矩阵中边(i, j)权值不是无穷大(这说明i、j间存在不在T中的边),则求出{(i, j)边权值 - F[i][j]}的值,即为加入边(i, j)的代价,求最小的总代价即可。 另外注意三种特殊情况:【1】图G不连通,此时最小生成树和次小生成树均不存在。判定方法:在扩展T的过程中找不到新的可以加入的边;【2】图G本身就是一棵树,此时最小生成树存在(就是G本身)但次小生成树不存在。判定方法:在成功求出T后,发现邻接矩阵中的值全部是无穷大;【3】图G存在平行边。这种情况最麻烦,因为这时代价最小的可行变换(-E1, +E2)中,E1和E2可能是平行边!因此,只有建立两个邻接矩阵,分别存储每两点间权值最小的边和权值次小的边的权值,然后,每当一条新边(i, j)加入时,不是将邻接矩阵中边(i, j)权值改为无穷大,而是改为连接点i、j的权值次小的边的权值。
代码(这个代码是很久以前写的,有点丑了,注释也少,不过prim算法相对简单的多)
#include <iostream>
#include <string.h>
#include <stdio.h>
#define M 105
using namespace std;
int map1[M][M],map2[M][M],minmap[M][2],n,m,mst;
//map1是原图,map2[i][j]表示最小生成树上i到j的最长的边的值,
//mst表示最小生成树的值,那么此短路就是遍历所有i到j,
//求mst-map2[i][j]+map1[i][j]的嘴小指,就是次小生成树
bool demap[M];
void prim()
{
memset(map2,100,sizeof(map2));
memset(minmap,100,sizeof(minmap));
memset(demap,true,sizeof(demap));
int star,i,j,k,m;
mst=0;
demap[star=1]=false;
while(1)
{
map2[star][star]=0;
for(i=1,m=100000;i<=n;i++)
{
if(map1[star][i]<minmap[i][0])
{
minmap[i][1]=star;
minmap[i][0]=map1[star][i];
}
if(demap[i]&&minmap[i][0]<m)
{
m=minmap[i][0];
k=i;
}
}
if(m==100000)
break;
for(i=1;i<=n;i++)
if(!demap[i])
map2[k][i]=map2[i][k]=max(map2[minmap[k][1]][i],minmap[k][0]);
map1[k][minmap[k][1]]=map1[minmap[k][1]][k]=1684300900;
mst+=minmap[star=k][0];
demap[star]=false;
}
int Max=1684300900;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(map1[i][j]!=1684300900)
Max=min(mst-map2[i][j]+map1[i][j],Max);
if(Max!=mst)
cout<<mst<<endl;
else
cout<<"Not Unique!"<<endl;
}
int main()
{
int N;cin>>N;
while(N--)
{
memset(map1,100,sizeof(map1));
int i,j,k,x,y,z;
cin>>n>>m;
for(i=0;i<m;i++)
{
scanf("%d%d%d",&x,&y,&z);
map1[x][y]=map1[y][x]=min(z,map1[x][y]);
}
prim();
}
}
(2)Kruskal:
Kruskal算法也可以用来求次小生成树。在准备加入一条新边(a, b)(该边加入后不会出现环)时,选择原来a所在连通块(设为S1)与b所在连通块(设为S2)中,点的个数少的那个(如果随便选一个,最坏情况下可能每次都碰到点数多的那个,时间复杂度可能增至O(NM)),找到该连通块中的每个点i,并遍历所有与i相关联的边,若发现某条边的另一端点j在未选择的那个连通块中(也就是该边(i, j)跨越了S1和S2)时,就说明最终在T中"删除边(a, b)并加入该边"一定是一个可行变换,且由于加边是按照权值递增顺序的,(a, b)也一定是T中从i到j路径上权值最大的边,故这个可行变换可能成为代价最小的可行变换,计算其代价为[(i, j)边权值 - (a, b)边权值],取最小代价即可。注意,在遍历时需要排除一条边,就是(a, b)本身(具体实现时由于用DL边表,可以将边(a, b)的编号代入)。另外还有一个难搞的地方:如何快速找出某连通块内的所有点?方法:由于使用并查集,连通块是用树的方式存储的,可以直接建一棵树(准确来说是一个森林),用“最左子结点+相邻结点”表示,则找出树根后遍历这棵树就行了,另外注意在合并连通块时也要同时合并树。 对于三种特殊情况:【1】图G不连通。判定方法:遍历完所有的边后,实际加入T的边数小于(N-1);【2】图G本身就是一棵树。判定方法:找不到这样的边(i, j);【3】图G存在平行边。这个对于Kruskal来说完全可以无视,因为Kruskal中两条边只要编号不同就视为不同的边。 其实Kruskal算法求次小生成树还有一个优化:每次找到边(i, j)后,一处理完这条边就把它从图中删掉,因为当S1和S2合并后,(i, j)就永远不可能再是可行变换中的E2了。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
const int N = 110;
const int M = N * (N - 1);
using namespace std;
struct Edg{//edg1用于储存图,edg2用于储存集合
int key, id, next;
}edg1[M], edg2[M];
struct Edg1{//edg3用于储存边
int x, y, len;
bool operator < (const Edg1 a)const{
return len < a.len;
}
}edg3[M];
int nod1[N], nod2[N], nod2_num[N], k1, k2;//nod1、k1和nod2、k2分别对应edg1和edg2;邻接表存图
int n, m, bing[N];
int mst, diff;//mst是最小生成树的权值,diff表示次小生成树与最小生成树的差值,如果是0就代表次小生成树的权值和最小生成树一样
void addedg(struct Edg edg[], int nod[], int x, int y, int len, int *k){//加边操作
edg[k1].key = len;
edg[k1].id = y;
edg[k1].next = nod[x];
nod[x] = (*k) ++;
}
int find(int x){
return (bing[x] != -1 ? find(bing[x]) : x);
}
void init(){//预处理函数
k1 = k2 = mst = 0;
diff = (1 << 31) - 1;
memset(nod1, -1, sizeof(nod1));
memset(nod2, -1, sizeof(nod2));
memset(bing, -1, sizeof(bing));
for(int i = 0;i < n; i++)
nod2_num[i] = 1;
for(int i = 0; i < m ; i ++){
cin >> edg3[i].x >> edg3[i].y >> edg3[i].len;
addedg(edg1, nod1, edg3[i].x, edg3[i].y, edg3[i].len, &k1);
addedg(edg1, nod1, edg3[i].y, edg3[i].x, edg3[i].len, &k1);
}
sort(edg3,edg3 + m);
}
void fun(int x, int y, int len, int t){//遍历所有与点x相连接的点并且该点在 要合并的y的集合里
for(int k = nod1[x]; k != -1; k = edg1[k].next){
int v = edg1[k].id;
if(v == t) continue;
v = find(v);
if(v == y)
diff = min(edg1[k].key - len,diff);
}
}
void Kruskal(){
int x, y, num = 0;
for( int i = 0; i < m && num + 1 != n; i ++){
x = find(edg3[i].x);
y = find(edg3[i].y);
int t = edg3[i].y;
if(x != y){
mst += edg3[i].len;
if(nod2_num[x] > nod2_num[y]){
swap(x, y);
t = edg3[i].x;
}
fun(x, y, edg3[i].len, t);
for(int j =nod2[x]; j != -1; j = edg2[j].next){
fun(edg2[j].id, y, edg3[i].len, t);
}
for(int j = nod2[x]; j != -1; j = edg2[j].next){//合并要合并的两个子树
addedg(edg2, nod2, y, edg2[j].id, 0, &k2);
}
bing[x] = y;
}
}
}
void pri(){
if(diff) cout << mst << endl;
else cout<<"Not Unique!"<<endl;
}
int main(){
//freopen("1.txt","r",stdin);
int T; cin >> T;
while(T --){
cin >> n >> m;
init();
Kruskal();
pri();
}
}