数据结构与算法系列研究七——图prim算法dijkstra算法

Posted 精心出精品

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法系列研究七——图prim算法dijkstra算法相关的知识,希望对你有一定的参考价值。

图、prim算法、dijkstra算法

一、图的定义

 图(Graph)可以简单表示为G=<V, E>,其中V称为顶点(vertex)集合,E称为边(edge)集合。图论中的图(graph)表示的是顶点之间的邻接关系。

(1) 无向图(undirect graph)
      E中的每条边不带方向,称为无向图。
(2) 有向图(direct graph)
      E中的每条边具有方向,称为有向图。
(3) 混合图
       E中的一些边不带方向, 另一些边带有方向。
(4) 图的阶
      指图的顶点数目,即顶点集V中的元素个数。
(5) 多重图
      拥有平行边或自环的图。
(6) 简单图
      不含平行边和自环的图.

    (7) 边的表示方法与有关术语
      a. 无向图的边称为无向边(edge),它用无序偶表示

       称顶点vi与vj相互邻接或互为邻接点(adjacent);边(vi, vj)依附于(incident)顶点vi和vj或与顶点vi和vj相关联。
     b. 有向图的边称为有向边或弧(arc),它用有序偶表示

     称顶点vi为弧尾(tail)或始点(initial node),顶点vj为弧头(head)或终端点(terminal node) ;vi邻接至vj,而vj邻接自vi;弧<vi, vj>依附于或关联顶点vi和vj。
  (8) 顶点的度(degree)
     a. 无向图顶点的度定义为与该顶点相关联的边的数目;
       性质1:无向图中,各顶点的度数和等于边数的2倍。
    b. 有向图顶点的度定义为与该顶度相关联的弧的数目。   
       即,有向图顶点的度=入度(indegree)+出度(outdegree),其中入度定义为连接该顶点的弧头的数目;出度定义为连接该顶点的弧尾的数目。
      性质2:有向图中,顶点的入度和=出度和=弧的数目。
  (9) 完全图
     a. n阶无向简单图中,若每个顶点的度均为n-1,称该图为无向完全图。

       性质3:n阶无向完全图边的数目为
     b. n阶有向简单图中,若每个顶点的入度=出度=n-1,称该图为n阶有向完全图。
       性质4:n阶有向完全图弧的数目为n(n-1)。
  (10) 网(Network)
      若图中的边带有权重(weight),称为网。边上的权重一般代表某种代价或耗费。比如:顶点表示城市,边表示城市间的公路,则权重可以用公路里程来表示。若边上的权重为无穷大,一般表示代价无穷大等含义。
  (11) 稀疏图(sparse graph)与稠密图(dense graph)
        若无向图或有向图有e条边或弧,若e很小(如e<nlog2n),称为稀疏图,否则称为稠密图。
  (12) 子图
        对于图G=<V, E>,若有另一图G\'=<V\', E\'>满足,称图G\'为G的子图。

二、 图的路径

  (1)路径(path)
        图G=<V, E>中,从任一顶点开始,由边或弧的邻接至关系构成的有限长顶点序列称为路径。注意:
          有向图的路径必须沿弧的方向构成顶点序列;
          构成路径的顶点可能重复出现(即允许反复绕圈)。
   (2) 路径长度
       路径中边或弧的数目。
   (3) 简单路径
        除第一个和最后一个顶点外,路径中无其它重复出现的顶点,称为简单路径。
   (4) 回路或环(cycle)
        路径中的第一个顶点和最后一个顶点相同时,称为回路或环。

三、图的连通性

   (1) 无向连通图:在无向图中,若从顶点vi到vj有路径,则称vi和vj是连通的。若该图中任意两个顶点都是连通的,则称它是连通图。
   (2) 连通分量:无向图中的极大连通子图(包括子图中的所有顶点和所有边)称为连通分量或连通分支。连通图也可以定义为连通分支数等于1的图。
  

 

   (3) 有向连通图
        在有向图中,任一对顶点vi和vj(vi不等于vj),若从vi到vj以及从vj到vi均连通(即存在路径),称它是强连通的。
   (4) 强连通分量
        有向图中的极大强连通子图称为强连通分量。
   性质1:有向强连通图的充要条件是该图存在一个回路经过每个顶点至少1次。
   性质2:n阶无向连通图中至少有n-1条边; n阶有向连通图中至少有n条边。
   例如,3个顶点组成的最小无向和有向连通图

  (5) 生成树
        一个n阶连通图的生成树是一个极小连通子图,它包含图中全部n个顶点以及保证该子图是连通图的最少的n-1条边。
        性质3:在生成树上增加任何一条边,必形成回路。
  (6) 有向树与生成森林
       如果一个有向图恰有一个顶点入度为0,其余顶点的入度均为1,则是一棵有向树。一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有中以构成若干棵不相交的有向树的弧。

