DS博客作业04--图

| 这个作业属于哪个班级 | 数据结构--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业04--图 |
| 这个作业的目标 | 学习图结构设计及相关算法|
| 姓名 | 黄抒鸿 |

0.PTA得分截图

1.本周学习总结

1.1 图的存储结构

1.1.1 邻接矩阵

1)造一个图,展示其对应邻接矩阵

一个无向图的邻接矩阵:

一个网图的邻接矩阵:

2)邻接矩阵的结构体定义:

#define MAXV<最大顶点个数>
typedef struct
{
 int no;//顶点编号
 InfoType info;//顶点其他信息
}VertexType;

typedef struct //图的定义
{
 int edges[MAXV][MAXV];//邻接矩阵
 int n,e;//顶点数,边数
 VertexType vexs[MAXV];//存放顶点信息
}MatGraph

3)建图函数

//邻接矩阵创建图
void CreateMGraph(MGraph &g,int n,int e)//n顶点个数,e边个数
{  
  int i,j,a,b;
  for(i=0;i<n;i++)
   for(j=0;j<n;j++)
      g.edges[i][j]=0;
  for(i=1;i<=e;i++)
  {
     cin>>a>>b;
     g.edges[a-1][b-1]=1;
     g.edges[b-1][a-1]=1;
   }
 g.n=n;
 g.e=e;
}

1.1.2 邻接表

1)造一个图,展示其对应邻接表

有向图:

该图的邻接表:

若该图无向,则邻接表为:

2)邻接矩阵的结构体定义

typedef struct ANode
{
 int adjvex;//该边的邻接点编号
 struct ANode * nextarc;//指向下一条边的指针
 int weight;//权值
}ArcNode;//边结点类型

typedef struct Vnode
{
 InfoType info;//顶点的其他信息
 ArcNode * firstarc;//指向第一个边结点
}VNode;//邻接表的头结点类型

typedef struct
{
 VNode adjlist[MAXV];//邻接表的头结点数组
 int n,e;//n顶点数,e边数
}AdjGraph;//完整图邻接表类型

3)建图函数

void CreatrAdj(AdjGraph *&G,int n,int e)//创建图邻接表
{ 
  int i,j,a,b;
  ArcNode *p;
  G=new AdjGrapg;
  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;
}

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

邻接表的主要特点:邻接表表示不唯一,特别适合于稀疏图存储,时间复杂度为O(n+e)
邻接矩阵的主要特点:一个图的邻接矩阵表示是唯一的,特别适合与稠密图的存储,时间复杂度为O(n^2)

1.2 图遍历

1.2.1 深度优先遍历

1)选择上述有向图,继续介绍深度优先遍历结果:首先从1开始,1结点处可以访问2,3两个结点,那么按照我们自定义的优先顺序线访问2结点,此时,2结点有4,5两个结点访问,依旧按次序访问呢4结点,4结点可以访问5结点,5结点无法继续向下访问故结束访问,并回退4结点,4结点无法没有其他分支且自己已被访问故又退回2结点,2结点的两个分支4,5结点均已被访问,故再退回1结点,此时只有3结点未被访问,访问3结点,最终得到次序:1-2-4-5-3
2)深度遍历代码

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;	
   }
}

3)深度遍历适用哪些问题的求解。
迷宫问题的求解;判断是否有简单路径;测试图的结构是否正确。

1.2.2 广度优先遍历

1)选择上述有向图,继续介绍广度优先遍历结果:首先从1开始,1结点处可以访问2,3两个结点,我们访问并以此把两个结点的访问顺序放入队列,然后按照入队顺序(如2,3),之后我们出队状态2,依次访问2结点的下两个结点(4,5结点),并入队4,5结点,再之后我们出队3结点,并依次访问后续,此时发现所有的结点已经被访问完毕了,可以结束搜索,最后我们得到次序:1-2-3-4-5
2)广度遍历代码

