poj 1679 The Unique MST
题目大意:给一个连通图,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(); } }