四、图的存储结构

 1.邻接矩阵:若n阶图表示为G=<V, E>,其中V={v0, v1, …, vn-1},则定义

若图G为n阶网,则定义:

其中,wij为边(vi, vj)或弧<vi, vj>上的权重。
无向简单图邻接矩阵的性质:
    关于主对角线对称,即A=AT;
    主对角线元素全为0;
    矩阵中1的数目=边数的2倍;
    第i行1的数目=第i列1的数目=顶点vi的度。
2. 邻接表与逆邻接表:若n阶图表示为G=<V, E>,其中V={v0, v1, …, vn-1},则可用链表实现图的存储结构。
(1)、邻接表:
    a. 无向图:关联顶点vi的所有边组成的集合用单链表实现存储,头结点存储顶点vi的编号和信息,其余结点存储邻接于顶点vi的其它顶点的编号、边的权重和信息。这样共形成n个单链表,称为邻接表。
    b. 有向图:以顶点vi为弧尾的所有弧组成的集合用单链表实现存储,头结点存储弧尾vi的编号和信息,其余结点存储弧头顶点编号、弧的权重和信息。
头结点(存储顶点vi):

1 typedef struct
2 { //顶点数据(可选)
3    ElemTp data;
4    //顶点信息(可选)
5    InfoTp info;
6    int i;  //顶点下标
7    ArcNode *firstarc;
8 } HNode;
View Code

表结点(存储边或弧):

1 typedef struct node
2 { //边或弧的权重(可选)
3    double w;
4    //边或弧的信息(可选)
5    InfoTp info;
6    int j;  //邻接点下标
7    struct node *nextarc;
8 } ArcNode;
View Code

整体数据结构:

#define MAX_N  最大顶点数
typedef  enum { DG, UDG, DN, UDN } GraphKind;
// DG:有向图, UDG:无向图, DN:有向网, UDN:无向网
typedef   struct
{  HNode h[MAX_N];  //头结点形成数组
    int n, e;   //n:实际顶点数; e:边或弧的数目
    Graphkind   kind;    //图的类型(可选)                                         
} ALGraph;
View Code

(2)、 逆邻接表
        有向图中,表结点存储邻接至顶点vi的所有弧,即头结点是弧头,表结点是弧尾。
无向图邻接表存储结构示意图:

特点:表结点数为边数的2倍;顶点vi的度为第i个单链表的表结点数。
有向图邻接表存储结构示意图:

特点:表结点数为弧的数目;顶点vi的出度为第i个单链表的表结点数。 (求入度不方便)
有向图逆邻接表存储结构示意图:

 

特点:表结点数为弧的数目;顶点vi的入度为第i个单链表的表结点数。(求出度不方便)

3. 有向图的十字链表
        每个表结点(弧<vi, vj>)在水平方向构成单链表,形成以vi为弧尾的所有弧组成的集合;
        每个表结点(弧<vi, vj>)在垂直方向构成单链表,形成以vj为弧头的所有弧组成的集合。

typedef struct
{ //顶点数据(可选)
   ElemTp data; 
   //顶点信息(可选)
   InfoTp info;
   int i;  //顶点下标
   OLANode *firstin; 
   OLANode *firstout;
} OLHNode;
typedef struct node
{ //弧的权重(可选)
   double w; 
   //弧的信息(可选)
   InfoTp info;
   int i, j;  //弧的端点下标
   struct node *hlink;
   struct node *vlink;
} OLANode;
typedef   struct
{  OLHNode h[MAX_N];  //头结点形成数组
    int n, e;   //n:实际顶点数; e:边或弧的数目
    Graphkind   kind;    //图类型(可选)                                         
} OLGraph;
View Code

十字链表特点:表结点数等于弧的数目;求入度和出度都很方便。
有向图十字链表存储结构示意图:

4. 无向图的邻接多重表
      采用类似十字链表的思想实现无向图存储。任意边(vi, vj)只存储一个表结点,每个表结点有inext和jnext两个指针域,inext指向关联于顶点vi的下一条边,而jnext指向关联于顶点vj的下一边条。顶点vi的头结点仅含一个指针域,指向关联于vi的第1条边。

邻接多重表存储结构示意图:

五、图的遍历和相关算法

1. 遍历的定义
      从图中某顶点出发,沿路径方向访问每个顶点一次且仅一次。