void BFS(ALGraph* G, int v)//v节点开始遍历
{
   queue<int>q; int w;
   int visited[MAXV];//定义存放节点的访问标志数
   ArcNode* p;
   for (i = 0; i < g.n; i++) visited[i] = 0;//访问标志数组q
   q.push(v); visited[v] = 1; cout << v << " ";
   while (!q.empty())
   {
   	w = q.front(); q.pop();//访问队头
   	p = G.adjlist[w].firstarc;//访问w第一条边
   	while (p!=NULL)
   	{
   		W = P->adjvex;//边的邻接点
   		if (visited[w] == 0)//若当前邻接点未被访问
   		{
   			q.push(w);
   			visited[w] = 1;
   			cout << w << " ";
   		}
   		p = p->nextarc;
   	}
   }
}

3)广度遍历适用哪些问题的求解。
图的广度遍历可以以层次型遍历图找到特定的第几个或者最近最远的顶点,可求解迷宫问题的最短路径。

1.3 最小生成树

什么是最小生成树?

将给出的所有点连接起来(即从一个点可到任意一个点),且连接路径之和最小的图叫最小生成树。最小生成树属于一种树形结构(树形结构是一种特殊的图),或者说是直链型结构,因为当n个点相连,且路径和最短,那么将它们相连的路一定是n-1条。

1.3.1 Prim算法求最小生成树

1)基于上述图结构求Prim算法生成的最小生成树的边序列:
假设从顶点0出发,0到3的权值最小,为3;继续找,3到1的权值最小为1,找到1;继续,1到2的权值最小,找到2;最后找到4。所以改图的一个Prim算法生成的最小生成树边序列:{(0,3),(3,1),(1,2),(2,4)}
2)实现Prim算法的2个辅助数组是什么?其作用是什么?
1.closest[i]:最小生成树的边依附在U中顶点编号。
2.lowcost[i]表示顶点i到U中顶点的边权重,取最小权重的顶点k加入U。并规定lowcost[k]=0表示这个顶点在U中
3.(closest[k],k)构造最小生成树的一条边。
4.Prim算法代码:

void Prim(MatGraph 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记录最近顶点的编号
               }
       printf("边(%d,&d)权为:%d\n",closest[k],k,MIN);//输出最小生成树的一条边
       lowcost[k]=0;//标记k已经加入U
       for(j=0;j<g.n;j++)//对(V-U)中的顶点j进行调整
           if(lowcost[j]!=0&&g.edges[k][j]<lowcost[j])
             {
                  lowcost[j]=g.edges[k][j]
                  closest[j]=k;//修改数组lowcost和closest
              }
     }
}

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

1.3.2 Kruskal算法求解最小生成树

1)基于上述图结构求Kruskal算法生成的最小生成树的边序列:
所有边按照权值大小排序,按照从小到大的顺序进行判断,首先最小是(0,3),其次是(1.2),然后是(1,3),两点不在同一棵树上,可以连接,然后是(2,4),同样不在一棵树上,进行连接。所以改图用Kruskal算法生成的最小生成树的边序列为{(0,3),(1,2),(1,3),(2,4)}。
2)实现Kruskal算法的辅助数据结构是什么?其作用是什么?
1.设置一个辅组数组vset[0,n-1],vset[i]用于记录一个顶点i所在的连通分量编号
2.用一个数组E[]存放图G中的所有边,要求它们是按照权值从小到大的顺序排序的,为此先从图G的邻接矩阵中获取所有边集E,再采用直接插入排序法对边集E按权值递增排序。
3.Kruskal算法代码:

void Kruskal(AdjGraph *g)
{
 int i,j,u1,v1,sn1,sn2,k;
 int vest[MAXV];//集合辅组数组
 Edge E[MaxSize];//存放所有边
 k=0;//E数组的下标从0开始计
 for(i=0;i<g.n;i++)
   {
      p=g->adjlist[i].firstarc;
      while(p!=NULL)
      {
        E[k].u=i;E[k].v=p->adjlist;
        E[k].w=p->weight;
        k++;p=p->nextarc;
       }
    InsertSort(E,g.e);
    for(i=0;i<g.n;i++)
        vest[i]=i;
    k=1;
    j=0;
    while(k<g.n)
    {
       u1=E[j].u;v1=E[j].v;
       sn1=vset[u1];
       sn2=vset[v1];
       if(sn1!=sn2)
         {
           printf("(%d,%d):%d\n",u1,v1,E[j].w);
           k++;
           for(i=0;i<g.n;i++)
              if(vest[i]==sn2)
                 vest[i]=sn1;
          }
         j++;
      }
}

3)分析Kruskal算法时间复杂度,适用什么图结构,为什么?
如果给定的带全连通图G有n个顶点,e条边,在上诉算法中,对边集E采用直接插入排序的时间复杂度为O(e2)。while循环是在e条边中选取(n-1)条边,而其中的for循环执行n次,因此while循环的时间复杂度为O(n2+e2)。对于连通无向图,e>=(n-1),那么用Krusakl算法构造最小生成树的时间复杂度的为O(e2)。Kruskal算法的执行时间仅与图中的边数有关,与顶点数无关,所以它特别适合用稀疏图求最小生成树。

1.4 最短路径

1.4.1 Dijkstra算法求解最短路径

1)基于上述图结构,求解某个顶点到其他顶点最短路径。

2)Dijkstra算法需要哪些辅助数据结构
1.数组dist[]:源点V0到每个终点的最短路径长度
2.数组path[]:最短路径序列的前一项点的序号;初值或无路径用-1表示
3.数组s[]:表示最短路径项点的集合
4.Dijkstra算法代码:

void Dijkstra(MatGraph 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;
   	if (g.edges[v]]i] < INF)//v到i有边,初始化前继结点
   	{
   		path[i] = v;
   	}
   	else
   	{
   		path[i] = -1;
   	}
   }
   s[v] = 1;
   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;
   	for (j = 0; j < g.n; j++)//修改改变结点后的路径长度
   	{
   		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;
   			}
   		}
   	}
   }
}

3)Dijkstra算法如何解决贪心算法无法求最优解问题?
贪心算法无法求最优解问题,在该代码中dist[u]+g.edges[u][j]<dist[j]此处的小于号将该题的解法固定唯一,若改为<=号则会出现多种解法。

4)Dijkstra算法的时间复杂度,适用什么图结构,为什么。
Dijkstra算法的时间复杂度为O(n^2),其中n为图中顶点个数。更适用于求稠密图的最短路路径问题,因为稠密图的边数远远大于点数。

1.4.2 Floyd算法求解最短路径

1)Floyd算法解决什么问题?
Floyd算法是解决给定的加权图中顶点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。
2)Floyd算法需要哪些辅助数据结构
1.有向图G=(V,E)用邻接矩阵g表示。
2.设置一个二维数组A用于存放当前顶点之间的最短路径长度,即分量A[i][j]表示当前i->j的最短路径长度。
3.用二维数组path保存最短路径,它与当前迭代的次数有关。在算法结束时,由二维数组path的值追溯,可以得到从i->j的最短路径。
4.Floyd算法代码

void Floyd(MatGraph g)
{
	int A[MAXV][MAXV];
	int path[MAXV][MAXV];
	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.edgse[i][j] < INF)//存在边的关系时
			{
				path[i][j] = i;
			}
			else
			{
				path[i][j] = -1;
			}
		}
	}
	for (k = 0; k < g.n; k++)
	{
		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[i][j];//修改路径长度
					path[i][j] = k;//修改顶点
				}
			}
		}
	}
}

3)Floyd算法优势。
Floyd算法容易理解,可以算出任意两个节点之间的最短距离,代码编写简单,但时间复杂度比较高,不适合计算大量数据。

1.5 拓扑排序

1)找一个有向图,并求其对要的拓扑排序序列

有向无环图:

1.从图中选择一个没有前驱的顶点并输出。
2.从图中删除该顶点和所有以它为起点的有向边。
3.重复 1 和 2 直到当前的图为空或当前图中不存在无前驱的顶点为止。

得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。

通常,一个有向无环图可以有一个或多个拓扑排序序列。

2)实现拓扑排序代码,结构体如何设计?

结构体:

typedef struct
{
	Vertex data;//顶点信息
	int count;//存放入度
	AreNode *firstarc;//头结点类型
}VNode;

3)书写拓扑排序伪代码,介绍拓扑排序如何删除入度为0的结点?

