• DS博客作业04--图


    这个作业属于哪个班级 数据结构
    这个作业的地址 DS博客作业04--图
    这个作业的目标 学习图结构设计及运算操作
    姓名 曹卉潼

    0.PTA得分截图

    1.本周学习总结(6分)

    1.1 图的存储结构

    1.1.1 邻接矩阵(不用PPT上的图)

    造一个图,展示其对应邻接矩阵
    用一个二维数组edges[][]保存两个顶点之间的关系。edges[i][j]表示从第i个顶点到第j个顶点的边信息。我们可以根据该二维数组每一行的数据判断每个顶点的入度,根据每一列的数据判断每个顶点的出度。每个顶点的其他信息(例如:顶点名称,顶点编号等)用一个一维数组去vexs[]保存;

    结构体定义

    
    typedef struct
    {
      int **edges;//保存边关系,定义为二级指针的原因:可以根据结点个数申请相对的空间,提高空间的利用效率;
      int n,e;//n保存顶点个数,e保存图中边的条数;
      VertexType *vexs;//保存顶点其他信息;VertexType是顶点其他信息的类型,可以是int,char,或者自定义结构体等;
    }
    
    

    无向图

    对于无向图来说,两个顶点之间存在一条边(i,j),那么这两个顶点互为邻接点,不仅可以从顶点i到顶点j,也可以从顶点j到顶点i。于是在建立邻接矩阵时,不仅要对edges[i][j]赋值,也要对edges[j][i]赋值(无权值,如果存在边就赋值为1,否则赋为0);于是我们可以看出,最后得到无向图的邻接矩阵一定是沿对角线对称的

    有向图

    对于有向图来说,若存在一条边(i,j),则此边只表示为从顶点i到顶点j,不可以由边(i,j)得到可以从顶点j到顶点i的信息。所以在建有向图的邻接矩阵时,只对edges[i][j]赋值(无权值,如果存在边就赋值为1,否则赋为0);和无向图不一样的是,最后得到的邻接矩阵不一定是一个对称图形。


    创建领接矩阵函数

    1.1.2 邻接表

    造一个图,展示其对应邻接表(不用PPT上的图)
    邻接表是数组和链表的结合。对于每个顶点都建立一个单链表存储该顶点所有的邻接点。然后将定义一个结构体VNode,里面保存顶点邻接点的链表和顶点其他信息。设置VNode类型的结构体数组AdjGraph[]就可以保存图中所有顶点的邻接点,达到保存图中所有边的目的。结构体数组AdjGraph[]即为邻接表。
    邻接表的结构体定义

    
    typedef struct ANode //边结点;
    {
       int adjvex;//指向该边的终点编号;
       struct ANode*nextarc;//指向下一个邻接点;
       INfoType info;//保存该边的权值等信息;
    }ArcNode;
    typedef struct  //头结点
    {
       int data;//顶点;
       ArcNode *firstarc;//指向第一个邻接点;
    }VNode;
    typedef struct
    {
       VNode adjlist[MAX];//邻接表;
       int n,e;//图中顶点数n和边数e;
    }AdjGraph;
    
    

    无向图
    对于无向图,输入边(a,b),那么就代表可以从顶点a到顶点b,也可以从顶点b到顶点a,所以我们不仅要在顶点a的邻接点链表中插入结点b,还要在顶点b的邻接点链表中插入结点a。

    有向图
    对于有向图,输入边(a,b),只需在顶点a的邻接点链表中插入b就行。

    创建邻接表函数

    1.1.3 邻接矩阵和邻接表表示图的区别

    (1)邻接矩阵:

    • 因为邻接矩阵需要申请一个二维数组,空间复杂度为O(n2),邻接矩阵的初始化需要初始化整个二维数组,所以时间复杂度为O(n2);
    • 好处:方便我们提取,修改边的信息;
    • 劣势:占用空间较大,如果图中边条数较少(稀疏图)的话,需要我们保存的边信息就比较少,用邻接矩阵就会有多余的空间被闲置,空间利用效率不高;不利于顶点的插入和删除。

    (2)邻接表:

    • 因为共有e条边和n个结点,需要开辟n个空间来保存结点,e个空间来保存e条边信息,所以,创建邻接表的空间复杂度为O(n+e);因为对n个结点的单链表进行初始化,处理了n次,还要对e条边信息进行保存,故时间复杂度为O(n+e);
    • 优势:占用空间相对邻接矩阵来说较小。
    • 劣势:不方便我们提取两个顶点之间边的信息。

    1.2 图遍历

    1.2.1 深度优先遍历

    DFS遍历
    深度优先遍历图的方法是,从图中某顶点v出发:
    (1)访问顶点v;
    (2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
    (3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

    深度遍历代码

    深度遍历适用哪些问题的求解。(可百度搜索)

    1.2.2 广度优先遍历

    选上述的图,继续介绍广度优先遍历结果

    BFS遍历
    从给定的任意结点v(初始顶点)开始,访问v所有的未被访问过的邻接点,然后按照一定次序访问每一个顶点的所有未被访问过的邻接点,直到图中和初始顶点邻接的所有顶点都被访问过为止。BFS遍历我们在用队列求解迷宫问题时接触过,是不可回溯的,逐渐向外扩散的过程。

    建一个访问队列q;
    访问v节点,进队;
    while(队列不为空)
       出队一个节点w;
       遍历节点w的邻接点
          取邻接点j,如果j未被访问则入队列q,然后把j标记为已访问;
    end while
    

    广度遍历代码

    广度遍历适用哪些问题的求解。(可百度搜索)

    1.3 最小生成树

    一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。
    对于带权值的图,其中权值之和最小的生成树称为图的最小生成树。

    1.3.1 Prim算法求最小生成树

    基于上述图结构求Prim算法生成的最小生成树的边序列
    实现Prim算法的2个辅助数组是什么?其作用是什么?

    设置2个辅助数组:
    
    1.closest[i]:最小生成树的边依附在U中顶点编号。
    
    2.lowcost[i]表示顶点i(i ∈ V-U)到U中顶点的边权重,取最小权重的顶点k加入U。
    并规定lowcost[k]=0表示这个顶点在U中
    
    每次选出顶点k后,要队lowcost[]和closest[]数组进行修正
    
    

    Prim算法代码

    void Prim(MGraph g, int v)
    {
    	int lowcost[MAXV],  closest[MAXV];//lowcost表示到该点最短距离,closest
    	int i, j, k, min ;// k记录最近顶点的编号
    
    	lowcost[1] = 1;//起点最近点为它本身
    	for (i = 1; i <= g.n; i++)	//顶点从1开始,给lowcost[]和closest[]置初值
    	{
    		lowcost[i] = g.edges[v][i];//建图时未有直接相连的边,lowcost=edges为无穷大INF
    		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; //
    			}
    		lowcost[k] = 0;		//遍历所有点后找到距离最近点,标记k已经加入
    
    		for (j = 1; 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算法时间复杂度

    
    Prim()算法中有两重for循环,时间复杂度为O(n^2),n为图的顶点个数。其执行时间与图中边数e无关,所以适合用稠密图求最小生成树。
    

    1.3.2 Kruskal算法求解最小生成树

    克鲁斯卡尔算法过程:
    (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];
    

    Kruskal算法代码

    分析Kruskal算法时间复杂度

    连通图G有n个顶点、e条边,其时间复杂度为O(e^2)。该算法的执行时间只与图的边数有关,与顶点数无关,适用于稀疏图求最小生成树。
    

    1.4 最短路径

    1.4.1 Dijkstra算法求解最短路径

    基于上述图结构,求解某个顶点到其他顶点最短路径。(结合dist数组、path数组求解)

    最短路径与最小生成树不同:
    最小生成树需要包含所有顶点, 而最短路径只考虑路径最短

    1.从T中选取一个其距离值为最小的顶点W, 加入S
    
    2.S中加入顶点w后,对T中顶点的距离值进行修改:
       若加进W作中间顶点,从V0到Vj的距离值比不加W的路径要短,则修改此距离值;
    
    3.重复上述步骤1,直到S中包含所有顶点,即S=V为止。
    

    Dijkstra算法代码

    void Dijkstra(MGraph g, int v)//源点v到其他顶点最短路径 
    {
    	int dist[MAXV], path[MAXV],s[MAXV];
    	int mindistance,u;//u为每次所选最短路径点
    
    	for (int i = 0; i < g.n; i++)//初始化各数组
    	{
    		s[i] = 0;//初始已选入点置空
    		dist[i] = g.edges[v][i];//初始化最短路径
    
    		if (dist[i] < INF) path[i] = v;
    		else path[i] = -1;//即无直接到源点V的边,因此初始化为-1
    	}
    	s[v] = 1;//源点入表示已选
    
    	for (int j = 0; j < g.n; j++)//要将所有点都选入需循环n-1次
    	{
    		mindistance = INF;//每次选之前重置最短路径
    		for (int i = 1; i < g.n; i++)//每次都遍历源点以外其他点来选入点
    		{
    			if (s[i] == 0 && dist[i] < mindistance)//在未选的点中找到最短路径
    			{
    				mindistance = dist[i];
    				u = i;//u记录选入点
    			}
    		}
    		s[u] = 1;//最后记录的u才为最后选入点
    
    		for (int i = 1; i < g.n; i++)//修正数组值
    		{
    			if (s[i] == 0)//!!仅需修改未被选入点的,已选入的既定
    			{
    				if (g.edges[u][i] < INF && dist[u] + g.edges[u][i] < dist[i])//先判断选入点到与该点存在时再比较判断
    				{
    					dist[i] = dist[u] + g.edges[u][i];
    					path[i] = u;
    				}
    			}
    		}
    	}
    	Dispath(dist, path, s, g.n, v);
    
    }
    

    Dijkstra算法的时间复杂度

    1.时间复杂度为O(n^2)。
    2.不适用带负权值的带权图求单源最短路径。
    3.  不适用求最长路径长度:
    	最短路径长度是递增
    	顶点u加入S后,不会再修改源点v到u的最短路径长度
            (按Dijkstra算法,找第一个距离源点S最远的点A,这个距离在以后就不会改变。但A与S的最远距离一般不是直连。)
    

    1.4.2 Floyd算法求解最短路径

    算法思路

    有向图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(MatGraph 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			 //i和j顶点之间没有一条边时
    				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] = k; 	//修改经过顶点k
    				}
    	}
    }
    

    Floyd算法优势

    1.弗洛伊德算法可以解决负权值的带权图,也可以解决求最长路径长度问题。
    2.弗洛伊德算法是一种动态规划的算法,在规划的同时又对之前的内容进行调整修改。
    

    1.5 拓扑排序

    在一个有向图中,如果我们需要访问一个节点,要先把这个节点的所有前驱节点都访问过后,才能访问该节点。按照这样的顺序访问所有节点得到的序列叫做拓扑序列。在一个有向图中求一个拓扑序列的过程叫做拓扑排序。

    拓扑排序思路

    1.选择一个没有前驱结点的顶点,输出该顶点编号;
    2.从有向图中删去此顶点,以及以他为起点的弧。这里的删除并不是在图结构中对该顶点的删除(物理删除),我们还是最好保留原来的图结构,这里我们可以借用顶点的入度实现模拟删除。当我们要'删除'某个顶点及以他为起点的弧时,我们可以直接将该顶点所有邻接点的入度-1;
    重复上述两步,直到找不到没有前驱的顶点。
    


    伪代码

    遍历邻接表
       计算每个顶点的入度,存入头结点count成员中;
    遍历图顶点
       找到一个入度为0的顶点,入栈/队列/数组;
    while(栈不为空)
       出栈结点v,访问;
       遍历v的所有邻接点
       {
          所有邻接点的入度-1;
          若有邻接点入度为0,入栈/队列/数组;
       }
    

    拓扑排序代码

    void TopSort(AdjGraph* G)//邻接表拓扑排序
    {
    	ArcNode* p;
    	int stack[MAXV], top=-1;//顺序栈结构
    	int visitedcout = 0;//记录已得到的拓扑序列长度
    	int sequence[MAXV];//用于保存拓扑序列
    
    	for (int i = 0; i < G->n; i++)
    	{
    		G->adjlist[i].count = 0;
    	}
    	for (int i = 0; i < G->n; i++)//遍历每条链,记录每个节点的入度
    	{
    		p = G->adjlist[i].firstarc;
    		while (p)
    		{
    			G->adjlist[p->adjvex].count++;
    			p = p->nextarc;
    		}
    	}
    
    	for (int i = 0; i < G->n; i++)//先遍历图顶点,找出入度为0的点入栈
    	{
    		if (G->adjlist[i].count == 0)
    		{
    			top++; stack[top] = i;
    		}
    	}
    
    	int i = 0;
    	while (top!=-1)//接下来通过不断出栈过程中同时判断是否有点要入栈
    	{
    		sequence[i] = stack[top]; visitedcout++;//保存拓扑序列,并且记录已遍历点
    		p = G->adjlist[stack[top]].firstarc;//则该点的后继点入度都要减一
    		top--;//出栈
    
    		while (p)
    		{
    			G->adjlist[p->adjvex].count--;
    			if (G->adjlist[p->adjvex].count == 0)
    			{
    				top++; stack[top] = p->adjvex;
    			}
    			p = p->nextarc;
    		}
    		i++;
    	}
    
    	if (visitedcout == G->n)//则无环路得到拓扑序列,
    	{
    		cout << sequence[0];
    		for (int i = 1; i < G->n; i++)
    		{
    			cout << " " << sequence[i];
    		}
    	}
    	else cout << "error!";
    }
    

    用拓扑排序代码检查是否有环路

    由上图可知,在一个环路中,我们是没办法找到入度为0的顶点。
      同样的全局图来说,即使利用拓扑排序可以得到一定的拓扑序列,但只要存在环路,就不可能得到完整的拓扑序列
    
      因此判断是否存在有环,只需记录一下得到的序列长度,与图的顶点数相比即可知,序列是否完整,是否就是拓扑序列
     
      具体实现也已经在上述具体代码中体现
    

    1.6 关键路径

    什么叫AOE-网?

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

    求关键路径

    1.对有向图拓扑排序
    
    2.根据拓扑序列计算事件(顶点)的ve, vl数组
    
    3.计算关键活动的e[],l[[]。即边的最早、最迟时间
    
    4.找e=l边即为关键活动
    
    5.关键活动连接起来就是关键路径
    
    

    2.PTA实验作业(4分)

    2.1 六度空间(2分)

    2.1.1 伪代码

    定义结点数n和边数e;
    定义顶点访问标记数组visited[MAXV];
    定义一个充当邻接矩阵的二维数组edgex[MAXV][MAXV];
    int main() 
        输入n和e
        for i=1 to e do
            输入边的关系;
            将edgex[][]对应的点的值改为1;
        end for
        for i=1 to n do 
            初始化visited数组 ;
            调用BFS函数,返回距离不超过5的结点数;
            计算并输出百分比; 
        end for 
    
    int BFS(int v) 
        定义一个队列qu; 
        将v入队; 
        visited[v]=1;
        while 队不空且距离小于6 do
            取队首做临时调用点temp; 
            循环遍历与该结点相连接的点
                if 结点未遍历 then
                    结点数++;
                    入队;
                    visited[i]=1;
                    记录位置tail=i; 
                end if
            if temp==last then 
                记录当前层数的最后一个元素的位置 ;
                结点层数加一;
            end if 
        end while
        return count;
    
    • 代码




    2.1.2 提交列表

    2.1.3 本题知识点

    • 创建图函数运用了头插法
    • 运用BFS遍历(引入队列)
    • 不仅要用广度遍历BFS,还要和递归相结合,分层运算。

    2.2 村村通

    2.2.1 伪代码

    main()函数
    {
        for (i = 1; i <= n; i++)
    		for (j = 1; j <= n; j++)
                    {
                            用二维数组代表邻接矩阵,并进行初始化。
                     }
          for (i = 1; i <= e; i++) 
            {
                    给邻接矩阵赋值
            }
            调用普里姆算法;
    }
    Prim() 函数
    {
           for (i = 1; i <= n; i++)
            {
                    给lowcost[]和closest[]置初值;
               }
            for (i = 1; i <n; i++)
            {
                    给min赋初值(表示无穷);
                   for (j = 1; j <= n; j++) 
                    {
                        在(V-U)中找出离U最近的顶点k
                         k记录最近顶点的编号
                        }
                   for (j = 1; j <= n; j++) 
                    {
                           对(V-U)中的顶点j 进行调整;
                            修改数组lowcost[]和closest[];
                    }
                }
            输出num;
    }
    
    • 代码


    2.2.2 提交列表

    2.2.3 本题知识点

    • main函数中运用二维数组代表邻接矩阵
    • 运用Prim()算法
  • 相关阅读:
    <转> 百度空间 最大子图形问题详解
    Hdu 1124 Factorial
    Uva 457 Linear Cellular Automata
    求01矩阵中的最大的正方形面积
    【HYSBZ】1036 树的统计Count
    【SPOJ】375 Query on a tree
    【POJ】3580 SuperMemo
    【CodeForces】191C Fools and Roads
    【FOJ】2082 过路费
    【HDU】3726 Graph and Queries
  • 原文地址:https://www.cnblogs.com/chtdeboke/p/14799608.html
Copyright © 2020-2023  润新知