2. 图遍历算法的辅助数据结构
      为避免顶点重复访问,需定义一个顶点标志数组visited[0..n-1],标记每个顶点是否已访问。
3. 图的深度优先搜索(Depth First Search)算法
     搜索原则:沿出发顶点的第1条路径尽量深入,遍历路径上的所有顶点;然后退回到该顶点,搜索第2条, 第3条, …, 路径,直到以该顶点为始点的所有路径上的顶点都已访问过(这是递归算法)。对于非连通图,需从每个顶点出发,尝试深度优先遍历。

void DFStravel(Graph  &G)  //Graph为邻接矩阵
{  bool *visited=new bool[G.n];
    for(i=0; i<G.n; i++) visted[i]=false; 
    for(i=0; i<G.n; i++) //保证非连通图的遍历
        if (!visited[i])  DFS(G, i);
    delete []visited;
 }
void DFS(Graph  &G, int i) //从vi出发深度优先搜索
{  visit(i); visited[i]=true;
    for (j=First_Adj(G, i); j!=-1; j=Next_Adj(G, i, j))
       if (!visited[j])  DFS(G, j);
}
View Code

4. 图的宽度优先搜索(Breadth First Search)算法
搜索原则:

  1.  访问遍历出发顶点,该顶点入队;
  2.  队列不空,则队头顶点出队;
  3.  访问出队顶点所有的未访问邻接点并将访问的顶点入队;
  4.  重复(2), (3), 直到队列为空。

 以上为非递归算法,需设队列实现算法。对于非连通图,需从每个顶点出发,尝试宽度优先搜索。

 1 void BFStravel(Graph  &G)  //Graph为邻接矩阵
 2 {  bool *visited=new bool[G.n];
 3     for(i=0; i<G.n; i++) visted[i]=false;
 4     InitQuene(Q); 
 5     for(i=0; i<G.n; i++) 
 6        if (!visited[i])  
 7        { visit(i); visited[i]=true; enQueue(Q, i);
 8           while(!Empty(Q))
 9           { u=delQueue(Q); 
10              for(v=First_Adj(G,u);v!=-1;v=Next_Adj(G,u,v))
11                 if(!visited[v])
12                { visit(v); visted[v]=true; enQueue(Q, v); 
13                 } // end of if !visited[v]
14            }   // end of while  
15          }      // end of if !visited[i]
16   delete []visited; 
17 }
View Code

5. 求第1邻接点和下一个邻接点算法

 1 //邻接矩阵
 2 int First_Adj(Graph &G, int u)
 3 { for(v=0; v<G.n; v++) if(G.arcs[u][v]!=0) break;
 4    if(v<G.n) return v;
 5    return -1;
 6 }
 7 int  Next_Adj(Graph &G, int u, int v)
 8 { for(++v; v<G.n; v++) if(G.arcs[u][v]) break;
 9    if(v<G.n) return v;
10    return -1;
11 }
12 //邻接表和十字链表
13 for(v=First_Adj(G, u); v!=-1; v=Next_Adj(G, u, v))
14 //用以下循环语句代替
15 for(p=G.h[u].firstarc, v=p?p->j:-1; v!=-1;   \\
16 p=p->nextarc, v=p?p->j:-1)      
17 //若为十字链表,则用以下循环语句代替
18 for(p=G.h[u].firstout, v=p?p->j:-1; v!=-1;   \\
19 p=p->hlink, v=p?p->j:-1)
View Code

6. 图的遍历算法的复杂度

深度优先遍历顶点访问次序(从顶点v0出发):

求邻接点次序不同,可得到不同的访问序列,如:v0, v2, v5, v6, v1, v3, v7, v4等
宽度优先遍历顶点访问次序(从顶点v0出发):

给定存储结构示意图,则遍历次序唯一确定:

