• DS博客作业04--图


    0.PTA得分截图

    1.本周学习总结

    1.1 总结图内容

    1.1.1 图的概念:

    图形结构是最普遍的一类数据结构,具有广泛的实际应用。
    图中数据元素的关系是多对多的关系,(线性结构一对一,树形结构一对多)

    1.1.2 图的定义和基本术语:

    图:顶点集 V 和顶点间的关系:边集合E组成的数据结构。

    • 图的逻辑结构描述:
      Graph = (V , E)

    G=(V1,E1)
    V1={A, B, C, D, E}
    E1={<A,B>, <A,E>, <B,C>, <C,D>,<D,B>,<D,A>, <E,C> }

    • 有向图和无向图:
      1.有向图:
      有向图称由顶点集和弧(“弧”是有方向的边,弧可以用尖括号表示。)集构成的图。
      eg:
      有向图G1:
      G1 = (V1, E1)

      V1={A, B, C, E, F}
      E1={<A,B>, <A,E>, <B,C>, <C,F>,<F,B>,<F,A>, <E,C> }

    2.无向图:
    无向图:没方向边(若<v, w>E 必有<w, v>VR。边可以用圆括号表示。)
    eg:
    无向图G2:
    G2 = (V2, E2)

    V2={A, B, C, D, E, F}
    E2={(A,B), (A,E), (B,E), (C,D), (D,F),(B,F), (C,F) }

    • 图的基本术语:
    1. 端点和邻接点:
      无向图:若存在一条边(i,j),则称顶点i和顶点j互为邻接点。
      有向图:存在一条边<i,j>,则称此边是顶点i的一条出边,同时也是顶点j的一条入边;称顶点i 和顶点j 互为邻接点。

    2、顶点的度、入度和出度:
    无向图:以顶点i为端点的边数称为该顶点的度。
    有向图:以顶点i为终点的入边的数目,称为该顶点的入度。以顶点i为始点的出边的数目,称为该顶点的出度。一个顶点的入度与出度的和为该顶点的度。
    若一个图中有n个顶点和e条边,每个顶点的度为di(0≤i≤n-1),则有:e=(d0+d1+d2+...+dn-1)/2

    3、完全图:
    无向图:每两个顶点之间都存在着一条边,称为完全无向图, 包含有n(n-1)/2条边。
    有向图:每两个顶点之间都存在着方向相反的两条边,称为完全有向图,包含有n(n-1)条边。

    4、稠密图、稀疏图:
    当一个图接近完全图时,则称为稠密图。
    相反,当一个图含有较少的边数(即当e<<n(n-1))时,则称为稀疏图。

    5、子图:
    设有两个图G=(V,E)和G'=(V',E'),若V'是V的子集,即V'V,且E'是E的子集,即E'E,则称G'是G的子图。

    1. 路径和路径长度:
      从顶点i到顶点j的一条路径是一个顶点序列(i,i1,i2,…,im,j),序列中边(i,i1),(i1,i2),…,(im-1,im),(im,j)属于E(G);
      路径长度是指一条路径上经过的边的数目。
      简单路径:一条路径上除开始点和结束点可以相同外,其余顶点均不相同,

    2. 回路或环:
      回路或环:一条路径上的开始点与结束点为同一个顶点。
      简单回路或简单环:开始点与结束点相同的简单路径。

    8、连通、连通图和连通分量:
    无向图:若从顶点i到顶点j有路径,则称顶点i和j是连通的。
    连通图:若图中任意两个顶点都连通,否则称为非连通图。
    连通分量:无向图G中的极大连通子图。
    任何连通图的连通分量只有一个,即本身而非连通图有多个连通分量。
    有向图:若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。
    否则,其各个强连通子图称作它的强连通分量。
    *在一个非强连通中找强连通分量的方法:
    1.在图中找有向环。
    2.扩展该有向环:如果某个顶点到该环中任一顶点有路径,并且该环中任一顶点到这个顶点也有路径,则加入这个顶点。

    1. 权和网
      权:图中每一条边都可以附有一个对应的数值,这种与边相关的数值称为权。
      网:边上带有权的图称为带权图,也称作网。

    1.1.3 图存储结构

    • 图的两种主要存储结构:

    1.邻接矩阵(二维数组);

    • 邻接矩阵的主要特点: 
      一个图的邻接矩阵表示是唯一的。
      特别适合于稠密图的存储。(一个图的邻接矩阵表示是唯一的。特别适合于稠密图的存储。)

    • 邻接矩阵存储表示:
      1.顶点信息:记录各个顶点信息的顶点表。
      2.边或弧信息:各个顶点之间关系的邻接矩阵。
      设图 A = (V, E)是一个有 n 个顶点的图
      A.Vex[n]:表示顶点信息集
      二维数组 A.edge[n][n]表示边的关系
      如果<i,j>属于E,或者(i,j)属于E: A.edge[i][j]=1。否则A.edge[i][j]=0.

    • 邻接矩阵结构体定义:

    #define  MAXV  <最大顶点个数>	
    typedef struct 
    {    int no;			//顶点编号
         InfoType info;		//顶点其他信息
    } VertexType;//声明顶点的类型
    
    typedef struct  			//图的定义
    {    int edges[MAXV][MAXV]; 	//邻接矩阵
         int n,e;  			//顶点数,边数
         VertexType vexs[MAXV];	//存放顶点信息
    }  MatGraph;//声明的邻接矩阵类型
    
     MatGraph g;//声明邻接矩阵存储的图
    
    
    • 两种图的邻接矩阵:
      无向图对称:

      有向图非对称:

    • 网络的邻接矩阵:
      A[i][j]=W(i,j),如果i!=j且<i,j>∈E或(i,j)∈E
      A[i][j]=∞,否则,但是i!=j
      A[i][j]=0,对角线i==j
      eg:

    • 借助于邻接矩阵容易求得顶点的度:
      在无向图中,统计第i行(列)1的个数可得顶点i的度。
      即:顶点i的度=第i行或列总度数
      在有向图中:
      统计第i行1的个数可得顶点i的出度OD;
      统计第j列1的个数可得顶点j的入度ID。

    2.邻接表

    邻接表是对图中每个顶点i建立一个单链表,将顶点i的所有邻接点链起来。
    每个单链表上添加一个表头结点(表示顶点信息)。并将所有表头结点构成一个数组,下标为i的元素表示顶点i的表头结点。
    图的邻接表存储方法是一种顺序分配与链式分配相结合的存储方法。 

    • 图的邻接表存储类型定义:
    typedef struct Vnode
    {    Vertex data;			//顶点信息
         ArcNode *firstarc;		//指向第一条边
    }  VNode;
    
    typedef struct ANode
    {     int adjvex;			//该边的终点编号
          struct ANode *nextarc;	//指向下一条边的指针
          InfoType info;		//该边的权值等信息
    }  ArcNode;
    
    typedef struct 
    {     VNode adjlist[MAXV] ;	//邻接表
           int n,e;			//图中顶点数n和边数e
    } AdjGraph;
    AdjGraph *G;//声明一个邻接表存储的图G
    
    
    • 两种图的邻接表:
      无向图:

    有向图:

    • 带权值网络的邻接表:

    1.1.4 图的基本运算

    1.创建图并初始化:

    void CreateAdj(AdjGraph*& G, int n, int e) //创建图邻接表
    {
    	int i, j, a, b;
    	ArcNode* p;
    	G = new AdjGraph;
    	for (i = 0; i < n; i++)   G->adjlist[i].firstarc = NULL;
    	//给邻接表中所有头结点的指针域置初值
    	for (i = 1; i <= e; i++)		 //根据输入边建图      
    	{
    		cin >> a >> b;
    		p = new ArcNode;	//创建一个结点p
    		p->adjvex = b;		 //存放邻接点
    		p->nextarc = G->adjlist[a].firstarc;  //采用头插法插入结点p
    		G->adjlist[a].firstarc = p;
    	}
    	G->n = n; G->e = n;
    }
    

    2.输出图:

    void DispAdj(AdjGraph *G)	//输出邻接表G
    {      int i;
           ArcNode *p;
           for (i=0;i<G->n;i++)
           {	p=G->adjlist[i].firstarc;//访问第一个节点
    	printf("%3d: ",i);
    	while (p!=NULL)
    	{       printf("%3d[%d]→",p->adjvex,p->weight);
    	         p=p->nextarc;
    	}
    	printf("∧
    ");
           }
    }
    
    

    3.销毁图:

    void DestroyAdj(AdjGraph *&G)   //销毁邻接表
    {      int i; ArcNode *pre,*p;
           for (i=0;i<G->n;i++)		//扫描所有的单链表
           {	pre=G->adjlist[i].firstarc;//p指向第i个单链表的首结点
    	if (pre!=NULL)
    	{      p=pre->nextarc;
    	        while (p!=NULL)	//释放第i个单链表的所有边结点
    	        {	free(pre);
    		pre=p; p=p->nextarc;
    	         }
    	         delete pre;
    	}
            }
            delete G;			//释放头结点数组
    }
    
    
    • 邻接表的特点:
      1.邻接表表示不唯一。
      2.特别适合于稀疏图存储。 (邻接表的存储空间为O(n+e))

    • 邻接表和邻接矩阵的相互转换:
      1.邻接矩阵转邻接表:

    void MatToList(MGraph g,ALGraph *&G)
    //将邻接矩阵g转换成邻接表G
    {  int i,j,n=g.n; ArcNode *p; 	//n为顶点数
       G=new AdjGraph;
       for (i=0;i<n;i++)     //给所有头节点的指针域置初值
          G->adjlist[i].firstarc=NULL;
       for (i=0;i<n;i++)	 //检查邻接矩阵中每个元素
          for (j=n-1;j>=0;j--)
             if (g.edges[i][j]!=0) 			
             {  p=new ArcNode; //创建节点*p
    	      p->adjvex=j;
    	      p->nextarc=G->adjlist[i].firstarc;
                           //将*p链到链表头
    	      G->adjlist[i].firstarc=p;
    	   }
       G->n=n;G->e=g.e;
    }
    
    

    2.邻接表转邻接矩阵:

    void ListToMat(ALGraph *G,MGraph &g)
      {  int i,j,n=G->n;ArcNode *p;
         for (i=0;i<n;i++) 
         {  p=G->adjlist[i].firstarc;
            while (p!=NULL) 
    	  {   g.edges[i][p->adjvex]=1;
    	      p=p->nextarc;
    	  }
         }
         g.n=n;g.e=G->e;
      } 
    
    

    1.1.6 图遍历及应用:

    • 图的遍历的概念:
      从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次。

    • 图的两种遍历方式:
      1.深度优先遍历(DFS):
      *深度优先搜索遍历的过程是:
        (1)从图中某个初始顶点v出发,首先访问初始顶点v。
        (2)选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。 
       
      *深度优先遍历图的实质:对每个顶点查找其邻接点的过程。

    *DFS遍历(访问v节点,遍历v的邻接点w,若w未被访问,递归访问w节点):

    void DFS(ALGraph *G,int v)  
    {    ArcNode *p;
          visited[v]=1;                   //置已访问标记
          printf("%d  ",v); 		
          p=G->adjlist[v].firstarc;      	
         while (p!=NULL) 
    	{
                      if (visited[p->adjvex]==0)  DFS(G,p->adjvex);    
    	     p=p->nextarc;              	
    	}
    }
    
    

    邻接表:时间复杂度为O(n+e)
    邻接矩阵:O(n2)

    • 非连通图的深度优先搜索遍历:
      首先将图中每个顶点的访问标志设为 FALSE, 之后搜索图中每个顶点,如果未被访问,则以该顶点为起始点,进行深度优先搜索遍历,否则继续检查下一顶点。
    void DFSTraverse(Graph G) {
       // 对非连通图 G 作深度优先遍历。
      for (v=0; v<G.vexnum; ++v) 
         visited[v] = FALSE; // 访问标志数组初始化
      for (v=0; v<G.vexnum; ++v) 
         if (!visited[v])  DFS(G,v);  // 对尚未访问的顶点调用DFS
    }
    
    

    2.广度优先遍历(BFS):
    *广度优先搜索遍历的过程是:
    (1)访问初始点v,接着访问v的所有未被访问过的邻接点。
    (2)按照次序访问每一个顶点的所有未被访问过的邻接点。  
    (3)依次类推,直到图中所有顶点都被访问过为止。

    *eg:

    BFS序列:2 1 3 4 0
    类似树的层次遍历:队列

    *BFS搜索思路:

    建一个访问队列q
    访问v节点,加入队列q
    while(队列不空)
        取队头元素w
        遍历w的邻接表    
             取邻接点j
             若j未被访问,则加入队列q,并访问j。
    end while
    
    

    邻接表:时间复杂度为O(n+e)
    邻接矩阵:O(n2)

    *用BFS遍历非连通图:

    void  BFS1(AdjGraph *G)
    {      int i;
            for (i=0;i<G->n;i++)     //遍历所有未访问过的顶点
                 if (visited[i]==0) 
                      BFS(G,i);
    }
    
    

    *用DFS判断图是否连通:
    采用某种遍历方式来判断无向图G是否连通。这里用深度优先遍历方法,先给visited[]数组(为全局变量)置初值0,然后从0顶点开始遍历该图。
    在一次遍历之后,若所有顶点i的visited[i]均为1,则该图是连通的;否则不连通。

    int  visited[MAXV];
    bool Connect(AdjGraph *G) 	//判断无向图G的连通性
    {     int i;
          bool flag=true;
          for (i=0;i<G->n;i++)		 //visited数组置初值
    	visited[i]=0;
          DFS(G,0); 	//调用前面的中DSF算法,从顶点0开始深度优先遍历
          for (i=0;i<G->n;i++)
                if (visited[i]==0)
               {     flag=false;
    	   break;
               }
          return flag;
    }
    
    

    *如何查找图路径:
    利用回溯的深度优先遍历方法。
    从顶点u开始进行深度优先遍历。增加path和d记录存走过的路径。
    若当前扫描的顶点u = v时,表示找到了一条路径,则输出路径path。
    当从顶点u出发的路径找完后,置visited[u]=0,即回溯。

    void FindAllPath(AGraph *G,int u,int v,int path[],int d)
    { //d表示path中的路径长度,初始为-1
      int w,i;  ArcNode *p;
      d++; path[d]=u;		//路径长度d增1,顶点u加入到路径中
      visited[u]=1;		//置已访问标记
      if (u==v && d>=1)		//找到一条路径则输出
            {	for (i=0;i<=d;i++)
    	    printf("%2d",path[i]);
    	printf("
    ");
            }
            p=G->adjlist[u].firstarc;	//p指向顶点u的第一个相邻点
            while (p!=NULL)
            {	 w=p->adjvex;		//w为顶点u的相邻顶点
    	 if (visited[w]==0)	//若w顶点未访问,递归访问它
    	     FindAllPath(G,w,v,path,d);
    	 p=p->nextarc;		//p指向顶点u的下一个相邻点
            }
           visited[u]=0;//恢复环境,使该顶点可重新使用
    }
    
    

    *如何找最短路径:
    定义非循环队列结构体类型:

    typedef struct
    {      int data;	//顶点编号
           int parent;	//前一个顶点的位置
    } QUERE;		
    

    以路径上经过的边数来衡量路径长度:

    void ShortPath(AdjGraph *G,int u,int v)
    {   //输出从顶点u到顶点v的最短逆路径
           qu[rear].data=u;//第一个顶点u进队
            while (front!=rear)//队不空循环
            {      front++;		//出队顶点w
                   w=qu[front].data;
                  if (w==v)   根据parent关系输出路径break; 
                  while(遍历邻接表)   
                    {         rear++;//将w的未访问过的邻接点进队
    		 qu[rear].data=p->adjvex;
    		 qu[rear].parent=front;
    	  }
             }	      
    }
    
    

    广度优先遍历找到的路径一定是最短路径,而深度优先遍历则不一定。
    深度优先遍历能找所有路径,而广度优先遍历难以实现。

    1.1.6 最小生成树相关算法及应用

    • 生成树和最小生成树:
      一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边。不能回路。  
      如果在一棵生成树上添加一条边,必定构成一个环。

      一个连通图的生成树不一定是唯一的。

    *最小生成树的概念:
    对于带权连通图G ,n个顶点,n-1条边
    根据深度遍历或广度遍历生成生成树,树不唯一
    其中权值之和最小的生成树称为图的最小生成树。

    *非连通图和生成树:
    非连通图:需多次调用遍历过程。
    每个连通分量中的顶点集和遍历时走过的边一起构成一棵生成树。所有连通分量的生成树组成非连通图的生成森林。

    • 寻找最小生成树:
      一.普里姆算法:

    普里姆(Prim)算法是一种构造性算法,用于构造最小生成树。过程如下:
    (1)初始化U={v}。v到其他顶点的所有边为候选边;
    (2)重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
    从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
    考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边

    *构造的最小生成树不一定唯一,但最小生成树的权值之和一定是相同的。

    *如何选j到U顶点集的最小边?
    设置2个辅助数组。
    1.closest[i]:最小生成树的边依附在U中顶点编号。
    2.lowcost[i]表示顶点i(i ∈ V-U)到U中顶点的边权重,取最小权重的顶点k加入U。并规定lowcost[k]=0表示这个顶点在U中
    3.(closest[k],k)构造最小生成树一条边。
    最小生成树求解:
    1.找最小lowcost
    2.加入新顶点,修正lowcost, closest

    • prim算法代码的实现:
    #define INF 32767//INF表示∞
    void  Prim(Graph G,int v)
    {     
          int lowcost[MAXV];
          int min;
          int closest[MAXV], i, j, k;
          for (i=0;i<G.n;i++)//给lowcost[]和closest[]置初值
          {	
            lowcost[i]=G.edges[v][i];
    	closest[i]=v;
          }
    
           for (i=1;i<G.n;i++)//输出(n-1)条边
           {	
              min=INF;
    	  for (j=0;j<G.n;j++) //在(V-U)中找出离U最近的顶点k
    	     if (lowcost[j]!=0 && lowcost[j]<min)
    	     {	
                    min=lowcost[j];
    		k=j;//k记录最近顶点编号
    	     }
    	     lowcost[k]=0;//标记k已经加入U
                 for (j=0;j<G.n;j++)//修改数组lowcost和closest
    	     if (lowcost[j]!=0 && G.edges[k][j]<lowcost[j])
    	     {	
                      lowcost[j]=G.edges[k][j];
    		  closest[j]=k;
    	     }
            }
    }
    
    • 贪心算法:
      算法原理:以当前情况为基础作最优选择,而不考虑各种可能的整体情况,所以贪心法不要回溯。
      算法优点:因为省去了为寻找解而穷尽所有可能所必须耗费的大量时间,因此算法效率高。
      贪婪算法的精神就是“只顾如何获得眼前最大的利益”,有时不一定是最优解。

    prim算法的应用:
    公路村村通:
    现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
    解决代码:

    #include<stdio.h>
    #include <iostream>
    using namespace std;
    #define INF 32767
    #define MAX 1001
    typedef struct
    {
    	int n, e;//定义图的顶点数和边数
    	int edge[MAX][MAX];//邻接矩阵表示顶点之间的关系
    }MINGraph;
    //创建村庄图
    void CreateEdge(MINGraph*& G, int n, int e)
    {
    	int v1, v2;//同一条边的两端顶点
    	int weight;//边的权值
    	G = new MINGraph;
    	G->n = n;
    	G->e = e;
    	int i, j;
    	//邻接矩阵的初始化
    	for (i = 1; i <= n; i++)
    	{
    		for (j = 1; j <= n; j++)
    		{
    			if (i == j)
    			{
    				G->edge[i][j] = 0;
    			}
    			else
    			{
    				G->edge[i][j] = INF;
    			}
    		}
    	}
    	//为邻接矩阵赋值
    	for (i = 1; i <= e; i++)
    	{
    		cin >> v1 >> v2 >> weight;
    		G->edge[v1][v2] = G->edge[v2][v1] = weight;
    	}
    }
    int Prim(MINGraph *G, int v)
    {
    	//边权重    最小边权重  边依附在U中顶点
    	int lowcost[MAX], min, closest[MAX], i, j, k=0;
    	int fee=0;//最小费用
    	for (i = 1; i <=G->n; i++)	//给lowcost[]和closest[]置初值
    	{
    		lowcost[i] = G->edge[v][i];
    		closest[i] = v;
    	}
    	for (i = 1; i < G->n; i++)	  //找出(n-1)个顶点
    	{
    		min = INF;
    		for (j = 1; j <= G->n; j++) //     在(V-U)中找出离U最近的顶点k
    			if (lowcost[j] != 0 && lowcost[j] < min)
    			{
    				min = lowcost[j];
    				k = j; // k记录最近顶点的编号
    			}
    		lowcost[k] = 0;		//标记k已经加入U
    		fee = fee + min;//最小权值的和
    		for (j = 1; j <= G->n; j++)	//修改数组lowcost和closest
    			if (lowcost[j] != 0 && G->edge[k][j] < lowcost[j])
    			{
    				lowcost[j] = G->edge[k][j];
    				closest[j] = k;
    			}
    	}
    	if (G->e < n - 1)
    		return -1;
    	for (i = 1; i <= G->n; i++)
    	{
    		if (lowcost[i] != 0)
    			return -1;
    	}
    	return fee;
    }
    int main()
    {
    	MINGraph* G;
    	int n, e;
    	cin >> n >> e;
    	CreateEdge(G, n, e);
    	int fee;
    	fee = Prim(G,1);
    	cout << fee;
    	return 0;
    
    }
    

    二.克鲁斯卡尔算法(Kruskal)
    克鲁斯卡尔(Kruskal)算法也是一种求带权无向图的最小生成树的构造性算法。是一种按权值的递增次序选择合适的边来构造最小生成树的方法。

    • 克鲁斯卡尔(Kruskal)算法过程(时间复杂度为O(n2)):
      (1)置U的初值等于V(即包含有G中的全部顶点),TE(表示最小生成树的边集)的初值为空集(即图T中每一个顶点都构成一个连通分量)。
      (2)将图G中的边按权值从小到大的顺序依次选取:
      若选取的边未使生成树T形成回路,则加入TE;
      否则舍弃,直到TE中包含(n-1)条边为止。

    • Kruskal算法实现的具体代码:

    typedef struct 
    {    int u;     //边的起始顶点
         int v;      //边的终止顶点
         int w;     //边的权值
    } Edge; 
    Edge E[MAXV];
    void Kruskal(Graph G)
    {    
          int i,j,u1,v1,sn1,sn2,k;
          int vset[MAXV];
          Edge E[MaxSize];	//存放所有边
          k=0;			//E数组的下标从0开始计
          for (i=0;i<G.n;i++)	//由G产生的边集E
    	for (j=0;j<G.n;j++)
    	      if (G.edges[i][j]!=0 && G.edges[i][j]!=INF)
    	      {     
                         E[k].u=i;  E[k].v=j;  E[k].w=G.edges[i][j];
    	             k++;
    	      }
            InsertSort(E,G.e);	//用直接插入排序对E数组按权值递增排序
            for (i=0;i<g.n;i++) 	//初始化辅助数组
    	vset[i]=i;
    	k=1;		//k表示当前构造生成树的第几条边,初值为1
    	j=0;		//E中边的下标,初值为0
    	while (k<G.n)	//生成的边数小于n时循环
    	{ 
                  u1=E[j].u;v1=E[j].v;	//取一条边的头尾顶点
    	      sn1=vset[u1];
    	      sn2=vset[v1];		//分别得到两个顶点所属的集合编号
     	      if (sn1!=sn2)  	//两顶点属于不同的集合
    	      {
                    k++;		   	//生成边数增1
    		for (i=0;i<g.n;i++)  	//两个集合统一编号
    		    if (vset[i]==sn2) 	//集合编号为sn2的改为sn1
    		       vset[i]=sn1;
    	      }
    	     j++;			   //扫描下一条边
            }
    }
    

    并查集改良Kruskal算法(时间复杂度是O(elog2e)):

    typedef struct 
    {    int u;     //边的起始顶点
         int v;      //边的终止顶点
         int w;     //边的权值
    } Edge; 
    Edge E[MAXV];
    void Kruskal(AdjGraph *g)
    {     int i,j,u1,v1,sn1,sn2,k;
          int vset[MAXV]; //集合辅助数组
          Edge E[MaxSize];	//存放所有边
          k=0;			//E数组的下标从0开始计
        for (i=0;i<g.n;i++)	//由g产生的边集E,邻接表
    	{   p=g->adjlist[i].firstarc;
            while(p!=NULL)    
    	  {	E[k].u=i;E[k].v=p->adjvex;
                E[k].w=p->weight;
    		k++; p=p->nextarc;
    	  }
         }
         Sort(E,g.e);	//用快排对E数组按权值递增排序
          for (i=0;i<g.n;i++) 	//初始化集合
    	vset[i]=i;
            k=1;		//k表示当前构造生成树的第几条边,初值为1
    	j=0;		//E中边的下标,初值为0
    	while (k<g.n)	//生成的顶点数小于n时循环
    	{ 
                        u1=E[j].u;v1=E[j].v;	//取一条边的头尾顶点
    	      sn1=vset[u1];
    	      sn2=vset[v1];	//分别得到两个顶点所属的集合编号
     	      if (sn1!=sn2)  	//两顶点属于不同的集合
    	      {	printf("  (%d,%d):%d
    ",u1,v1,E[j].w);
    		k++;		   	//生成边数增1
    		for (i=0;i<g.n;i++)  	//两个集合统一编号
    		      if (vset[i]==sn2) 	//集合编号为sn2的改为sn1
    			vset[i]=sn1;
    	     }
    	     j++;			   //扫描下一条边
                }
    }
    
    • 2种算法比较;
      普里姆算法:O(n2)、适用于稠密图
      克鲁斯卡尔算法:O(eloge)、适用于稀疏图
      实现Prim算法,选择图的邻接矩阵存储结构
      实现克鲁斯卡尔算法,选择图的邻接表存储结构

    1.1.7 最短路径

    • 最短路径问题是指:
      如果从图中某一顶点(源点)到达另一顶点(终点)的路径可能不止一条,如何找到一条路径使得沿此路径上各边的权值总和(称为路径长度)达到最小。

    • 两种常见的最短路径问题:
      1、 单源最短路径—用Dijkstra(迪杰斯特拉)算法(一顶点到其余各顶点)
      2、所有顶点间的最短路径—用Floyd(弗洛伊德)算法(任意两顶点之间)

    Dijkstra算法:

    • 过程:
    初始化顶点合集S和未选顶点合集T;
    1.S={入选顶点集合,初值V0},T={未选顶点集合}。
      若存在<V0,Vi>,距离值为<V0,Vi>弧上的权值
      若不存在<V0,Vi>,距离值为∞
      从T中选取一个其距离值为最小的顶点W, 加入S
    2.从T中选取一个其距离值为最小的顶点W, 加入S
    3.S中加入顶点w后,对T中顶点的距离值进行修改:
      若加进W作中间顶点,从V0到Vj的距离值比不加W的路径要短,则修改此距离值;
    4.重复上述步骤1,直到S中包含所有顶点,即S=V为止。
    
    
    • 两个常见问题的解决:
    1.如何存放最短路径?(最短路径中所有顶点都是最短路径)
    采用一维数组path来保存:
    ![](https://img2020.cnblogs.com/blog/1776748/202005/1776748-20200516231441070-1145664932.png)
    
    2.如何存放最短路径长度?
    用一维数组dist[j]存储:dist[j]表示起点v到顶点j的最短路径长度并从源点开始根据最短顶点来调整。
    
    • eg:


    • Dijkstra算法的具体代码实现:(算法的时间复杂度为O(n2))

    void Dijkstra(Graph G,int v)
    {    
          int dist[MAXV],path[MAXV];
          int s[MAXV];
          int mindis,i,j,u;
          for (i=0;i<G.n;i++)
          {       
            dist[i]=G.edges[v][i];	//距离初始化
    	s[i]=0;			//s[]置空
    	if (G.edges[v][i]<INF)	//路径初始化
    	       path[i]=v;  //顶点v到i有边时
    	else
    	      path[i]=-1;		
          }
          s[v]=1;	 		//源点v放入S中
          for (i=0;i<G.n;i++)	 	//循环n-1次
          {     
             mindis=INF;
    	 for (j=0;j<G.n;j++)
    	 {
                 if (s[j]==0 && dist[j]<mindis) 
    	     {        
                    u=j;
    		mindis=dist[j];
    	     }
             }
    	 s[u]=1;			//顶点u加入S中
    	 for (j=0;j<G.n;j++)	//修改不在s中的顶点的距离
    	 {
                if (s[j]==0)
    	    {
                    if (G.edges[u][j]<INF && dist[u]+G.edges[u][j]<dist[j])
    	        {      
                      dist[j]=dist[u]+G.edges[u][j];
    	   	   path[j]=u;
    	        }
                }
             }
          }
        //输出最短路径
    }
    
    • Dijkstra算法特点:
      1.不适用带负权值的带权图求单源最短路径。
      2.不适用求最长路径长度。
      3.最短路径长度是递增
      4.顶点u加入S后,不会再修改源点v到u的最短路径长度
      5.按Dijkstra算法,找第一个距离源点S最远的点A,这个距离在以后就不会改变。但A与S的最远距离一般不是直连。

    Floyd算法(时间复杂性是O(n3)):

    • 算法思路:
    有向图G=(V,E)采用邻接矩阵存储
    二维数组A用于存放当前顶点之间的最短路径长度,分量A[i][j]表示当前顶点i到顶点j的最短路径长度。
    递推产生一个矩阵序列A0,A1,…,Ak,…,An-1
         Ak+1[i][j]表示从顶点i到顶点j的路径上所经过的顶点编号k+1的最短路径长度。
    
    
    • Floyd算法的具体代码:
    void Floyd(Graph G)		
    {     
        int A[MAXVEX][MAXVEX];	//建立A数组
        int path[MAXVEX][MAXVEX];	//建立path数组
        int i, j, k;
       for (i=0;i<G.n;i++)   		
           for (j=0;j<G.n;j++) 
           {       
              A[i][j]=G.edges[i][j];
    	 if (i!=j && G.edges[i][j]<INF)
    	      path[i][j]=i; 	//i和j顶点之间有一条边时
           else			 
    	      path[i][j]=-1;
           }
    
      for (k=0;k<G.n;k++)		//求Ak[i][j]
     {     
         for (i=0;i<G.n;i++)
           for (j=0;j<G.n;j++)
    	    if (A[i][j]>A[i][k]+A[k][j])	//找到更短路径
    	    {   
                    A[i][j]=A[i][k]+A[k][j];	//修改路径长度
    	       path[i][j]=path[k][j]; 	//修改最短路径为经过顶点k
              }
      }
    }	
    

    1.1.8 拓扑排序、关键路径

    一.拓扑排序:

    在一个有向图中找一个拓扑序列的过程称为拓扑排序。序列必须满足条件:
    每个顶点出现且只出现一次。
    若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
    图中有回路,无法拓扑排序.拓扑排序可以用来检测图中是否有回路.
    

    拓扑排序的过程:
    1.从有向图中选取一个没有前驱的顶点,并输出之;
    2.从有向图中删去此顶点以及所有以它为尾的弧;
    3.重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。


    *拓扑排序:1->2->3->4->7->5->6

    • 具体代码:
    typedef struct 	       	//表头结点类型
    {     
          Vertex data;         	//顶点信息
          int count;           	//存放顶点入度
          ArcNode *firstarc;   	//指向第一条边
    }VNode;
    
     void TopSort(AdjGraph *G)	//拓扑排序算法
    {      
            int i,j;
            int St[MAXV],top=-1;	//栈St的指针为top
            ArcNode *p;
            for (i=0;i<G->n;i++)		//入度置初值0
    	G->adjlist[i].count=0;
            for (i=0;i<G->n;i++)		//求所有顶点的入度
            {	
                p=G->adjlist[i].firstarc;
    	    while (p!=NULL)
    	   {        
                      G->adjlist[p->adjvex].count++;
    	          p=p->nextarc;
    	   }
            }
             
             for (i=0;i<G->n;i++)		//将入度为0的顶点进栈
    	 if (G->adjlist[i].count==0)
    	 {	
                top++;
    	    St[top]=i;
    	 }
             while (top>-1)			//栈不空循环
             {	  
                i=St[top];top--;			//出栈一个顶点i
    	    printf("%d ",i);		//输出该顶点
    	    p=G->adjlist[i].firstarc;		//找第一个邻接点
    	    while (p!=NULL)		//将顶点i的出边邻接点的入度减1
    	    {      
                     j=p->adjvex;
    	         G->adjlist[j].count--;
    	         if (G->adjlist[j].count==0)	//将入度为0的邻接点进栈
    	         {      
                       top++;
    		   St[top]=j;
    	         }
    	         p=p->nextarc;		//找下一个邻接点
    	    }
            }
    }
    

    二.关键路径:

    AOE-网:
    用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是带权的有向无环图
    整个工程完成的时间为:从有向图的源点到汇点的最长路径。又叫关键路径(critical path).
    “关键活动(key activity)”指的是:关键路径中的边.

    AOE 网图关键词:

    AOE网——带权的有向无环图
    顶点--事件或状态
    弧(有向边)--活动及发生的先后关系
    权--活动持续的时间
    起点--入度为0的顶点(只有一个)
    终点--出度为0的顶点(只有一个)
    
    

    eg:

    拓扑序列为:
    C4,C0,C3,C2,C1,C5

    • 求关键路径的过程:
      1.事件的最早开始和最迟开始时间
      事件v最早开始时间ve(v):v作为源点事件最早开始时间为0。
      2.事件v的最迟开始时间vl(v):定义在不影响整个工程进度的前提下,事件v必须发生的时间称为v的最迟开始时间

    • 活动:边 的最早开始时间和最迟开始时间:
      1.活动a(边)的最早开始时间e(a)指该活动起点x事件的最早开始时间,即:e(a)=ve(x)

    2.活动a的最迟开始时间l(a)指该活动终点y事件的最迟开始时间与该活动所需时间之差,即:l(a)=vl(y)-c

    • 关键活动:d(a)=l(a)-e(a),若d(a)为0,则称活动a为关键活动。
      关键路径上的活动都是关键活动
      关键活动不存在富余时间,适当增加对关键活动的投资,减少关键活动的持续时间,缩短工程工期。

    • 求关键路径步骤:

    1.对有向图拓扑排序
    2.根据拓扑序列计算事件(顶点)的ve,vl数组
              ve(j) = Max{ve(i) + dut(<i,j>)}
               vl(i) = Min{vl(j) - dut(<i,j>)}
    3.计算关键活动的e[],l[]。即边的最早、最迟时间
      e(i) = ve(j)
    	l(i) = vl(k) - dut(<j, k>
    4.找e=l边即为关键活动
    5.关键活动连接起来就是关键路径
    
    
    • eg:

    1.2.谈谈你对图的认识及学习体会

    对图的认识:
    图结构多对多结构其实是线性结构和非线性结构的大本营,线性结构一对一,树形结构一对多,图形结构多对多,图常见类型是有向图 无向图 稀疏图 稠密图 连通图 非连通图 完全图 非完全图等等。用邻接矩阵和邻接表进行存储。
    学习体会:
    感觉拓扑序列有点不太懂,有的结构是在下一个结构开讲之后才搞懂的,图的建立感觉最难,结构体定义代码量少了也不太容易记住。

    2.阅读代码(来源:力扣(LeetCode))

    2.1 题目及解题代码

    • 题目:

    • 解题代码:

    /*
    trustSize:表示对数
    *trustColSize:表示每队是2个
    */
    
    int findJudge(int N, int** trust, int trustSize, int* trustColSize) {
    
    	int cnt[N + 2];
    	memset(cnt, 0, sizeof(int) * (N + 2));
    
    	// 如果被信任,则cnt++,如果信任了别人,则cnt置为-1,肯定不是法官,以后cnt不再增加
    	for (int i = 0; i < trustSize; i++) {
    		if (cnt[trust[i][1]] >= 0) {
    			cnt[trust[i][1]]++;
    		}
    		cnt[trust[i][0]] = -1;
    	}
    
    	// 被信任数是N - 1的则是法官
    	for (int i = 1; i <= N; i++) {
    		if (cnt[i] == N - 1) {
    			return i;
    		}
    	}
    	return -1;
    }
    
    
    
    
    

    2.1.1 该题的设计思路

    法官不相信任何人,说明法官不存在出度
    所有人都信任法官,说明法官的入度为N-1
    那么法官的出度加入度为N-1



    时间复杂度:O(n)
    空间复杂度:O(n)

    2.1.2 该题的伪代码

    int findJudge(int N, int** trust, int trustSize, int* trustColSize) {
    
    	//定义出入度数组cnt
    
    	// 如果被信任,则cnt++,如果信任了别人,则cnt置为-1,肯定不是法官,以后cnt不再增加
    	for i = 0 to i = trustSize-1
    		if 出度不为0即信任后继节点人
    			后继者可能是法官,入度+1
    		end if
    		cnt[trust[i][0]] = -1;//前一个人相信别人,肯定不是法官
    	end for
    
    	// 被信任数是N - 1的则是法官
    	for i = 1 to i = N
    		if (入度为N - 1)
    			return i;
    		end if 
    	}
    	return -1;
    }
    
    

    2.1.3 运行结果

    2.1.4分析该题目解题优势及难点。

    优势:
    根据图中节点的出度入度来判断节点人的身份,当所有人除了法官自己都相信法官时法官就可以判断出来了。
    难点:
    用图的方式来做这道题,其实我感觉方法比较多变叭,好像线性结构也可以做?
    

    2.2 题目及解题代码

    • 题目:

    • 解题代码:

    class Solution {
    	public boolean isBipartite(int[][] graph) {
    		int n = graph.length;
    		int[] color = new int[n];
    		Arrays.fill(color, -1);
    
    		for (int start = 0; start < n; ++start) {
    			if (color[start] == -1) {
    				Stack<Integer> stack = new Stack();
    				stack.push(start);
    				color[start] = 0;
    
    				while (!stack.empty()) {
    					Integer node = stack.pop();
    					for (int nei : graph[node]) {
    						if (color[nei] == -1) {
    							stack.push(nei);
    							color[nei] = color[node] ^ 1;
    						}
    						else if (color[nei] == color[node]) {
    							return false;
    						}
    					}
    				}
    			}
    		}
    
    		return true;
    	}
    }
    
    
    

    2.2.1 该题的设计思路

    使用数组(或者哈希表)记录每个节点的颜色: color[node]。颜色可以是 0, 1,或者未着色(-1 或者 null)。
    搜索节点时,需要考虑图是非连通的情况。对每个未着色节点,从该节点开始深度优先搜索着色。每个邻接点都可以通过当前节点着相反的颜色。如果存在当前点和邻接点颜色相同,则着色失败。
    使用栈完成深度优先搜索,栈类似于节点的 “todo list”,存储着下一个要访问节点的顺序。在 graph[node] 中,对每个未着色邻接点,着色该节点并将其放入到栈中。


    时间复杂度:O(N+E)
    空间复杂度:O(N)

    2.2.2 该题的伪代码

    class Solution {
    	public boolean isBipartite(int[][] graph) {
    		定义图节点数n
    		颜色数组color[]
    		Arrays.fill(color, -1);
    
    		for (int start = 0; start < n; ++start) {
    			if 邻接点未着色
    				初始化栈stack
    				未着色邻接点设为已着色并入栈
    
    				while 栈不为空 
    					取栈顶node
    					for (int nei : graph[node]) //检查邻边
    						if 当前节点未着色 
    							nei节点入栈
    							给color[nei]着相反色
    						end if
    						else if color[nei]和color[node]颜色相同;即弧两端节点颜色相同
    							着色失败
    						end if
    					end for
    				end while
    			end if
    		end for
    
    		return true;
    	}
    }
    
    
    

    2.2.3 运行结果

    2.2.4分析该题目解题优势及难点

    优势:
    用红色蓝色标记模块,然后一条弧两边的节点不能是同一个颜色。
    用颜色标记各节点之间的关系,到了统计合集的时候用颜色代表就可以了
    难点:
    需要注意图可能是不连通的:
    1、有单独一个点的,没有任何连接,单独的点不算一个独立子集
    2、有两个子图的,没有连接
    

    2.3 题目及解题代码

    • 题目:

    • 解题代码:

    int trap(vector<int>& height)
    {
    	if (height == null)
    		return 0;
    	int ans = 0;
    	int size = height.size();
    	vector<int> left_max(size), right_max(size);
    	left_max[0] = height[0];
    	for (int i = 1; i < size; i++) {
    		left_max[i] = max(height[i], left_max[i - 1]);
    	}
    	right_max[size - 1] = height[size - 1];
    	for (int i = size - 2; i >= 0; i--) {
    		right_max[i] = max(height[i], right_max[i + 1]);
    	}
    	for (int i = 1; i < size - 1; i++) {
    		ans += min(left_max[i], right_max[i]) - height[i];
    	}
    	return ans;
    }
    
    

    2.3.1 该题的设计思路

    找到数组中从下标 i 到最左端最高的条形块高度 left_max.
    找到数组中从下标 i 到最右端最高的条形块高度 right_max.
    扫描数组 height 并更新答案:
    累加 min(max_left[i], max_right[i])−height[i]到 ans 上

    时间复杂度:O(n)。
    空间复杂度:O(n)。

    2.3.2 该题的伪代码

    int trap(vector<int>& height)
    {
    	if height为 0(没有原始图)
    		return 0;
    	初始化可以接到的雨水量ans
    	int size = height.size();
    	定义左右两边最大高度数组left_max[]和right_max[]
    	left_max[0] = height[0];//左边第一最大高度=当前最大高度
    	for i = 1 to i = size-1 
    		从当前元素向左扫描并更新:找到数组中从下标 i 到最左端最高的条形块高度left_max。
    	end for
    	right_max[size - 1] = height[size - 1];//右边第一最大高度=当前最大高度
    	for i = size - 2 to i >= 0
    		从当前元素向右扫描并更新:找到数组中从下标 i 到最左端最高的条形块高度right_max。
    	end for
    	for i = 1 to i = size - 2
    		累加两边最大高度的较小值减去当前高度的值到ans上;
    	end for
    	return ans;
    }
    
    

    2.3.3 运行结果

    2.3.4分析该题目解题优势及难点

    优势:
    提前存储可以接到雨水的最大值,不用每次都扫描获得最大值。
    
    难点:
    空间复杂度稍微大一点;
    然后要注意雨水量是左边右边模块的更小值去减雨水的当前高度。
    
    
  • 相关阅读:
    Java面向对象(三)
    Java面向对象(二)
    Java面向对象(一)
    java基础(六)
    java基础(五)
    java基础(四)
    java基础(三)
    java基础2
    java基础
    计算机内存
  • 原文地址:https://www.cnblogs.com/zyc01-jm/p/12831404.html
Copyright © 2020-2023  润新知