[关闭]
@DATASOURCE 2015-08-07T09:13:33.000000Z 字数 20496 阅读 2082

生成树

图论


最小生成树

概述

在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边(即),而 w(u, v) 代表此边的权重,若存在 T 为 E 的子集(即)且为无循环图,使得 的 w(T) 最小,则此 T 为 G 的最小生成树。
最小生成树其实是最小权重生成树的简称。 许多应用问题都是一个求无向连通图的最小生成树问题。例如:要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同;另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。 

性质

• 最小生成树的边数必然是顶点数减一,|E| = |V| - 1。
• 最小生成树不可以有环。
• 最小生成树不必是唯一的。

Prim 算法

描述

普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克发现;并在1957年由美国计算机科学家罗伯特·普里姆独立发现;1959年,艾兹格·迪科斯彻再次发现了该算法。因此,在某些场合,普里姆算法又被称为DJP算法、亚尔尼克算法或普里姆-亚尔尼克算法。

主要操作

从单一顶点开始,Prim算法按照以下步骤逐步扩大树中所含顶点的数目,直到遍及连通图的所有顶点。
  • 输入:一个加权连通图,其中顶点集合为V,边集合为E;
  • 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {};
  • 重复下列操作,直到Vnew = V:

    • 在集合E中选取权值最小的边(u, v),其中u为集合Vnew中的元素,而v则不是(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
    • 将v加入集合Vnew中,将(u, v)加入集合Enew中;
  • 输出:使用集合Vnew和Enew来描述所得到的最小生成树。过程如图所示:

复杂度分析

通过邻接矩阵图表示的简易实现中,找到所有最小权边共需O(V2)的运行时间。使用简单的二叉堆与邻接表来表示的话,普里姆算法的运行时间则可缩减为O(E log V),其中E为连通图的边数,V为顶点数。如果使用较为复杂的斐波那契堆,则可将运行时间进一步缩短为O(E + V log V),这在连通图足够密集时(当E满足Ω(V log V)条件时),可较显著地提高运行速度。

代码示例

  1. //邻接矩阵:
  2. #define N 10000
  3. #define M 0x3f3f3f3f
  4. int n; // n 是点的数目;
  5. int map[N][N], low[N], div[N]; // map[N][N] 存储点和权值,low[N]中存储未收录到树的
  6. int prim(){ // 点到树的最小距离,div记录已经收录到树中的点。
  7. int i, j, min, minp, sum = 0;
  8. for(i = 0; i < n; i++) // 初始化div[N] 数组的值 0 为未收录该点,1 为收录。
  9. div[i] = 0;
  10. div[0] = 1;
  11. minp = 0;
  12. for(i = 1; i < n; i++) // 第一次初始化low[N] 数组,存储个点到minp的距离
  13. low[i] = map[minp][i];
  14. for(i = 1; i < n; i++){
  15. min = M;
  16. for(j = 0; j < n; j++)
  17. if(div[j] == 0 && min > low[j]){ // 找出距离minp 最小的点,用minp记录,并记距离
  18. min = low[j];
  19. minp = j;
  20. }
  21. div[minp] = 1; // 上一个过程中找到的点被收录到树中,并加上权值
  22. sum += min;
  23. for(j = 0; j < n; j++){ // 更新low[N] 数组,使其始终保持未收录到树中的点
  24. if(div[j] == 0 && low[j] > map[minp][j]) // 到树的最小距离。
  25. low[j] = map[minp][j];
  26. }
  27. }
  28. return sum;
  29. }
  1. //邻接表:
  2. #include <queue>
  3. #include <cstdio>
  4. #include <iostream>
  5. #define N 100
  6. #define M (1 << 30)
  7. using namespace std;
  8. struct Edge{
  9. int v, w, n;
  10. };
  11. struct Node{
  12. int v, w;
  13. friend bool operator < (const Node &a, const Node &b){
  14. return a.w > b.w;
  15. }
  16. };
  17. int head[N], dis[N], n, m, e;
  18. bool vis[N];
  19. Edge edge[N * N / 2];
  20. priority_queue < Node > que;
  21. void init(){
  22. e = 0;
  23. while(!que.empty()) que.pop();
  24. fill(head, head + N, -1);
  25. fill(dis, dis + N, M);
  26. fill(vis, vis + N, false);
  27. }
  28. void addedge(int u, int v, int w){
  29. edge[e].v = v;
  30. edge[e].w = w;
  31. edge[e].n = head[u];
  32. head[u] = e++;
  33. }
  34. int prime(){
  35. int ans = 0;
  36. Node t;
  37. t.v = 1;
  38. t.w = 0;
  39. que.push(t);
  40. int nume = 0;
  41. while(!que.empty() && nume <= m){
  42. t = que.top();
  43. que.pop();
  44. if(vis[t.v]) continue;
  45. ans += t.w;
  46. vis[t.v] = true;
  47. nume++;
  48. for(int i = head[t.v]; i != -1; i = edge[i].n)
  49. if(!vis[edge[i].v]){
  50. Node tt;
  51. tt.v = edge[i].v;
  52. tt.w = edge[i].w;
  53. que.push(tt);
  54. }
  55. }
  56. If(nume == n) return ans;
  57. return -1;
  58. }
  59. int main(){
  60. while(scanf("%d", &n) != EOF && n){
  61. scanf("%d", &m);
  62. init();
  63. int a, b, w;
  64. for(int i = 0; i < m; i++){
  65. scanf("%d%d%d", &a, &b, &w);
  66. addedge(a, b, w);
  67. addedge(b, a, w);
  68. }
  69. printf("%d\n", prime());
  70. }
  71. return 0;
  72. }

Kruskal 算法

描述

假设 WN=(V,{E}) 是一个含有 n 个顶点的连通网,则按照克鲁斯卡尔算法构造最小生成树的过程为:先构造一个只含 n 个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有 n 棵树的一个森林。之后,从网的边集 E 中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图,也就是说,将这两个顶点分别所在的两棵树合成一棵树;反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。依次类推,直至森林中只有一棵树,也即子图中含有 n-1条边为止。

主要操作

1).记Graph中有v个顶点,e个边