伪代码:

while(栈不空)
{
   出栈v,访问;
   遍历v所有邻接点
   {
      所有邻接点的入度-1
      当入度为0时,则入栈,以此实现入度为0时的删除操作
   }
}

代码:

void TopSort(AdjGraph *G)//邻接表拓扑排序。注:需要在该函数开始计算并初始化每个节点的入度,然后再进行拓扑排序
{
	int node[MAXV];
	int counts = 0;
	int top = -1;
	int stacks[MAXV];
	ArcNode *p;
	int i, j, k = 0;
	for (i = 0; i < G->n; i++)//初始化count
	{
		G->adjlist[i].count = 0;
	}
	for (i = 0; i < G->n; i++)
	{
		p = G->adjlist[i].firstarc;
		while (p)//计算每个结点入度
		{
			G->adjlist[p->adjvex].count++;
			p = p->nextarc;
		}
	}
	for (i = 0; i < G->n; i++)
	{
		if (G->adjlist[i].count == 0)//结点为0入栈
		{
			stacks[++top] = i;
		}
	}
	while (top > -1)
	{
		i = stacks[top--];
		node[k++] = i;//进入数组
		counts++;
		p = G->adjlist[i].firstarc;
		while (p)
		{
			j = p->adjvex;
			G->adjlist[j].count--;//该节点入度-1
			if (G->adjlist[j].count == 0)
			{
				stacks[++top] = j;
			}
			p = p->nextarc;
		}
	}
	if (counts < G->n)//判断个数是否符合
	{
		cout << "error!";
	}
	else
	{
		for (i = 0; i < k; i++)
		{
			cout << node[i];
			if (i != k - 1)
			{
				cout << " ";
			}
		}
	}
}

4)如何用拓扑排序代码检查一个有向图是否有环路?
拓扑排序的方法如下:
1.从有向图中选择一个没有前驱(即入度为0)的顶点并输出它。
2.从图中删除该顶点,并且删除从该顶点出发的全部有向边。
3.重复上述两步,直到剩余的图中不再存在没有前驱的顶点为止。
这样操作的结果有两种:一种是图中全部顶点都被输出,即该图中所有顶点都在其拓扑序列中,这说明图中不存在回路;另一种就是图中顶点未被全部输出,这说明图中存在回路。

1.6 关键路径

1)什么叫AOE-网?
AOV-网:用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是带权的有向无环图。
AOE-网是在AOV-网的基础上,其中每一个边都具有各自的权值,是一个带权的有向无环网。
2)什么是关键路径概念?
整个工程活动完成的时间为:从有向图的源点到汇点的最长路径。又叫关键路径。
3)什么是关键活动?
“关键活动”指的是:关键路径中的边。

2.PTA实验作业

2.1 六度空间


2.1.1 伪代码

伪代码:

int main()
{
	定义矩阵;
	输入数据对矩阵的元素进行修改;
	for (i = 1; i <= n; i++)
	{
		cout = BFS(i);
	}
}
int BFS(int i)
{
	static int visited[MAXV];//存放已访问过的结点
	queue<int>q;
	int level;
	int last = i;//用于判断是否为该层最后一个
	int tail;
	int count = 0;
	访问该结点并进队;
	while (队不为空)
	{
		队头元素出;
		for (遍历结点)
		{
			if (未访问过且边存在)
			{
				访问进队;
				count++;
				tail记录此时结点;
			}
		}
		if (last == i)//为该层最后一个
		{
			level++;
			last = j;//移动到下一层
		}
		if (达到6层)
		{
			return count;
		}
	}
}

代码:

