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()算法
posted @ 2021-05-22 21:18  SmileCHT  阅读(64)  评论(1编辑  收藏  举报