2).新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边

3).将原图Graph中所有e个边按权值从小到大排序

4).循环:从权值最小的边开始遍历每条边 直至图Graph中所有的节点都在同一个连通分量中
if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中添加这条边到图Graphnew中;

过程如图所示:

复杂度分析

从Kruskal算法中可以看到,执行该算法时间主要花费在堆排序和单层循环上,而循环是线性级的,则可以认为时间复杂性主要花费在堆排序上,由堆排序算法可知,Kruskal算法的时间复杂度为O(eloge),其中e为图的边数。

代码示例:

  1. #define M 10000 // M 为最多的点的数目
  2. struct maps{ // 定义新类型,maps x,y代表两个点,w为他们的权值;
  3. int x, y, w;
  4. };
  5. maps map[M * M];
  6. int par[M], n, ne; // n 为实际点的数目,ne为两个不同的点构成的边的数目
  7. bool cmp(const maps& a, const maps& b){ // sort 函数调用将边按权值由小到大排序
  8. return a.w < b.w;
  9. }
  10. void init(){ // 以下为并查集内容
  11. int i;
  12. for(i = 1; i <= n; i++)
  13. par[i] = i;
  14. }
  15. int find(int x){
  16. if(par[x] != x)
  17. par[x] = find(par[x]);
  18. return par[x];
  19. }
  20. bool unit(int x, int y){
  21. int fx = find(x);
  22. int fy = find(y);
  23. if(fx != fy){
  24. par[fy] = fx;
  25. return true;
  26. }
  27. return false;
  28. }
  29. int kruscal(){
  30. int i, sum = 0;
  31. b = 0; // b为加入树中的边的数目;
  32. sort(map, map + ne, cmp); // 排序;
  33. for(i = 1; i <= ne; i++){
  34. if(unit(map[i].x, map[i].y)){ // 当两点至少有一个未收录到树中时,向树中加入该边
  35. sum += map[i].w; // 这样做是为了防止树中出现环路;
  36. b++;
  37. if(b == n - 1) // 当边数比点数少1时,所有的点被收录到树中;
  38. break;
  39. }
  40. }
  41. return sum;
  42. }

POJ 例题分析

