zoukankan      html  css  js  c++  java
  • 图基本算法 最小生成树 Prim算法(邻接表/邻接矩阵+优先队列STL)

      这篇文章是对《算法导论》上Prim算法求无向连通图最小生成树的一个总结,其中有关于我的一点点小看法。

      最小生成树的具体问题可以用下面的语言阐述:
        输入:一个无向带权图G=(V,E),对于每一条边(u, v)属于E,都有一个权值w。

        输出:这个图的最小生成树,即一棵连接所有顶点的树,且这棵树中的边的权值的和最小。

      举例如下,求下图的最小生成树:

      这个问题是求解一个最优解的过程。那么怎样才算最优呢?

      首先我们考虑最优子结构:如果一个问题的最优解中包含了子问题的最优解,则该问题具有最优子结构。

      最小生成树是满足最优子结构的,下面会给出证明:

      最优子结构描述:假设我们已经得到了一个图的最小生成树(MST) T,(u, v)是这棵树中的任意一条边。如图所示:

        现在我们把这条边移除,就得到了两科子树T1和T2,如图:

        T1是图G1=(V1, E1)的最小生成树,G1是由T1的顶点导出的图G的子图,E1={(x, y)∈E, x, y ∈V1}

        同理可得T2是图G2=(V2, E2)的最小生成树,G2是由T2的顶点导出的图G的子图,E2={(x, y)∈E, x, y ∈V2}

      现在我们来证明上述结论:使用剪贴法。w(T)表示T树的权值和。

        首先权值关系满足:w(T) = w(u, v)+w(T1)+w(T2)

        假设存在一棵树T1'比T1更适合图G1,那么就存在T'={(u,v)}UT1'UT2',那么T'就会比T更适合图G,这与T是最优解相矛盾。得证。

      因此最小生成树具有最优子结构,那么它是否还具有重叠子问题性质呢?我们可以发现,不管删除那条边,上述的最优子结构性质都满足,都可以同样求解,因此是满足重叠子问题性质的。

      考虑到这,我们可能会想:那就说明最小生成树可以用动态规划来做咯?对,可以,但是它的代价是很高的。

      我们还能发现,它还有个更强大的性质:贪心选择性质。因而可用贪心算法完成。

      贪心算法特点:一个局部最优解也是全局最优解。

      最小生成树的贪心选择性质:令T为图G的最小生成树,另A⊆V,假设边(u, v)∈E是连接着A到A的补集(也就是V-A)的最小权值边,那么(u, v)属于最小生成树。

      证明:假设(u, v)∉T, 使用剪贴法。现在对下图进行分析,图中A的点用空心点表示,V-A的点用实心点表示:

      在T树中,考虑从u到v的一条简单路径(注意现在(u, v)不在T中),根据树的性质,它是唯一的。

        现在把(u, v)和这条路上中的第一条连接A和V-A的边交换,即画红杠的那条边,边(u, v)是连接A和V-A的权值最小边,那我们就得到了一棵更小的树,这就与T是最小  生成树矛盾。得证。

      现在呢,我们来看看Prim的思想:Prim算法的特点是集合E中的边总是形成单棵树。树从任意根顶点s开始,并逐渐形成,直至该树覆盖了V中所有顶点。每次添加到树中的边都是使树的权值尽可能小的边。因而上述策略是“贪心”的。

      算法的输入是无向连通图G=(V, E)和待生成的最小生成树的根r。在算法的执行过程中,不在树中的所有顶点都放在一个基于key域的最小优先级队列Q中。对每个顶点v来说,key[v]是所有将v与树中某一顶点相连的边中的最小权值;按规定如果不存在这样的边,则key[v]=∞。

      实现Prim算法的伪代码如下所示:

      MST-PRIM(G, w, r)

        for each u∈V

          do key[u] ← ∞

             parent[u]← NIL

        key[r] ← 0

        Q ← V

        while Q ≠∅

          do u ← EXTRACT-MIN(Q)

            for each v∈Adj[u]

              do if v∈Q and w(u, v) < key[v]

                then parent[v] ← u

                    key[v] ← w(u, v)

        其工作流程为:

          (1)首先进行初始化操作,将所有顶点入优先队列,队列的优先级为权值越小优先级越高

          (2)取队列顶端的点u,找到所有与它相邻且不在树中的顶点v,如果w(u, v) < key[v],说明这条边比之前的更优,加入到树中,即更改父节点和key值。这中间还    隐含着更新Q的操作(降key值)

          (3)重复2操作,直至队列空为止。

          (4)最后我们就得到了两个数组,key[v]表示树中连接v顶点的最小权值边的权值,parent[v]表示v的父结点。

        现在呢,我们发现一个问题,这里要用到优先队列来实现这个算法,而且每次搜索邻接表都要进行队列更新的操作。

          不管用什么方法,总共用时为O(V*T(EXTRACTION)+E*T(DECREASE))

          (1)如果用数组来实现,总时间复杂度为O(V2)

          (2)如果用二叉堆来实现,总时间复杂度为O(ElogV)

          (3)如果使用斐波那契堆,总时间复杂度为O(E+VlogV)

        上面的三种方法,越往下时间复杂度越好,但是实现难度越高,而且每次对最小优先队列的更新是非常麻烦的,那么,有没有一种方法,可以不更新优先队列也达到同样的  效果呢?

        答案是:有。

        其实只需要简单的操作就可以达到。首次只将根结点入队列。第一次循环,取出队列顶结点,将其退队列,之后找到队列顶的结点的所有相邻顶点,若有更新,则更新它们的key值后,再将它们压入队列。重复操作直至队列空为止。因为对树的更新是局部的,所以只需将相邻顶点key值更新即可。push操作的复杂度为O(logV),而且省去了之前将所有顶点入队列的时间,因而总复杂度为O(ElogV)。

      具体实现代码,优先队列可以用STL实现:

      1 #include <iostream>
      2 #include <cstdio>
      3 #include <vector>
      4 #include <queue>
      5 using namespace std;
      6 
      7 #define maxn 110  //最大顶点个数
      8 int n, m;       //顶点数,边数
      9 
     10 struct arcnode  //边结点
     11 {
     12     int vertex;     //与表头结点相邻的顶点编号
     13     int weight;     //连接两顶点的边的权值
     14     arcnode * next; //指向下一相邻接点
     15     arcnode() {}
     16     arcnode(int v,int w):vertex(v),weight(w),next(NULL) {}
     17 };
     18 
     19 struct vernode      //顶点结点,为每一条邻接表的表头结点
     20 {
     21     int vex;    //当前定点编号
     22     arcnode * firarc;   //与该顶点相连的第一个顶点组成的边
     23 }Ver[maxn];
     24 
     25 void Init()  //建立图的邻接表需要先初始化,建立顶点结点
     26 {
     27     for(int i = 1; i <= n; i++)
     28     {
     29         Ver[i].vex = i;
     30         Ver[i].firarc = NULL;
     31     }
     32 }
     33 
     34 void Insert(int a, int b, int w)  //尾插法,插入以a为起点,b为终点,权为w的边,效率不如头插,但是可以去重边
     35 {
     36     arcnode * q = new arcnode(b, w);
     37     if(Ver[a].firarc == NULL)
     38         Ver[a].firarc = q;
     39     else
     40     {
     41         arcnode * p = Ver[a].firarc;
     42         if(p->vertex == b)
     43         {
     44             if(p->weight > w)
     45                 p->weight = w;
     46             return ;
     47         }
     48         while(p->next != NULL)
     49         {
     50             if(p->next->vertex == b)
     51             {
     52                 if(p->next->weight > w);
     53                     p->next->weight = w;
     54                 return ;
     55             }
     56             p = p->next;
     57         }
     58         p->next = q;
     59     }
     60 }
     61 void Insert2(int a, int b, int w)   //头插法,效率更高,但不能去重边
     62 {
     63     arcnode * q = new arcnode(b, w);
     64     if(Ver[a].firarc == NULL)
     65         Ver[a].firarc = q;
     66     else
     67     {
     68         arcnode * p = Ver[a].firarc;
     69         q->next = p;
     70         Ver[a].firarc = q;
     71     }
     72 }
     73 struct node     //保存key值的结点
     74 {
     75     int v;
     76     int key;
     77     friend bool operator<(node a, node b)   //自定义优先级,key小的优先
     78     {
     79         return a.key > b.key;
     80     }
     81 };
     82 
     83 #define INF 0xfffff    //权值上限
     84 int parent[maxn];   //每个结点的父节点
     85 bool visited[maxn]; //是否已经加入树种
     86 node vx[maxn];      //保存每个结点与其父节点连接边的权值
     87 priority_queue<node> q; //优先队列stl实现
     88 void Prim()    //s表示根结点
     89 {
     90     for(int i = 1; i <= n; i++) //初始化
     91     {
     92         vx[i].v = i;
     93         vx[i].key = INF;
     94         parent[i] = -1;
     95         visited[i] = false;
     96     }
     97     vx[1].key = 0;
     98     q.push(vx[1]);
     99     while(!q.empty())
    100     {
    101         node nd = q.top();  //取队首,记得赶紧pop掉
    102         q.pop();
    103         if(visited[nd.v])   //注意这一句的深意,避免很多不必要的操作
    104             continue;
    105         visited[nd.v] = true;
    106         arcnode * p = Ver[nd.v].firarc;
    107         while(p != NULL)    //找到所有相邻结点,若未访问,则入队列
    108         {
    109             if(!visited[p->vertex] && p->weight < vx[p->vertex].key)
    110             {
    111                 parent[p->vertex] = nd.v;
    112                 vx[p->vertex].key = p->weight;
    113                 vx[p->vertex].v = p->vertex;
    114                 q.push(vx[p->vertex]);
    115             }
    116             p = p->next;
    117         }
    118     }
    119 }
    120 
    121 int main()
    122 {
    123     int a, b ,w;
    124     cout << "输入n和m: ";
    125     cin >> n >> m;
    126     Init();
    127     cout << "输入所有的边:" << endl;
    128     while(m--)
    129     {
    130         cin >> a >> b >> w;
    131         Insert2(a, b, w);
    132         Insert2(b, a, w);
    133     }
    134     Prim();
    135     cout << "输出所有结点的父结点:" << endl;
    136     for(int i = 1; i <= n; i++)
    137         cout << parent[i] << " ";
    138     cout << endl;
    139     cout << "最小生成树权值为:";
    140     int cnt = 0;
    141     for(int i = 1; i <= n; i++)
    142         cnt += vx[i].key;
    143     cout << cnt << endl;
    144     return 0;
    145 }

    (当明确知道没有重边时,用Insert2()进行插入能提高效率)

    运行结果如下(基于第一个例子):

    可用下列题进行测试:HDU搜索“畅通工程” POJ 1251

    接下来是邻接矩阵实现,非常简单,但是有几点还是需要注意的:

     1 #include <iostream>
     2 #include <cstdio>
     3 #include <queue>
     4 using namespace std;
     5 
     6 #define maxn 110
     7 #define INF 100020    //预定于的最大值
     8 int n;   //顶点数、边数
     9 int g[maxn][maxn];      //邻接矩阵表示
    10 
    11 struct node     //保存key值的结点
    12 {
    13     int v;
    14     int key;
    15     friend bool operator<(node a, node b)   //自定义优先级,key小的优先
    16     {
    17         return a.key > b.key;
    18     }
    19 };
    20 int parent[maxn];   //每个结点的父节点
    21 bool visited[maxn]; //是否已经加入树种
    22 node vx[maxn];      //保存每个结点与其父节点连接边的权值
    23 priority_queue<node> q; //优先队列stl实现
    24 void Prim()    //s表示根结点
    25 {
    26     for(int i = 1; i <= n; i++) //初始化
    27     {
    28         vx[i].v = i;
    29         vx[i].key = INF;
    30         parent[i] = -1;
    31         visited[i] = false;
    32     }
    33     vx[1].key = 0;
    34     q.push(vx[1]);
    35     while(!q.empty())
    36     {
    37         node nd = q.top();  //取队首,记得赶紧pop掉
    38         q.pop();
    39         if(visited[nd.v] == true)   //深意,因为push机器的可能是重复但是权值不同的点,我们只取最小的
    40             continue;
    41         int st = nd.v;
    42         //cout << nd.v << " " << nd.key << endl;
    43         visited[nd.v] = true;
    44         for(int j = 1;  j <= n; j++)
    45         {
    46             if(j!=st && !visited[j] && g[st][j] < vx[j].key)    //判断
    47             {
    48                 parent[j] = st;
    49                 vx[j].key = g[st][j];
    50                 q.push(vx[j]);
    51 
    52             }
    53         }
    54     }
    55 }
    56 int main()
    57 {
    58     while(~scanf("%d", &n))  //点的个数
    59     {
    60         for(int i = 1; i <= n; i++)  //输入邻接矩阵
    61             for(int j = 1; j <= n; j++)
    62             {
    63                 scanf("%d", &g[i][j]);
    64                 if(g[i][j] == 0)
    65                     g[i][j] = INF;  //注意0的地方置为INF
    66             }
    67         Prim();  //调用
    68         int ans = 0;  //权值和
    69         for(int i = 1; i <= n; i++)
    70             ans += vx[i].key;
    71         printf("%d
    ", ans);
    72 
    73     }
    74     return 0;
    75 }

    题目:POJ 1258

    望支持,谢谢。

  • 相关阅读:
    Java实现 LeetCode 30 串联所有单词的子串
    Java实现 LeetCode 29 两数相除
    Java实现 LeetCode 29 两数相除
    Java实现 LeetCode 29 两数相除
    Java实现 LeetCode 28 实现strStr()
    Java实现 LeetCode 28 实现strStr()
    Java实现 LeetCode 28 实现strStr()
    Java实现 LeetCode 27 移除元素
    Java实现 LeetCode 27 移除元素
    字符编码终极笔记:ASCII、Unicode、UTF-8、UTF-16、UCS、BOM、Endian
  • 原文地址:https://www.cnblogs.com/dzkang2011/p/prim_1.html
Copyright © 2011-2022 走看看