这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 王小雨 |
0.PTA得分截图
图题目集总得分,请截图,截图中必须有自己名字。题目至少完成2/3,否则本次作业最高分5分。
1.本周学习总结(6分)
本次所有总结内容,请务必自己造一个图(不在教材或PPT出现的图),围绕这个图展开分析。建议:Python画图展示。图的结构尽量复杂,以便后续可以做最短路径、最小生成树的分析。
1.1 图的存储结构
1.1.1 邻接矩阵(不用PPT上的图)
造一个图,展示其对应邻接矩阵
邻接矩阵的结构体定义
建图函数
1.1.2 邻接表
造一个图,展示其对应邻接表(不用PPT上的图)
邻接矩阵的结构体定义
建图函数
1.1.3 邻接矩阵和邻接表表示图的区别
各个结构适用什么图?时间复杂度的区别。
1.2 图遍历
1.2.1 深度优先遍历
选上述的图,继续介绍深度优先遍历结果
深度遍历代码
深度遍历适用哪些问题的求解。(可百度搜索)
1.2.2 广度优先遍历
选上述的图,继续介绍广度优先遍历结果
广度遍历代码
广度遍历适用哪些问题的求解。(可百度搜索)
1.3 最小生成树
用自己语言描述什么是最小生成树。
1.3.1 Prim算法求最小生成树
基于上述图结构求Prim算法生成的最小生成树的边序列
实现Prim算法的2个辅助数组是什么?其作用是什么?Prim算法代码。
分析Prim算法时间复杂度,适用什么图结构,为什么?
1.3.2 Kruskal算法求解最小生成树
基于上述图结构求Kruskal算法生成的最小生成树的边序列
实现Kruskal算法的辅助数据结构是什么?其作用是什么?Kruskal算法代码。
分析Kruskal算法时间复杂度,适用什么图结构,为什么?
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
基于上述图结构,求解某个顶点到其他顶点最短路径。(结合dist数组、path数组求解)
Dijkstra算法需要哪些辅助数据结构
Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码。
Dijkstra算法的时间复杂度,使用什么图结构,为什么。
1.4.2 Floyd算法求解最短路径
Floyd算法解决什么问题?
Floyd算法需要哪些辅助数据结构
Floyd算法优势,举例说明。
最短路径算法还有其他算法,可以自行百度搜索,并和教材算法比较。
1.5 拓扑排序
找一个有向图,并求其对要的拓扑排序序列
实现拓扑排序代码,结构体如何设计?
书写拓扑排序伪代码,介绍拓扑排序如何删除入度为0的结点?
如何用拓扑排序代码检查一个有向图是否有环路?
1.6 关键路径
什么叫AOE-网?
什么是关键路径概念?
什么是关键活动?
2.PTA实验作业(4分)
2.1 六度空间(2分)
选一题,介绍伪代码,不要贴代码。请结合图形展开分析思路。
2.1.1 伪代码(贴代码,本题0分)
伪代码为思路总结,不是简单翻译代码。
2.1.2 提交列表
2.1.3 本题知识点
2.2 村村通或通信网络设计或旅游规划(2分)
2.2.1 伪代码(贴代码,本题0分)
伪代码为思路总结,不是简单翻译代码。
2.2.2 提交列表
2.2.3 本题知识点
1.本周学习总结
1.1图的存储结构
1.1.1邻接矩阵
设图A是一个有n个顶点的图
A.Vex[n]表示顶点信息;二维数组A.edge[n][n]表示边的关系:A.Edge[i][j]=1表示该关系被包含;A.Edge[i][j]=0表示不被包含
无向图的邻接矩阵是对称矩阵,有向图可能不对称
网络的邻接矩阵:
邻接矩阵易求顶点的度:
无向图中:第i行(列)1的个数即为顶点i的度
有向图中:第i行1的个数即为顶点i的出度;第j列1的个数即为顶点j的入度;度=入度+出度
存储类型定义:
#define MAXV<最大顶点个数>
typedef struct{ //声明顶点类型
int no;//顶点编号
InfoType info;//顶点其他信息
}VertexType;
typedef struct{ //声明邻接矩阵类型
int edges[MAXV][MAXV];//邻接矩阵
int n,e;//顶点数,边数
VertexType vexa[MAXV];//顶点信息
}MatGraph;//图
邻接矩阵建图
void CreateMGraph(MGragh &g,int n,int 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邻接表
存储方法
图中每个顶点i建立一个单链表,将顶点i的所有邻接点链起来;每个单链表上添加一个头结点,表示顶点信息,并将所有表头结点构成一个数组,下标为i的元素表示顶点i的表头结点
设某有向图的邻接表中有n个表头结点和m个表结点,则该图中有()条有向边。
n个表头结点表示有n个结点,m个表结点表示有m条有向边;表结点不包含表头结点
若无向图G=(V,E)中含8个顶点,为保证图G在任何情况下都是连通的,则需要的边数最少是( )。
要保证无向图G在任何情况下都是连通的,即任意变动图G中的边,G始终保持连通。首先需要图G的任意7个结点构成完全连通子图G1,需n(n-1)/2=7×(7-1)/2=21条边,然后再添加一条边将第8个结点与G1连接起来,共需22条边。
邻接表中包含两种结点:头结点和边结点
存储类型定义
typedef struct ANode{
int adjvex;//该边的终点编号
struct ANode *nextarc;//指向下一条边的指针
InfoType info;//该边的权值等信息
}ArcNode;//边结点
typedef struct Vnode{
Vertex data;//权
ArcNode *firstarc;//指向第一条边
}VNode;//头结点
typedef struct{
VNode adjlist[MAXV];//邻接表
int n,e;//图的顶点数和边数
}AdjGraph;//图
adjlist数组中存放头结点
常用指针引用:
p=G->adjlist[i].firstarc;//p指向数组中第i+1条链的第一条边,也是第一个结点
w=p->adjvex;//该边对应的邻接点
p=p->nextarc;//下一条边
构造邻接表
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;//n条链的n个头结点的指针域初始化
for(i=1;i<=e;i++)
{
cin>>a>>b;//例(1,2)中1为a,2为b,表示1指向2,1为头结点
p=new ArcNode;
p->adjvex=b;
p->nextarc=G->adjlist[a].firstarc;//头插法插入p
G->adjlist[a].firstarc=p;
}
G->n=n;G->e=e;
}
此为有向图,若要无向图,则重复上述步骤,把a,b位置交换即可
输出图
void DispAdj(AdjGraph *G)
{
int i;
ArcNode *p;
for(i=1;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("
");一条链输出后换行
}
}
销毁图
void DestroyAdj(AdjGraph *&G)
{
int i;ArcNode *pre,*p;
for(i=0;i<G->n;i++)
{
pre=G->adjlist[i].firstarc;
if(pre!=NULL)
{
p=pre->nextarc;
while(p!=NULL)
{
free(pre);//释放第i个单链表的所有边结点
pre=p;p=p->nextarc;
}
free(pre);
}
}
free(G);//释放头结点数组
}
1.1.3邻接矩阵和邻接表表示图的区别
邻接矩阵特别适合于稠密图的存储,邻接矩阵的存储空间为O(n^2)
邻接表特别适合于稀疏图存储,邻接表的存储空间为O(n+e)
1.2图遍历
1.2.1深度优先遍历
邻接表
从图中某个顶点v出发,访问v;选择一个与v相邻且没被访问过的顶点w,再从w出发开始深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止
访问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;
}
}
非连通图的深度遍历
void DFSTraverse(Graph G)
{
for(v=0;v<G.vexnum;++v)visited[v]=FALSE;//初始化为false
for(v=0;v<G.vexnum;++v)
if(!visited[v])DFS(G,v);//对没有访问的顶点调用DFS
}
多次调用DFS
邻接矩阵
void DFS(MGraph g, int v)
{
int j;
visited[v] = 1;
cout <<" "<<v;
for (j = 1; j <= g.n; j++)
{
if (g.edges[v][j] ==1 && visited[j]==0)//当前顶点与j顶点邻接且未被访问
DFS(g, j);
}
}
深度优先遍历结果为:123456
深度遍历适用于解决的问题
判断无向图是否连通
先给visited[]数组置为0,然后从0顶点开始遍历图;遍历完一次后,若所有顶点i的visited[i]都为1,则连通
int visited[MAXV];
bool Connect(AdjGraph *G)
{
int i;
bool flag=true;
for(i=0;i<G->n;i++)visited[i]=0;
DFS(G,0);//从0顶点开始深度遍历
for(i=0;i<G->n;i++)
if(visited[i]==0)
{
flag=false;break;
}
return flag;
}
判断顶点u到v是否有简单路径
递归出口:所有顶点都访问过
void ExistPath(AGraph*G,int u,int v,bool &has)//has初始值为false
{
int w;ArcNode *p;
visited[u]=1;//置已访问标志
if(u==v)//u变,v不变,u=v时,找到一条路径
{
has=true;
return;//结束算法
}
p=G->adjlist[u].firstarc;//p指向顶点u的第一个邻接点(第一条边)
while(p!=NULL)
{
w=p->adjvex;//w是p的终点编号,是顶点u的相邻顶点
if(visited[w]==0) ExistPath(G,w,v,has);//w顶点未访问,递归访问它
p=p->nextarc;//p指向顶点u的下一个邻接点
}
}
输出从顶点u到v的一条简单路径
void FindaPath(AGraph *G,int u,int v,int path[],int d)//d表示path中的路径长度,初始为-1
{
int w,i;ArcNode *p;;
visited[u]=1;
d++;path[d]=u;//路径长度加一,顶点u加入到路径中
if(u==v)
{
printf("一条简单路径为:");
for(i=0;i<=d;i++)printf("%d",path[i]);
printf("
");
return;//找到一条路径后返回
}
p=G->adjlist[u].firstarc;//p指向顶点u的第一个邻接点(第一条边)
while(p!=NULL)
{
w=p->adjvex;//w是p的终点编号,是顶点u的相邻顶点
if(visited[w]==0) FindaPath(G,w,v,path,d);//w顶点未访问,递归访问它
p=p->nextarc;//p指向顶点u的下一个邻接点
}
}
输出顶点u到v的所有简单路径
利用回溯的深度搜索
找到一条路径后,再把该路径的所有visited置为0,即回溯
void FindAIIPath(AGraph *G,int u,int v,int path[],int d)//d表示path中的路径长度,初始为-1
{
int w,i;ArcNode *p;;
visited[u]=1;
d++;path[d]=u;//路径长度加一,顶点u加入到路径中
if(u==v&&d>=1)
{
for(i=0;i<=d;i++)printf("%2d",path[i]);
printf("
");
//不return
}
p=G->adjlist[u].firstarc;//p指向顶点u的第一个邻接点(第一条边)
while(p!=NULL)
{
w=p->adjvex;//w是p的终点编号,是顶点u的相邻顶点
if(visited[w]==0) FindaPath(G,w,v,path,d);//w顶点未访问,递归访问它
p=p->nextarc;//p指向顶点u的下一个邻接点
}
visited[u]=0;//恢复环境,使该顶点可重新使用
}
1.2.2广度优先遍历
邻接表
访问初始点v,接着访问v所有未被访问过的邻接点;按照次序访问每一个顶点的所有未被访问过的邻接点;直到所有顶点都被访问完
建一个访问队列q
访问v结点,进队
while(队列不空)
取队头w
遍历w的邻接表
取邻接点j
若j未被访问,进队,访问j
end while
void BFS(ALGraph *G,int v)
{
queue<int>q;int w;
int visited[MAXV];//存放结点访问标志的数组
ArcNode *p;
for(i=0;i<g.n;i++)visited[i]=0;//初始化访问标志
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;
}
}
}
非连通图广度遍历
void BFS1(AdjGraph *G)
{
int i;
for(i=0;i<G->n;i++)//遍历所有未被访问过的顶点
if(visited[i]==0)BFS(G,i);
}
非连通图:调用BFS()的次数等于连通分量的个数
邻接矩阵
void BFS(MGraph g, int v)
{
int num;
queue<int>qu;//队列初始化
if (visited[v] == 0)//未被访问
{
cout << v;
visited[v] = 1;
qu.push(v);
}
while (!qu.empty())
{
num = qu.front();
qu.pop();
for (int i = 1; i <= g.n; i++)
{
if (g.edges[num][i] == 1 && visited[i] == 0)
{
cout <<" "<<i;
visited[i] = 1;
qu.push(i);
}
}
}
}
广度优先遍历结果为:125346
1.3最小生成树
一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的n-1条边,不能构成回路(如果在一棵生成树上添加一条边,必定构成一个环)
一个连通图的生成树不一定是唯一的
每棵生成树的所有边的权值之和可能不同,其中权值之和最小的生成树称为图的最小生成树
1.3.1Prim算法求最小生成树
用于构造最小生成树(构造的最小生成树不一定唯一,但权值之和一定相同)
Prim算法生成的最小生成树的边序列:(1,4)(1,3)(3,6)(3,5)(5,2)
用到两个数组:
closest[]:初始化为0,存储未选附在已选顶点的顶点编号
lowcost[]:已选与未选之间的侯选边最小权值,选中后lowcost修改为0
g.edges[j][v]:j为已选顶点,v为未选顶点
判断是否连通图:最后lowcost都为0则连通
选中顶点lowcost为0;未选中lowcost不为0;
采用邻接矩阵存储
初始化lowcost,closest数组
for(v=1;v<=n;v++)
{
遍历lowcost,选最小边
若lowcost[i]!=0,找最小边邻接点k
lowcost[k]=0;输出边(closest[k],k);
再遍历lowcost,修改其余未选lowcost
若lowcost[i]!=0&&edges[i][k]<lowcost[k]
修正lowcost[k]=edges[i][k];
}
#define INF 32767
void Prim(MGraph g,int v)
{
int lowcost[maxv],min,closest[maxv],i,j,k;
for(i=0;i<g.n;i++)//初始化
{
lowcost[i]=g.edges[v][i];closest[i]=v;//lowcost不能初始化为0,0表示已选中
}
for(i=1;i<g.n;i++)
{
min=INF;
for(j=0;j<g.n;j++)//遍历lowcost,找最小边
{
if(lowcost[j]!=0&&lowcost[j]<min) //未被选中且小于当前最小值
{
min=lowcost[j];k=j;
}
printf("边(%d,%d)权为:%d
",closest[k],k,min);
lowcost[k]=0;//选中修改lowcost值
}
for(j=0;j<g.n;j++)//遍历lowcost,修改其余未选lowcost
{
if(g.edges[k][j]!=0&&g.edges[k][j]<lowcost[j])
lowcost[j]=g.edges[k][j];closest[j]=k;
}
}
}
时间复杂度:O(n^2)
采用邻接矩阵存储更合适
1.3.2Kruskal算法求解最小生成树
按权值的递增次序选择合适的边来构造最小生成树的方法
Kruskal算法生成的最小生成树的边序列:(1,4)(1,3)(3,5)(5,2)(5,6)
用数组E存放图中所有边
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;
for(i=0;i<g.n;i++)
{
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;
}
}
InsertSort(E,g.e);//用直接插入排序对E数组按权值递增排序
for(i=0;i<g.n;i++) vset[i]=i;//初始化
k=1;//k表示当前构造生成树的第几条边,k值为1原因是有n-1条边,循环n-1次
j=0;//E中边的下标
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++;//生成边数
for(i=0;i<g.n;i++)//两集合统一编号
if(vset[i]=sn2) vset[i]=sn1;//集合编号为sn2的改为sn1;集合合并
j++;//扫描下一条边
}
}
时间复杂度:O(n^2)
采用邻接表存储更合适
克鲁斯卡尔:(0,1) (0,3) (1,2) (4,5) (2,5)
prim: (0,1) (0,3) (1,2) (2,5) (4,5)
1.4最短路径
1.4.1Dijkstra算法求解最短路径
dist[]数组存储最短路径长度,源点默认,dist[2]=5表示源点到顶点2的最短路径长度为5
path[]数组存储前驱顶点,初值或无路径为=1,path[2]=5表示顶点2的前驱顶点是5
S[]存储已选中顶点,U[]存储未选中顶点
![](https://img2020.cnblogs.com/blog/2196933/202105/2196933-20210521173353531-1611905832.png)
初始化dist[],path[],s[]
遍历所有节点
{
for(i=0;i<g.n;i++)//找最小dist
{
若s[i]!=0,则dist数组找最短路径,顶点为u
}
s[u]=1;//顶点u加入集合s[]
for(i=0;i<g.n;i++)
{
若s[i]!=0&&dist[i]>dist[u]+g.edges[u][i]
则修正dist[i]=dist[u]+g.edges[u][i]
path[i]=u
}
}
void Dijkstra(MatGraph g,int v)
{
int dist[maxv],path[maxv],s[maxv];
int min,i,j,u;
//dist和path初始化
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有边时,v为i的前驱
else path[i]=-1;//v到i没边时
}
s[v]=1;//顶点v放入s[]
for(i=0;i<g.n;i++)//循环n-1次
{
min=INF;
for(j=0;j<g.n;j++)//找最小dist[u]顶点u
{
if(s[j]==0&&dist[j]<min)
{
u=j;
min=dist[j];
}
s[u]=1;//u加入s[]
for(j=0;j<g.n;j++)
{
if(s[j]==0)//修改不在s中的顶点的dist
{
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;
}
}
}
Dispath(dist,path,s,g.n,v);//输出最短路径
用邻接矩阵存储
时间复杂度:O(n^2)
1.4.2Floyd算法求解最短路径
用邻接矩阵存储
Floyd算法适用于APSP(All Pairs Shortest Paths,多源最短路径),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法,也要高于执行V次SPFA算法。
优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。
缺点:时间复杂度比较高,不适合计算大量数据。
A[1][2]=3表示从顶点1到顶点2的最短路径长度为3
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];//A和path初始化
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++)//求A[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;
}
}
}
}
1.5拓扑排序
序列必须满足条件:每个顶点出现且只出现一次;若存在一条从a到b的路径,则在序列中a必须排在b前面
有向无环图才有拓扑排序;图中有回路,无法拓扑排序
该有向图的拓扑序列为:12435
头结点
typedef struct{
vertex data;//顶点信息
int count;//顶点入度
ArcNode *firstarc;//指向第一条弧
}VNode;
遍历邻接表
计算每个顶点的入度,存入count
遍历图顶点
若入度为0,入栈
while(栈不空)
{
出栈结点v,访问
遍历v的所有邻接点
{
所有邻接点的入度减1
若有邻接点入度为0,入栈
}
}
void TopSort(AdjGraph *g)
{
int i,j;
int st[maxv],top=-1;
ArcNode *p;
for(i=0;i<g->n;i++)
{
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++)
{
if(g->adjlist[i].count==0)//将入度为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;
}
}
}
1.6关键路径
什么叫AOE-网?
定义:在带权有向图中,以顶点表示事件,有向边表示活动,边上的权值表示完成该活动的开销,则称这种有向图为用边表示活动的网络,简称为AOE网
性质:
1、只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
2、只有在进入某一顶点的各有向边所代表的活动都已经结束时,该顶点所代表的事件才发生
什么是关键路径概念?
从源点到汇点的所有路径中,具有最长路径长度的路径
什么是关键活动?
最长路径(关键路径)上的活动