POJ 1751
  • 题目大意:一个有n个城市的国家,已知有些城市有道路联通,问增加哪些道路使得所有的城市都可以彼此联通且代价最小,已经代价是两个城市坐标的笛卡尔距离;

    • 输入:n 表示有九个点,接下来n行将输入这就个点的坐标,之后会有一个整数q代表已经有q条边连接,接下来q行将输入这q条边所连接的点(例如: 1 2 代表上面所输入的n个坐标的第一个和第二个点已经连接。
    • 输出:输出还需要连接的点,用空格隔开(已经连号的点不能输出)。
  • 解题思路:Prim 算法,因为这道题目已经有一些边连起来了,故在初始化map数组时,连起来的边要置为零,由于该题要求输出被连接的两个点,在算法内部可以用一个数组来记录当前正在验证的边的端点,此外,已经连接的点不能输出,故当权值为零时则不输出。

  1. //代码示例:
  2. #include<iostream>
  3. #include<iomanip>
  4. #define M 800
  5. #define MAXINT 1<<28
  6. using namespace std;
  7. struct a{ // 定义结构体来存放点的坐标;
  8. int x , y;
  9. };
  10. int map[M][M], low[M];
  11. int vis[M], p[M]; // 数组 P 用来存放 low 数组中权值所对应的点
  12. int n, q;
  13. a s[M];
  14. void prim(){
  15. int i, j, minp, f = 0; // f用来标记权值是否为零;
  16. int min;
  17. for(i = 1; i <= n; i++)
  18. vis[i] = 0;
  19. vis[1] = 1;
  20. minp = 1;
  21. for(i = 2; i <= n; i++){
  22. p[i] = minp; // p 数组用来存放 low 数组中的权值所对应的点
  23. low[i] = map[minp][i];
  24. }
  25. for(i = 1; i < n; i++){
  26. min = MAXINT;
  27. for(j = 1; j <= n; j++)
  28. if(vis[j] == 0 && min > low[j] && minp != j){ // minp != j 即跳过边的两端为同一个点的情况
  29. minp = j;
  30. min = low[j];
  31. if(map[p[j]][minp] == 0) // 若权值为零,则标记下来;
  32. f = 1;
  33. else
  34. f = 0;
  35. }
  36. vis[minp] = 1;
  37. if(f == 0) // 权值为零时不输出;
  38. cout<< p[minp] <<" "<< minp <<endl;
  39. mina = minp;
  40. for(j = 1; j <= n; j++)
  41. if(vis[j] == 0 && map[minp][j] < low[j]){
  42. p[j] = minp;
  43. low[j] = map[minp][j];
  44. }
  45. }
  46. }
  47. int main(){
  48. int i, j, a, b;
  49. int l;
  50. cin >> n;
  51. for(i = 1; i <= n; i++)
  52. cin >> s[i].x >> s[i].y;
  53. for(i = 1; i <= n; i++){
  54. for(j = 1; j <= n; j++){
  55. l = (s[i].x - s[j].x) * (s[i].x - s[j].x) + (s[i].y - s[j].y) * (s[i].y - s[j].y);
  56. map[i][j] = map[j][i] = l; // 注意无向图应该两个方向的值相同;
  57. }
  58. }
  59. cin >> q;
  60. for(i = 1; i <= q; i++){
  61. cin >> a >> b;
  62. map[a][b] = map[b][a] = 0; // 已经连起来的边权值为零;
  63. }
  64. prim();
  65. return 0;
  66. }
Kruskal 算法,注意在本题上的使用技巧,题目要求不输出已经连接的边的端点,故可以在初始化的时候将连接好的端点加入集合中,这样在kruskal算法执行过程中就不会涉及到那些边。
  1. //代码示例:
  2. #include<cstdio>
  3. #include<stdio.h>
  4. #include<algorithm>
  5. #include<iostream>
  6. #define N 755
  7. using namespace std;
  8. struct maps{
  9. int x, y, w;
  10. };
  11. struct s{ // 存储点的坐标
  12. int x, y;
  13. };
  14. bool cmp(const maps& a, const maps& b){
  15. return a.w < b.w;
  16. }
  17. maps map[N * N];
  18. s s[N];
  19. int par[N];
  20. int n, ne, b;
  21. void init(){
  22. int i;
  23. for(i = 1; i <= n; i++)
  24. par[i] = i;
  25. }
  26. int find(int x){
  27. if(par[x] != x)
  28. par[x] = find(par[x]);
  29. return par[x];
  30. }
  31. bool unit(int x, int y){
  32. int fx = find(x);
  33. int fy = find(y);
  34. if(fx != fy){
  35. par[fy] = fx;
  36. return true;
  37. }
  38. return false;
  39. }
  40. void kruscal(){
  41. int i, sum = 0;
  42. b = 0;
  43. sort(map, map + ne, cmp);
  44. for(i = 1; i <= ne; i++){
  45. if(unit(map[i].x, map[i].y)){
  46. cout << map[i].x << " " << map[i].y << endl;
  47. b++;
  48. if(b == n - 1)
  49. break;
  50. }
  51. }
  52. }
  53. int main(){
  54. int i, j, l, q, a;
  55. ne = 0;
  56. ios_base::sync_with_stdio(false);
  57. cin >> n;
  58. init();
  59. for(i = 1; i <= n; i++)
  60. cin >> s[i].x >> s[i].y;
  61. for(i = 1; i <= n; i++)
  62. for(j = 1; j <= n; j++)
  63. if(i != j){ // 避免重(chong)点
  64. l = (s[i].x - s[j].x) * (s[i].x - s[j].x) + (s[i].y - s[j].y) * (s[i].y - s[j].y);
  65. map[ne].x = i;
  66. map[ne].y = j;
  67. map[ne].w = l;
  68. ne++;
  69. }
  70. cin >> q;
  71. for(i = 0; i < q; i++){
  72. cin >> a >> b;
  73. par[find(b)] = par[find(a)]; // 已经连起来的边的端点收录到集合中;
  74. b++; // 记录边的条数;
  75. }
  76. kruscal();
  77. return 0;
  78. }
POJ 2966


  • 题目大意:一个图中有几个连通分支(并不是每个点与其他点都有连接,只有一部分边) 。你可以连通任意两个点。求在满足使这个图成为连通图的前提下,使你所连的两点间的边的sum(权值)最小。

    • 输入:第一行为case个数,每个case里的第一行为两个数N和E分别表示点的个数和边的条数,接下来的E行是两个点和他们的权值。
      *输出:每个case输出一行,即连接这些点的最小权值。
    • 解题思路: Prim 算法,最小生成树最基本的题目,只要把map数组初始化为很大的值,就可以在程序运行中避开那些不能连接的边。
  1. //代码示例:
  2. #include <iostream>
  3. #define M 505
  4. #define MAXINT 1000
  5. using namespace std;
  6. int map[M][M], low[M], vis[M];
  7. int t, e, n;
  8. void prim(){
  9. int i, j, min, minp, key = 0;
  10. min = MAXINT;
  11. for(i = 0; i < n; i++)
  12. vis[i] = 0;
  13. vis[0] = 1;
  14. minp = 0;
  15. for(i = 0; i < n; i++)
  16. low[i] = map[minp][i];
  17. for(i = 0; i < n - 1; i++){
  18. min = MAXINT;
  19. for(j = 0; j < n; j++)
  20. if(min > low[j] && vis[j] == 0){
  21. min = low[j];
  22. minp = j;
  23. }
  24. key += min;
  25. vis[minp] = 1;
  26. for(j = 0; j < n; j++)
  27. if(vis[j] == 0 && map[minp][j] < low[j])
  28. low[j] = map[minp][j];
  29. }
  30. cout<< key <<endl;
  31. }
  32. int main(){
  33. ios_base::sync_with_stdio(false);
  34. int i, j, k, c;
  35. cin >> t;
  36. while(t--){
  37. for(i = 0; i < M; i++)
  38. for(j = 0; j < M; j++)
  39. map[i][j] = MAXINT; // 初始化为很大的值
  40. cin >> n >> e;
  41. for(k = 0; k < e; k++){
  42. cin >> i >> j;
  43. cin >> c;
  44. map[j][i] = map[i][j] = c;
  45. }
  46. prim();
  47. }
  48. return 0;
  49. }
Kruskal 算法:没技巧,直接套模版。
  1. //代码示例:
  2. #include<iostream>
  3. #include<algorithm>
  4. #include<cstdio>
  5. #define N 505
  6. using namespace std;
  7. struct maps{
  8. int x, y, w;
  9. };
  10. maps map[N * N];
  11. int par[N], n, e;
  12. bool cmp(const maps& a, const maps& b){
  13. return a.w < b.w;
  14. }
  15. void init(){
  16. int i;
  17. for(i = 0; i <= n; i++){
  18. par[i] = i;
  19. }
  20. }
  21. int find(int x){
  22. if(par[x] != x)
  23. par[x] = find(par[x]);
  24. return par[x];
  25. }
  26. bool unit(int x, int y){
  27. int fx = find(x);
  28. int fy = find(y);
  29. if(fx != fy){
  30. par[fy] = fx;
  31. return true;
  32. }
  33. return false;
  34. }
  35. void kruscal(){
  36. int i, b = 0, sum = 0;
  37. init();
  38. sort(map, map + e, cmp);
  39. for(i = 0; i < e; i++)
  40. if(unit(map[i].x, map[i].y)){
  41. sum += map[i].w;
  42. b++;
  43. if(b == n - 1){
  44. printf("%d\n", sum);
  45. break;
  46. }
  47. }
  48. }
  49. int main(){
  50. int i, j, t, x, y, cost;
  51. scanf("%d", &t);
  52. while(t--){
  53. scanf("%d %d", &n, &e);
  54. for(i = 0; i < e; i++){
  55. scanf("%d %d %d", &x, &y, &cost);
  56. map[i].x = x;
  57. map[i].y = y;
  58. map[i].w = cost;
  59. }
  60. kruscal();
  61. }
  62. return 0;
  63. }

次小生成树(k小生成树)

概述

给出一个带边权的无向图G,设其最小生成树为T,求出图G的与T不完全相同的边权和最小的生成树(即G的次小生成树)。一个无向图的两棵生成树不完全相同,当且仅当这两棵树中至少有一条边不同。注意,图G可能不连通,可能有平行边,但一定没有自环(其实对于自环也很好处理:直接舍弃。因为生成树中不可能出现自环)。定义生成树T的一个可行变换(-E1, +E2):将T中的边E1删除后,再加入边E2(满足边E2原来不在T中但在G中),若得到的仍然是图G的一棵生成树,则该变换为可行变换,该可行变换的代价为(E2权值 - E1权值)。这样,很容易证明,G的次小生成树就是由G的最小生成树经过一个代价最小的可行变换得到。进一步可以发现,这个代价最小的可行变换中加入的边E2的两端点如果为V1和V2,那么E1一定是原来最小生成树中从V1到V2的路径上的权值最大的边。

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的权值次小的边的权值。

代码示例

  1. #include <ctime>
  2. #include <cstdio>
  3. #include <iostream>
  4. #include <algorithm>
  5. #define M (1 << 30)
  6. #define N 2010
  7. using namespace std;
  8. int mp[N][N], mp1[N][N], ma[N][N], pre[N], dis[N], n, m, ans, res;
  9. bool vis[N]; //mp1 存储次小边,ma存储每一条路径的最大值 pre 前驱
  10. void init(){
  11. fill(mp[0], mp[N], M);
  12. fill(mp1[0], mp1[N], M);
  13. fill(ma[0], ma[N], 0);
  14. fill(dis, dis + N, M);
  15. fill(vis, vis + N, false);
  16. scanf("%d%d", &n, &m);
  17. int u, v, w;
  18. for(int i = 0; i < m; i++){
  19. scanf("%d%d%d", &u, &v, &w);
  20. if(w < mp[u][v]){
  21. mp1[u][v] = mp[u][v];
  22. mp[u][v] = w;
  23. mp1[v][u] = mp1[u][v];
  24. mp[v][u] = mp[u][v];
  25. }else if(w < mp1[u][v]){
  26. mp1[u][v] = w;
  27. mp1[v][u] = mp1[u][v];
  28. }
  29. }
  30. }
  31. int prim(int s){
  32. ans = 0, res = M;
  33. int mi, mip;
  34. for(int i = 1; i <= n; i++){
  35. dis[i] = mp[s][i];
  36. pre[i] = s;
  37. }
  38. vis[s] = true;
  39. for(int i = 1; i < n; i++){
  40. mi = M, mip = 0;
  41. for(int j = 1; j <= n; j++){
  42. if(!vis[j] && dis[j] < mi){
  43. mi = dis[j];
  44. s = pre[j];
  45. mip = j;
  46. }
  47. }
  48. if(mi == M) return 0; // 返回 0 时,说明不存在最小生成树
  49. ans += mi;
  50. vis[mip] = true;
  51. mp[s][mip] = M;
  52. mp[mip][s] = M;
  53. if(mp1[s][mip] < M && mp1[s][mip] - mi < res)
  54. res = mp1[s][mip] - mi;
  55. for(int j = 1; j <= n; j++)
  56. if(!vis[j] && dis[j] > mp[mip][j]){
  57. dis[j] = mp[mip][j];
  58. pre[j] = mip;
  59. }
  60. for(int j = 1; j <= n; j++)
  61. if(vis[j] && j != s && j != mip){
  62. ma[j][mip] = max(ma[j][s], mi);
  63. ma[mip][j] = ma[j][mip];
  64. }
  65. }
  66. for(int i = 1; i <= n; i++)
  67. for(int j = 1; j <= n; j++)
  68. if(i != j && mp[i][j] != M)
  69. res = min(res, mp[i][j] - ma[i][j]);
  70. if(res == M) return 1; // 返回 1 时, 说明不存在次小生成树
  71. res += ans; // 之前的 res 都代表 E2-E1
  72. return 2; // 返回 2 时, 说明存在次小生成树
  73. }
  74. int main(){
  75. int t;
  76. //clock_t start = clock();
  77. scanf("%d", &t);
  78. for(int i = 1; i <= t; i++){
  79. init();
  80. int ch = prim(1);
  81. printf("Case %d: ", i);
  82. if(ch == 0) printf("-1\n");
  83. else if(ch == 1) printf("%d %d\n", ans, -1);
  84. else printf("%d %d\n", ans, res);
  85. }
  86. //clock_t stop = clock();
  87. //printf("Time = %.2lfMS\n", (double)(stop - start) / CLOCKS_PER_SEC * 1000);
  88. return 0;
  89. }
  1. //拓展只求两个点间的最大边
  2. int prime(int s){
  3. int mi, mip, ans;
  4. mip = s;
  5. vis[mip] = 1;
  6. for(int i = 0; i < n; i++)
  7. low[i] = mp[mip][i], pre[i] = s;
  8. for(int i = 1; i < n; i++){
  9. mi = MAX;
  10. for(int j = 0; j < n; j++)
  11. if(!vis[j] && mi > low[j])
  12. mi = low[j], mip = j, s = pre[j];
  13. ans += mi;
  14. vis[mip] = 1;
  15. for(int j = 0; j < n; j++){
  16. if(vis[j] && j != mip){
  17. ma[j][mip] = ma[j][s] > mi ? ma[j][s] : mi;
  18. ma[mip][j] = ma[j][mip];
  19. } if(!vis[j] && low[j] > mp[mip][j])
  20. low[j] = mp[mip][j], pre[j] = mip;
  21. }
  22. }
  23. return ans;
  24. }

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了。

代码示例

  1. #include <iostream>
  2. #include <stdlib.h>
  3. using namespace std;
  4. #define re(i, n) for (int i=0; i<n; i++)
  5. #define re3(i, l, r) for (int i=l; i<=r; i++)
  6. const int MAXN = 7000, MAXM = 130000, INF = ~0U >> 2;
  7. struct edge {
  8. int a, b, len, pre, next;
  9. } ed[MAXM + MAXM];
  10. struct edge2 {
  11. int a, b, len, No;
  12. } ed2[MAXM];
  13. int n, m = 0, m2, u[MAXN], ch[MAXN], nx[MAXN], q[MAXN], res1 = 0, res2 = INF;
  14. void init_d(){
  15. for(int i - 0; i < n; i++){
  16. ed[i].a = ed[i].pre = ed[i].next = i;
  17. if (n % 2) m = n + 1;
  18. else m = n;
  19. }
  20. void add_edge(int a, int b, int l) {
  21. ed[m].a = a; ed[m].b = b; ed[m].len = l; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
  22. ed[m].a = b; ed[m].b = a; ed[m].len = l; ed[m].pre = ed[b].pre; ed[m].next = b; ed[b].pre = m; ed[ed[m].pre].next = m++;
  23. }
  24. void del_edge(int No){
  25. ed[ed[No].pre].next = ed[No].next;
  26. ed[ed[No].next].pre = ed[No].pre;
  27. ed[ed[No ^ 1].pre].next = ed[No ^ 1].next;
  28. ed[ed[No ^ 1].next].pre = ed[No ^ 1].pre;
  29. }
  30. void init() {
  31. scanf("%d%d", &n, &m2);
  32. if (!m2) {
  33. if (n > 1) res1 = -INF;
  34. res2 = -INF;
  35. return;
  36. }
  37. init_d();
  38. int a, b, len;
  39. for(int i = 0; i < m2; i++){
  40. scanf("%d%d%d", &a, &b, &len);
  41. ed2[i].No = m; add_edge(--a, --b, len);
  42. ed2[i].a = a; ed2[i].b = b; ed2[i].len = len;
  43. }
  44. }
  45. int cmp(const void *s1, const void *s2) {
  46. return ((edge2 *)s1)->len - ((edge2 *)s2)->len;
  47. }
  48. void prepare() {
  49. for(int i = 0; i < n; i++){
  50. u[i] = ch[i] = nx[i] = -1;
  51. qsort(ed2, m2, 16, cmp);
  52. }
  53. int find(int x){
  54. int r = x, r0 = x, tmp;
  55. while (u[r] >= 0) r = u[r];
  56. while (u[r0] >= 0) {
  57. tmp = u[r0];
  58. u[r0] = r;
  59. r0 = tmp;
  60. }
  61. return r;
  62. }
  63. void uni(int r1, int r2, int No, int l0) {
  64. q[0] = r1;
  65. int j, k, l1, front, rear;
  66. for (front = 0, rear = 0; front <= rear; front++) {
  67. j = ch[q[front]];
  68. while (j != -1) {
  69. q[++rear] = j;
  70. j = nx[j];
  71. }
  72. }
  73. for(int i = 0; i <= rear; i++){
  74. j = q[i];
  75. for (int p=ed[j].next; p != j; p=ed[p].next) {
  76. k = ed[p].b;
  77. if (p != No && find(k) == r2) {
  78. l1 = ed[p].len - l0;
  79. if (l1 < res2) res2 = l1;
  80. del_edge(p);
  81. }
  82. }
  83. }
  84. u[r2] += u[r1]; u[r1] = r2; nx[r1] = ch[r2]; ch[r2] = r1;
  85. }
  86. void solve() {
  87. int r1, r2, l0, num = 0;
  88. for(int i = 0; i < m2; i++){
  89. r1 = find(ed2[i].a);
  90. r2 = find(ed2[i].b);
  91. if (r1 != r2) {
  92. l0 = ed2[i].len;
  93. res1 += l0;
  94. num++;
  95. if (u[r1] >= u[r2]) uni(r1, r2, ed2[i].No, l0);
  96. else uni(r2, r1, ed2[i].No ^ 1, l0);
  97. }
  98. }
  99. if (num < n - 1) {
  100. res1 = res2 = -INF;
  101. return;
  102. }
  103. if (res2 == INF) res2 = -INF;
  104. else res2 += res1;
  105. }
  106. void pri() {
  107. printf("Cost: %d\nCost: %d\n", res1 == -INF ? -1 : res1, res2 == -INF ? -1 : res2);
  108. }
  109. int main() {
  110. init();
  111. if (!res1 && res2 == INF) {
  112. prepare();
  113. solve();
  114. }
  115. pri();
  116. return 0;
  117. }
  1. //另解
  2. //#include <ctime>
  3. #include <cstdio>
  4. #include <iostream>
  5. #include <algorithm>
  6. #define N 100100
  7. #define M (1 << 29)
  8. #define Max(a, b) (a > b ? a : b)
  9. using namespace std;
  10. struct Edge{
  11. int a, b, w;
  12. friend bool operator < (const Edge &a, const Edge &b){
  13. return a.w < b.w;
  14. }
  15. };
  16. class LCA{
  17. public:
  18. struct Edge{
  19. int v, n, w;
  20. void init(int a, int b, int c){
  21. v = a, n = b, w = c;
  22. }
  23. };
  24. Edge tedge[2 * N], qedge[2 * N], aedge[2 * N];
  25. int thead[N], qhead[N], ahead[N];
  26. int vis[N], par[N], ma[2 * N], ans[2 * N];
  27. int te, qe, ae;
  28. void init(){
  29. te = qe = ae = 0;
  30. fill(ma, ma + 2 * N, -M);
  31. for(int i = 0; i < N; i++){
  32. par[i] = i;
  33. thead[i] = qhead[i] = ahead[i] = -1;
  34. //ma[i] = -M;
  35. vis[i] = false;
  36. }
  37. }
  38. int find(int x){
  39. int temp = par[x];
  40. if(x != temp) par[x] = find(par[x]);
  41. ma[x] = Max(ma[x], ma[temp]);
  42. return par[x];
  43. }
  44. void addEdge(int *head, Edge *edge, int u, int v, int w, int &e){
  45. edge[e].v = v, edge[e].w = w, edge[e].n = head[u], head[u] = e++;
  46. if(head == ahead) return;
  47. edge[e].v = u, edge[e].w = w, edge[e].n = head[v], head[v] = e++;
  48. }
  49. void dfs(int a){
  50. vis[a] = 1;
  51. for(int i = qhead[a]; i + 1; i = qedge[i].n){
  52. int b = qedge[i].v;
  53. if(vis[b]){
  54. int temp = find(b);
  55. addEdge(ahead, aedge, temp, a, i, ae);
  56. }
  57. }
  58. for(int i = thead[a]; i + 1; i = tedge[i].n){
  59. int b = tedge[i].v;
  60. if(vis[b] == 0){
  61. dfs(b);
  62. par[b] = a;
  63. ma[b] = tedge[i].w;
  64. }
  65. }
  66. for(int i = ahead[a]; i + 1; i = aedge[i].n){
  67. int x = aedge[i].v;
  68. int t = aedge[i].w;
  69. int y = qedge[t].v;
  70. find(x);
  71. find(y);
  72. ans[qedge[t].w] = Max(ma[x], ma[y]);
  73. }
  74. }
  75. }lca;
  76. int par[N], n, m;
  77. Edge edge[2 * N];
  78. bool vis[N];
  79. void init(){
  80. for(int i = 0; i < N; i++)
  81. par[i] = i, vis[i] = false;
  82. }
  83. int find(int x){
  84. if(par[x] != x) par[x] = find(par[x]);
  85. return par[x];
  86. }
  87. bool Union(int x, int y){
  88. int fx = find(x);
  89. int fy = find(y);
  90. if(fx == fy) return false;
  91. par[fx] = fy;
  92. return true;
  93. }
  94. int Kruscal(){
  95. int ne = 0, ans = 0;
  96. sort(edge, edge + m);
  97. for(int i = 0; i < m; i++)
  98. if(Union(edge[i].a, edge[i].b)){
  99. ans += edge[i].w;
  100. ne++;
  101. vis[i] = true;
  102. lca.addEdge(lca.thead, lca.tedge, edge[i].a, edge[i].b, edge[i].w, lca.te);
  103. if(ne == n - 1) break;
  104. }
  105. if(ne == n - 1) return ans;
  106. return -1;
  107. }
  108. int Ckruscal(){
  109. int res = M, me = 0, ne = 0;
  110. for(int i = 0; i < m; i++)
  111. if(!vis[i]) lca.addEdge(lca.qhead, lca.qedge, edge[i].a, edge[i].b, me++, lca.qe);
  112. lca.dfs(1);
  113. for(int i = 0; i < m; i++)
  114. if(!vis[i]){
  115. if(edge[i].w - lca.ans[ne] < res) res = edge[i].w - lca.ans[ne];
  116. ne++;
  117. }
  118. if(res < M) return res;
  119. return -1;
  120. }
  121. int main(){
  122. //clock_t start = clock();
  123. int t;
  124. scanf("%d", &t);
  125. for(int j = 1; j <= t; j++){
  126. //printf("Case %d: ", j);
  127. scanf("%d%d", &n, &m);
  128. int a, b, w;
  129. init();
  130. lca.init();
  131. for(int i = 0; i < m; i++){
  132. scanf("%d%d%d", &a, &b, &w);
  133. edge[i].a = a, edge[i].b = b, edge[i].w = w;
  134. }
  135. int ans = Kruscal();
  136. if(ans == -1){
  137. printf("%d\n", ans);
  138. continue;
  139. }else{
  140. int res = Ckruscal();
  141. printf("%d %d\n", ans, res == -1 ? -1 : ans + res);
  142. }
  143. }
  144. //clock_t stop = clock();
  145. //printf("Time = %.2lfMS\n", (double)(stop - start) / CLOCKS_PER_SEC * 1000);
  146. return 0;
  147. }

复杂度分析

效率分析:可以证明,如果每次都选取点少的连通块,Kruskal算法求次小生成树的时间复杂度为O(M*(logN+logM)),空间复杂度为O(M)。

总结:显然Prim适用于稠密图,而Kruskal适用于稀疏图。

生成树个数

Matrix-Tree定理(Kirchhoff矩阵-树定理)。Matrix-Tree定理是解决生成树计数问题最有力的武器之一。它首先于1847年被Kirchhoff证明。在介绍定理之前,我们首先明确几个概念:
1、G的度数矩阵D[G]是一个n*n的矩阵,并且满足:当i≠j时,dij=0;当i=j时,dij等于vi的度数。

2、G的邻接矩阵A[G]也是一个n*n的矩阵, 并且满足:如果vi、vj之间有边直接相连,则aij=1,否则为0。

我们定义G的Kirchhoff矩阵(也称为拉普拉斯算子)C[G]为C[G]=D[G]-A[G],则Matrix-Tree定理可以描述为:G的所有不同的生成树的个数等于其Kirchhoff矩阵C[G]任何一个n-1阶主子式的行列式的绝对值。所谓n-1阶主子式,就是对于r(1≤r≤n),将C[G]的第r行、第r列同时去掉后得到的新矩阵,用Cr[G]表示。

最小生成树的个数

算法思想:

抛开“最小”的限制不看,如果只要求求出所有生成树的个数,是可以利用Matrix-Tree定理解决的;  Matrix-Tree定理此定理利用图的Kirchhoff矩阵,可以在O(N3)时间内求出生成树的个数;  
kruskal算法:
将图G={V,E}中的所有边按照长度由小到大进行排序,等长的边可以按照任意顺序; 
初始化图G’为{V,?},从前向后扫描排序后的边,如果扫描到的边e在G’中连接了两个相异的连通块,则将它插入G’中; 
最后得到的图G’就是图G的最小生成树; 

由于kruskal按照任意顺序对等长的边进行排序,则应该将所有长度为L0的边的处理当作一个阶段来整体看待; 

令kruskal处理完这一个阶段后得到的图为G0,如果按照不同的顺序对等长的边进行排序,得到的G0也是不同; 

虽然G0可以随排序方式的不同而不同,但它们的连通性都是一样的,都和F0的连通性相同(F0表示插入所有长度为L0的边后形成的图); 

在kruskal算法中的任意时刻,并不需要关注G’的具体形态,而只要关注各个点的连通性如何(一般是用并查集表示); 

所以只要在扫描进行完第一阶段后点的连通性和F0相同,且是通过最小代价到达这一状态的,接下去都能找到最小生成树; 

经过上面的分析,可以看出第一个阶段和后面的工作是完全独立的; 

第一阶段需要完成的任务是使G0的连通性和F0一样,且只能使用最小的代价; 
计算出这一阶段的方案数,再乘上完成后续事情的方案数,就是最终答案; 
由于在第一个阶段中,选出的边数是一定的,所有边的长又都为L0; 
所以无论第一个阶段如何进行代价都是一样的,那么只需要计算方案数就行了; 
此时Matrix-Tree定理就可以派上用场了,只需对F0中的每一个连通块求生成树个数再相乘即可; 
代码示例
  1. //#include <ctime>
  2. #include <queue>
  3. #include <cstdio>
  4. #include <vector>
  5. #include <iostream>
  6. #include <algorithm>
  7. #define N 1100
  8. using namespace std;
  9. typedef long long LL;
  10. const int mod = 10000;
  11. struct Edge{
  12. int a, b, w;
  13. bool operator < (const Edge &a) const{
  14. return w < a.w;
  15. }
  16. };
  17. int n, m;
  18. LL par[N], fa[N], vis[N];
  19. LL mp[N][N], mp1[N][N];
  20. Edge edge[N];
  21. vector <int> vec[N];
  22. void Swap(LL &a, LL &b){
  23. a = a ^ b;
  24. b = a ^ b;
  25. a = a ^ b;
  26. }
  27. void init(){
  28. scanf("%d%d", &n, &m);
  29. fill(mp[0], mp[N], 0);
  30. for(int i = 0; i <= n; i++){
  31. vec[i].clear();
  32. par[i] = i;
  33. vis[i] = 0;
  34. }
  35. for(int i = 0; i < m; i++)
  36. scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].w);
  37. }
  38. int find(int x, LL father[]){
  39. if(x != father[x]) father[x] = find(father[x], father);
  40. return father[x];
  41. /*
  42. if(x == father[x]) return x;
  43. return find(father[x], father);
  44. */
  45. }
  46. LL Mtree(LL a[][N], int n){
  47. for(int i = 0; i < n; i++)
  48. for(int j = 0; j < n; j++)
  49. a[i][j] %= mod;
  50. int ans = 1;
  51. for(int i = 1; i < n; i++){
  52. for(int j = i + 1; j < n; j++)
  53. while(a[j][i]){
  54. int temp = a[i][i] / a[j][i];
  55. for(int k = i; k < n; k++)
  56. a[i][k] = (a[i][k] - a[j][k] * temp) % mod;
  57. for(int k = i; k < n; k++)
  58. Swap(a[i][k], a[j][k]);
  59. ans = -ans;
  60. }
  61. if(a[i][i] == 0) return 0;
  62. ans = ans * a[i][i] % mod;
  63. }
  64. return (ans + mod) % mod;
  65. }
  66. void Solve(){
  67. if(n == 1){
  68. printf("1\n");
  69. return;
  70. }
  71. sort(edge, edge + m);
  72. LL flag = -1;
  73. LL ans = 1;
  74. for(int k = 0; k <= m; k++){
  75. if(edge[k].w != flag || k == m){
  76. for(int i = 1; i <= n; i++){
  77. if(vis[i]){
  78. LL u = find(i, fa);
  79. vec[u].push_back(i);
  80. vis[i] = 0;
  81. }
  82. }
  83. for(int i = 1; i <= n; i++){
  84. if(vec[i].size() > 1){
  85. fill(mp1[0], mp1[n + 1], 0);
  86. int len = vec[i].size();
  87. for(int a = 0; a < len; a++){
  88. int a1 = vec[i][a];
  89. for(int b = a + 1; b < len; b++){
  90. int b1 = vec[i][b];
  91. mp1[a][b] = (mp1[b][a] -= mp[a1][b1]);
  92. mp1[a][a] += mp[a1][b1];
  93. mp1[b][b] += mp[a1][b1];
  94. }
  95. }
  96. LL res = (LL)Mtree(mp1, len);
  97. ans = (ans * res) % mod;
  98. for(int a = 0; a < len; a++)
  99. par[vec[i][a]] = i;
  100. }
  101. }
  102. for(int i = 1; i <= n; i++){
  103. fa[i] = par[i] = find(i, par);
  104. vec[i].clear();
  105. }
  106. if(k == m) break;
  107. flag = edge[k].w;
  108. }
  109. int a = edge[k].a;
  110. int b = edge[k].b;
  111. int a1 = find(a, par);
  112. int b1 = find(b, par);
  113. if(a1 == b1) continue;
  114. vis[a1] = vis[b1] = 1;
  115. fa[find(a1, fa)] = find(b1, fa);
  116. mp[a1][b1]++;
  117. mp[b1][a1]++;
  118. }
  119. int flag1 = 0;
  120. for(int i = 2; i <= n && !flag1; i++)
  121. if(fa[i] != fa[i - 1]) flag1 = 1;
  122. if(m == 0) flag1 = 1;
  123. printf("%lld\n", flag1 ? 0 : ans % mod);
  124. }
  125. int main(){
  126. //clock_t start = clock();
  127. int t;
  128. scanf("%d", &t);
  129. for(int i = 1; i <= t; i++){
  130. init();
  131. Solve();
  132. }
  133. //clock_t stop = clock();
  134. //printf("Time = %.2lfMS\n", (double)(stop - start) / CLOCKS_PER_SEC * 1000);
  135. return 0;
  136. }
  1. //另解:
  2. //#include <ctime>
  3. #include <queue>
  4. #include <cstdio>
  5. #include <vector>
  6. #include <iostream>
  7. #include <algorithm>
  8. #define N 550
  9. #define Mod 10000
  10. using namespace std;
  11. struct Edge{
  12. int a, b, w;
  13. friend bool operator < (const Edge &a, const Edge &b){
  14. return a.w < b.w;
  15. }
  16. };
  17. int par[N], fa[N], cnt[N], ne[N];
  18. int n, m, len, from, to;
  19. Edge edge[N];
  20. void init(int *par, int n){
  21. for(int i = 0; i <= n; i++)
  22. par[i] = i;
  23. }
  24. int find(int *par, int x){
  25. if(par[x] != x) par[x] = find(par, par[x]);
  26. return par[x];
  27. }
  28. void dfs(int pos, int num, int &ans){
  29. if(num == len){
  30. init(fa, N);
  31. for(int i = 0; i < from; i++){
  32. int fx = find(fa, edge[i].a);
  33. int fy = find(fa, edge[i].b);
  34. if(fx != fy) fa[fy] = fx;
  35. }
  36. for(int i = 0; i < len; i++){
  37. int di = ne[i];
  38. int fx = find(fa, edge[di].a);
  39. int fy = find(fa, edge[di].b);
  40. if(fx != fy) fa[fy] = fx;
  41. else return;
  42. }
  43. ans++;
  44. }else{
  45. for(int i = pos; i <= to; i++){
  46. ne[num] = i;
  47. dfs(i + 1, num + 1, ans);
  48. }
  49. }
  50. }
  51. void solve(){
  52. int ans = 1;
  53. from = to = 0;
  54. init(par, n);
  55. edge[m].w = -1;
  56. cnt[0] = 0;
  57. for(int i = 0; i < m; i++){
  58. int fx = find(par, edge[i].a);
  59. int fy = find(par, edge[i].b);
  60. if(i) cnt[i] = cnt[i - 1];
  61. if(fx != fy){
  62. par[fy] = fx;
  63. cnt[i]++;
  64. }
  65. if(edge[i].w != edge[i + 1].w){
  66. if(!from) len = cnt[i];
  67. else len = cnt[i] - cnt[from - 1];
  68. if(len == 0) continue;
  69. to = i;
  70. int c = 0;
  71. dfs(from, 0, c);
  72. from = i + 1;
  73. ans = ans * c % Mod;
  74. }
  75. }
  76. if(cnt[m - 1] == n - 1) printf("%d\n", ans);
  77. else printf("%d\n", 0);
  78. }
  79. int main(){
  80. int t;
  81. scanf("%d", &t);
  82. while(t--){
  83. scanf("%d%d", &n, &m);
  84. for(int i = 0; i < m; i++)
  85. scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].w);
  86. sort(edge, edge + m);
  87. solve();
  88. }
  89. return 0;
  90. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注