从0出发深度优先次序:
0, 1, 4, 2, 3
从0出发宽度优先次序:
0, 1, 3, 4, 2
7、连通性与最小生成树
  1. 连通性的判断方法
   无向图从任一顶点出发,若DFS或BFS可访问所有顶点,则该图是连通图;
   有向图从每个顶点出发,若DFS或BFS均可访问所有顶点,则该图是强连通图。
  2. 求连通分支  无向图DFSTravel或BFSTravel过程中,从顶点出发进行DFS或BFS的次数为连通分支数。
  3. 求生成树  DFSTravel或BFSTravel经历的路径和顶点构成连通分支的生成树森林。若图是连通的,则得到生成树。
  4.最小生成树的概念:对于带权无向图(无向网),其所有生成树中,边上权值之和最小的称为最小生成树。注意:最小生成树的构形不一定唯一。
  5.最小生成树生成算法的基本原理-MST性质
     MST性质:假设G=(V, E)是一个连通网,U是顶点V的一个非空子集。若(u, v)是满足条件u∈U且v∈V-U的所有边中一条具有最小权值的边,则必存在一棵包含边(u, v)的最小生成树。
  6.普里姆(Prim)算法
      算法思想:直接运用MST性质。
        假设G=(V, E)是连通网,TE是G上最小生成树中边的集合。算法从U={u0} (u0∈V)且TE={}开始,重复执行下列操作:
        在所有u∈U且v∈V-U的边(u, v) 中找一条权值最小的边(u\', v\')并入集合TE中,同时v\'并入U,直到V=U为止。
        最后,TE中必有n-1条边。T=(V, TE)就是G的一棵最小生成树。
       用Prim算法手工构造最小生成树:记为T1

    Prim算法的实现:
        设置辅助数组closedge[0..n-1],其中n表示无向连通网的顶点总数。
        设n个顶点组成的集合V={v0, v1, …, vn-1}且各顶点编号与closedge数组下标对应。若初始时U={v0},在Prim算法执行过程中,对任意顶点vi属于V-U,closedge[i]包含两个域,即

   若顶点vi已并入集合U,则令closedge[i].lowcost=0;
   若顶点vi在V-U中,且与U中每个顶点无边相边,可令closedge[i].lowcost=无穷。
   每趟从所有vi属于V-U中(closedge[i].lowcost>0表示vi属于V-U)选择lowcost最小的vi,将vi并入集合U。
   假设每趟并入U集合的顶点为vi,则
         a. 令closedge[i].lowcost=0;
         b. 调整其它lowcost>0的所有closedge元素,即
          对任意vj属于V-U,若cost(vi, vj)<closedge[j].lowcost,则更新 closedge[j].lowcost=cost(vi, vj);closedge[j].vex=i否则,closedge[j]不更新。

  T1的closedge数组动态变化过程:(vex, lowcost )

  7、克鲁斯卡尔(Kruskl)算法
      给定连通网N=(V, E),令最小生成树的初始状态为只有n个顶点而无边的非连通图T,图中每个顶点自成一个连通分量。在E中选择最小权重的边,若该边依附的顶点落在T中不同的连通分量中,则将该边加入到T中,否则舍去此边而选择下一条权重最小的边。依次类推,直到T中所有顶点都在同一连通分量上为止。
核心:每次选择一条具有最小权值、且跨越两个不同连通分量的边,使两个不同连通分量变成一个连通分量。

Kruskl算法:需使用堆和求等价类算法,不用掌握。
Prim和Kruskl算法的时间复杂度
   Prim:  T(n)=O(n2), 适合边多的稠密度
   Kruskl:  T(n)=O(elog2e),   适合边少的稀疏图
  8、最短路径的概念
    a.给定n阶有向或无向网N=(V,  E),其中,V={v0, v1, … , vn-1}。设P表示顶点vi到vj的一条路径中全部边(弧)组成的集合,则该条路径的带权路径长度定义为P中的所有边(弧)的权值之和。顶点vi到vj的最短路径是指vi到vj的所有路径中带权路径长度最小的路径。
   3点说明:
         顶点vi到vj的最短路径不一定唯一;
        若vi到vj不连通,则vi到vj的最短路径长度为无穷大;
        对于n阶无向网,顶点对的组合数为n(n-1)/2,即共有n(n-1)/2个最短路径;对于n阶有向网,则总共有n(n-1)个最短路径。
   b. 求最短路径的迪杰斯特拉算法(Dijkstra)
      算法说明:
        对于n阶网N=(V, E),Dijkstra算法按最短路径长度递增的次序求任意给定的某顶点(作为始点)到其它的n-1个顶点的最短路径。若需要求出全部顶点对间的最短路径,必须以每个顶点为源点应用Dijkstra算法n次。
       首先,引入辅助向量dist[0..n-1],该向量用于存储n-1条最短路径的长度。设始点为vk, 则算法结束后,dist[i](i不等于k)的值为始点vk至顶点vi的最短路径长度。
        初始化:dist[i]=wk,i   i=0, 1, 2, …, n-1
        其中,若vi邻接自vk,则wk,i为边上权值,否则w(k,i)=无穷大。
            第1步:求n-1个最短路径长度中的最小值以及对应路径终点
                显然,始点vk到其它n-1个顶点的最短路径的最小值应为依附于始点vk的所有边(弧)中权值的最小值,对应路径终点为该最小权值边(弧)依附的另一邻接点。
             故,最短的最短路径的终点下标可用下式计算。
                (1)式中,arg表示求下标i,使得i满足条件:dist[i]是所有dist[]中的最小值。
              总之,若下标j满足(1)式,则vk至vj的最短路径长度为dist[j],且dist[j]是n-1个最短路径中长度最短的。
            第2步:循环n-2趟(m=1, 2, … , n-2),
              按长度递增次序生成其它最短路径
              若视算法第1步为第0趟,记第m(m=0, 1, … , n-2)趟生成的最短路径终点下标为jm,则必须使
          

六、实验实现Prim算法

6.1.实验内容
   用prim算法实现最小生成树。
6.2.输入与输出
  输入:采用文件形式输入图的节点数,弧的数目,用三元组的形式输入弧的两个节点以及权重。
  输出:通过输出链接生成树的节点的次序以及对应边的权重得到最小生成树。
6.3.关键数据结构与算法描述
  关键数据结构:无向图的数据结构,closedge数组的数据结构。具体如下:

/***********************************************/
typedef struct network
{
    int n;                //实际节点数
    int arcnum;           //弧的数目
    double w[MAXSIZE][MAXSIZE];//权重
}Network;//构建带有权重的网络图结构
typedef  struct
{
    double lowcost; //节点的最小权重域
    int      vex;   //节点的对应顶点位置
} CD_TP;     
 /***********************************************/
View Code

算法描述:
    Prim算法的原理为构建closedge数组,每个节点有两个域,分别为对应于生成树的最小权重的节点域以及该节点和最小生成树对应的最小权重数lowcost。通过n-1次遍历,每次遍历都要加入一个与已有节点相邻的最小顶点,然后更新剩余节点的与最小生成树对应的最小权重,以便进行下次遍历,经过n-1次遍历之后得到n-1个与初始顶点相关的节点,同时也就是得到了n-1条弧,构成n个节点的最小生成树。具体算法如下:

/****************************************************/
for(i=0; i<G.n; i++)   //从k号顶点出发
   {
       closedge[i].vex=k;
       closedge[i].lowcost=G.w[k][i]; //定第k行,按行遍历
   }     
   cout<<"生成树按照从第"<<k+1<<"节点依次连接的节点为"<<endl;
   closedge[k].lowcost=0; //使第k行的权重由无穷大变为0,加入生成树
   cout<<k+1;              //输出k号顶点,因从0开始
   for(m=0; m<G.n-1; m++)  //n-1趟循环
   {
       for(i=0; i<G.n; i++)
           if(closedge[i].lowcost>0)
               break;
        for(j=i+1; j<G.n; j++)
           if(closedge[j].lowcost>0&&
               closedge[j].lowcost<closedge[i].lowcost)
               i=j; //找到生成树外的最小权重作为添加对象

         cout<<","<<i+1; //输出i号顶点,因从0开始
         closedge[i].lowcost=0;//添加进入生成树
         for(j=0; j<G.n; j++)
           if(closedge[j].lowcost>0&&
              closedge[j].lowcost>G.w[i][j])         
           {
               closedge[j].lowcost=G.w[i][j]; //更新符合条件closedge的最小权重域
               closedge[j].vex=i;       //同时更新对应的节点关联到目前最小权重关联点i
           }            
    }  
     cout<<endl<<"该生成树有"<<G.n<<"个节点,"<<G.arcnum<<"条弧"<<endl;
     cout<<"生成树的"<<G.n-1<<"条边及其权重为:"<<endl;
     for(i=0; i<G.n; i++)
     if(i!=k) //k为起始节点,与自身相隔无穷大
     {
         cout<<"("<<i+1<<","<<closedge[i].vex+1<<")";
         cout<<"-"<<G.w[i][closedge[i].vex]<<endl;//与上面两点对应
     }
     delete []closedge;
/****************************************************/
View Code

6.4.理论与测试
  对下图,经过5次遍历即可得到最小生成树:

测试:在文件中输入如下信息:

以上是关于数据结构与算法系列研究七——图prim算法dijkstra算法的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法系列----最小生成树(Prim算法&Kruskal算法)

图分析的22种算法与图形理解

C++ 图进阶系列之 kruskal 和 Prim 算法_图向最小生成树的华丽转身

[从今天开始修炼数据结构]图的最小生成树 —— 最清楚易懂的Prim算法和kruskal算法讲解和实现

最小生成树prim算法

(学习1)最小生成树-Prim算法与Kruskal算法