#include <iostream>
#include <string>
#include <queue>
using namespace std;
static int map[1002][1002];//定义矩阵,static省去初始化
int BFS(int i);
int n, e;
int main()
{
	cin >> n >> e;
	int i;
	for (i = 1; i <= e; i++)
	{
		int a, b;
		cin >> a >> b;
		map[a][b] = 1;
		map[b][a] = 1;
	}
	int count=0;
	double num;
	for (i = 1; i <= n; i++)
	{
		count = BFS(i);//计算数量
		num = count * 100.0 / n;
		printf("%d: %.2f%\n", i, num);
	}
	return 0;
}
int BFS(int i)//广度遍历
{
	bool visited[10002] = {false};//定义visited来储存已访问的结点
	queue<int>q;
	q.push(i);
	visited[i] = 1;
	int level = 0;//层数
	int count = 1;
	int last =i;
	int tail;
	while (!q.empty())
	{
		i = q.front();
		q.pop();
		for (int j = 1; j <= n; j++)
		{
			if (!visited[j] && map[i][j] == 1)
			{
				q.push(j);
				count++;
				visited[j] = true;
				tail = j;
			}
		}
        if (last == i)//判断是否为该层最后一个
		{
			level++;
			last = tail;//移动到下一层
		}
		if (level == 6)
		{
			return count;
		}
	}
	return count;
}

2.1.2 提交列表

2.1.3 本题知识点

利用邻接矩阵进行广度遍历,通过广度遍历进行层数的判断,需要引入last和tail进行结点访问的层数判断以及结点层数的改变,通过比较last可以判断层数是否需要改变,并及时返回数量。

2.2 村村通


2.2.1 伪代码

定义矩阵;
int main()
{
	输入边数和顶点数;
	Create(n, e);
	int num=0;
	num = Prim(n, e);
}
void Create(int n, int e)
{
	对矩阵初始化;
	修改矩阵;
}
int Prim(int n, int e)
{
	int closet[];//保存顶点下标
	int lowcost[];//保存权值
	int cost = 0;
	lowcost[1] = 0;
	lowcost[1] = 0;
	初始化lowcost[]和closet;
	for (i = 2; i <= 2; i++)
	{
		初始化min,j,k;
		while (j < n)
		{
			找到权值最小的点记录下标;
		}
		if (判断下标是否改变, 若有证明连通)
		{
			记录cost和访问顶点操作;
		}
		else return -1;
		修改lowcost和closet;
	}
}

代码:

#include <iostream>
#include <string>
using namespace std;
int edges[1002][1002];//构建矩阵
#define INF 65535;
void Create(int n,int e);
int Prim(int n,int e);
int main()
{	
	int n, e;
	cin >> n >> e;
	Create(n,e);
	int a=0;
	a = Prim(n,e);
	cout << a;

}
void Create(int n,int e)
{

	int i, j;
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			edges[i][j] = INF;//初始化
		}
	}
	int num1, num2,num3;
	for (i = 0; i < e; i++)
	{
		cin >> num1 >> num2>>num3;
		edges[num1][num2] = num3;
		edges[num2][num1] = num3;
	}
}
int Prim(int n,int e)
{
	int min;
	int i, j, k;
	int closet[1002];    //保存相关顶点下标
	int lowcost[1002];    //保存相关顶点间边的权值
    /*对顶点1进行操作,从顶点1开始*/
	int cost = 0;
	lowcost[1] = 0;
	closet[1] = 0;
	for (i = 2; i <= n; i++)//初始化lowcost和closet
	{
		lowcost[i] = edges[1][i];
		closet[i] = 1;
	}
	for (i = 2; i <= n; i++)
	{
		min = INF;//每次都要对最小值初始化一次
		j = 1;
		k = 0;
		while (j <= n)
		{
			if (lowcost[j] != 0 && lowcost[j] < min)//找到最小的
			{
				min = lowcost[j];
				k = j;//记录下标
			}
			j++;
		}
		if (k != 0)//判断是否连通
		{
			cost += min;
			lowcost[k] = 0;
		}
		else
		{
			return -1;
		}
		for (j = 2; j <= n; j++)//找到后要对权值和顶点进行修改
		{
			if (lowcost[j] != 0 && edges[k][j] < lowcost[j])//找到l与其他最小的
			{
				lowcost[j] = edges[k][j];
				closet[j] = k;
			}
		}
	}
	return cost;
}

2.2.2 提交列表

2.2.3 本题知识点

1.采用Prim算法解决最小生成树的问题
2.涉及到邻接矩阵的相关知识。

posted @ 2021-05-23 22:35  SUEHUNG  阅读(102)  评论(0)    收藏  举报