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

    望支持,谢谢。

  • 相关阅读:
    突然又想起了这首诗
    安装使用Androidx86打造快速流畅的Aandroid开发环境!
    解决电脑没插网线虚拟机无法桥接到主机
    使用WordPress更新通知服务,让搜索引擎知道你更新了,加快收录。
    php+apache+mysql环境配置时apache服务不能开启的解决
    WordPress备份的七种办法
    怎么在网站中正确使用JQuery代码
    如何让自己的博客在各搜索引擎中被搜索出来与快速收录
    轻松查看文件被哪个进程使用
    解密QQ非会员漫游聊天记录
  • 原文地址:https://www.cnblogs.com/dzkang2011/p/prim_1.html
Copyright © 2011-2022 走看看