这个作业属于哪个班级 | 数据结构--网络2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 李兴果 |
0.PTA得分截图
1.本周学习总结(6分)
- 图结构与之前学的树结构一样都是非线性数据结构,但是图结构比树结构更加复杂,图结构中每一个元素都可以有零个或者多个前驱元素,也可以有零个或者多个后继元素。图常用的存储结构有邻接矩阵与邻接表。图的遍历分为广度遍历和深度遍历。深度遍历类似于树的先序遍历,是先序遍历的一种推广,简称为DFS。而广度遍历类似于树的层序遍历,是树的层序遍历的推广,简称BFS。求图的最小生成树一般用普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法。求最短路径一般使用迪杰斯特拉(Dijkstra)算法和弗洛伊德(Floyd)算法
1.1 图的存储结构
1.1.1 邻接矩阵
-
造一个图,展示其对应邻接矩阵
-
邻接矩阵的结构体定义c图函数
-
时间复杂度为O(n^2)
void CreateMGraph(MatGraph &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 邻接表
邻接表表示不唯一
特别适合于稀疏图存储
存储空间O(n+e)
- 造一个图,展示其对应邻接表
-
邻接表的结构体定义
typedef struct ANode { int sdjvex;//该边的终点编号 struct ANode*nextarc;//指向下一条边的指针 InfoType info;//该边权值等信息 }ArcNode; typedef struct Vnode { Vertex data; ArcNode*firstarc;//指向第一条边 }VNode; typedef struct { VNode adjlist[MAXV];//邻接表 int n,e; }AdjGraph;
-
建图函数
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=0;i<e;i++)//据输入边建图 { cin>>a>>b; p=new ArcNode; 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^2)
-
邻接表:特别适合于密集图存储
时间复杂度:O(n+e)
-
1.2 图遍历
深度遍历流程
从图中某个初始顶点v出发,首先访问初始顶点v
选择一个顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,依次重复1 2 两步遍历过程是一个递归的过程。
若没有下一个未被访问过的顶点,则按照路径回溯至前一个被访问过但仍有相邻点未被访问,继续访问其相邻点
若回溯至初始顶点v仍有未被访问的结点,说明此图不连通,继续以一个未被访问结点作为初始顶点,重复1 2 3步骤
直至所有顶点都已被访问
深度遍历代码
邻接矩阵
void DFS(MGraph g, int v)//深度遍历
{
int i, j;
static int flag = 0;
visited[v] = 1;//访问
//输出
if (flag == 0)
{
cout << v;
flag = 1;
}
else
{
cout << " " << v;//输出
}
for (i = 1;i <= g.n;i++)
{
/如果未访问过且二者相邻/
if (visited[i] == 0 && g.edges[i][v] == 1)
{
DFS(g, i);
}
}
}
邻接表
void DFS(AdjGraph* G, int v)//深度遍历
{
ArcNode* p;
p = G->adjlist[v].firstarc;
if (flag == 0)
{
cout << v;
flag = 1;
}
else
cout << " " << v;
visited[v] = 1; //标记已访问
while (p)
{
if (!visited[p->adjvex])//未被访问过
DFS(G, p->adjvex);
p = p->nextarc;
}
}
适用问题
两点间是否存在路径
走迷宫所有路径
1.3.2 广度优先遍历
广度优先遍历:类似于树的层次遍历,从图中的某一顶点出发,遍历每一个顶点时,依次遍历其所有的邻接点,然后再从这些邻接点出发,同样依次访问它们的邻接点。按照此过程,直到图中所有被访问过的顶点的邻接点都被访问到。最后还需要做的操作就是查看图中是否存在尚未被访问的顶点,若有,则以该顶点为起始点,重复上述遍历的过程。
广度遍历流程
访问图的初始结点v
接着访问v的所有邻接点
再按照邻接点的访问顺序来访问每一个邻接点的未被访问过的邻接点
重复步骤,直至所有结点被访问
- 广度遍历代码
- 邻接矩阵
void DFS(MGraph g, int v)//深度遍历
{
int i, j;
static int flag = 0;
visited[v] = 1;//访问
//输出
if (flag == 0)
{
cout << v;
flag = 1;
}
else
{
cout << " " << v;//输出
}
for (i = 1;i <= g.n;i++)
{
/*如果未访问过且二者相邻*/
if (visited[i] == 0 && g.edges[i][v] == 1)
{
DFS(g, i);
}
}
}
- 邻接表
void DFS(AdjGraph* G, int v)//深度遍历
{
ArcNode* p;
p = G->adjlist[v].firstarc;
if (flag == 0)
{
cout << v;
flag = 1;
}
else
cout << " " << v;
visited[v] = 1; //标记已访问
while (p)
{
if (!visited[p->adjvex])//未被访问过
DFS(G, p->adjvex);
p = p->nextarc;
}
}
- 适用问题
1.求解最短路径
2.走迷宫最短路径
1.3 最小生成树
- 一个图的极小连通图,包含原图的所有顶点,并且所有边的权值尽可能最小,无回路
概念: - 对于带权连通图,有多颗不同生成树
- 每棵生成树所有边的权值之和可能不同
- 其中权值之和最小的生成树称为图的最小生成树
1.3.1 Prim算法
求最小生成树从某个顶点开始构建生成树,每次将代价最小(权值)的新顶点纳入生成树,直到所有顶点纳入为止
- 特点:
构造的最小生成树不一定唯一,但最小生成树权值之和一定是相同的
Prim算法:
从A点出发
- 实现Prim算法的2个辅助数组是什么?其作用是什么?
(1)closest[i]:依附在U中的顶点
(2)lowcost[i]:候选边每个顶点到U中的最小边,数组
-
初始化U={v},以v到其他顶点的所有边为候选边
-
重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
1.从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
2.考察当前V-U中的所有顶点j,若(j, k)的权值小于原来和顶点k关联的候选边,修改候选边
-
Prim算法代码:
伪代码:
初始化lowcost,closest数组 for(v=1;v<n;v++) 遍历lowscst[i]!=0,找到最小边邻接点k(最小权重的顶点k) lowcost[k]=0;//lowcost[k]=0顶点在U中,输出边(closest[k],k) 再次遍历lowcost数组//修正lowcost 若lowcost[i]!=0&&edges[i][k]<lowcost[k] 修正lowcost[k]=edges[i][k] end
代码块:
#define INF 32767 //INF表示无穷 void Prim(MGraph g,int v) { int lowcost[MAXV],min,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++)//在(v-u)中找出距离u最近的顶点k点 min=INF; for(j=0;j<g.n;j++) { if(g.edges[k][j]!=0&&g.edges[k][j]<lowcost[j]) { lowcost[j]=g.edges[k][j]; closest[j]=k; } } }
-
-
分析Prim算法时间复杂度,适用什么图结构
时间复杂度:O(n^2)
普里姆算法对于稠密图,即边数非常多的情况会更好一些
1.3.2 Kruskal算法求解最小生成树
每次选择一个权值最小的边,使得这条边连通,原本就已经连通的就不选,直到所有结点都连通
以A为顶点出发:
-
实现Kruskal算法的辅助数据结构
(1)设计算法:
-
采用邻接表结构存储更加合适
-
定义数组存数
-
高效排序算法
-
集合应用解决回路问题
(2)伪代码:
构造非连通图ST=(V,{}); k=i=0;//k计算选中的边数 while(k<n-1) { ++i; 检查边集E中第i条权值最小的边 若最小边加入ST后不使ST中产生回路,则输出边 且k++; }
代码:
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++; //扫描下一条边 } }
-
分析Kruskal算法时间复杂度,适用什么图结构
时间复杂度:O(eloge)
Kruskal算法对于稀疏图,即边数少一点的情况会更好一些
-
1.4 最短路径
Dijkstra算法使用了广度优先遍历解决赋权有向图或者无向图的单源最短路径问题Dijkstra算法采用的是一种贪心的策略,每次选取最优的点,逐渐拓展至整个图,以求出整个图的最短路径
-
基本思想:
1.采用二维数组邻接矩阵的形式储存图并将图初始化
2.选择其中一个顶点作为计算最短路径的起点
3.构造一个d一维数组dis[n],其中n是顶点个数,dis用来记录最短路径距离。初始化dis,其值为图中各点到起点的直接距离
4.每次中dis数组中找出最小值,该值就是起点到该点的最短路径距离
5.在加入了一个新的确定了点之后就需要更新dis数组,看其余点能否通过这个确定的点到达起始点且距离能够更短
6.重复4、5步,直到所有点都找到了最短路径
1.4.1 Dijkstra算法求解最短路径
1.首先,初始化dis数组,用集合S代表已找到最短路径的点
-
A B C D E 0 10 3 无穷 无穷 S={A}
2.找出dis数组中最小值1,将C点加入合集S
A B C D E 0 7 3 11 5 3.选出dis中最小值5,对应点E,将E加入集合S
A B C D E 0 7 3 11 5 .......
A B C D E 0 7 3 9 5 - Dijkstra算法需要哪些辅助数据结构
存放最短路径长度:dist[j]
存放最短路径:path[j]
-
Dijkstra算法特点
1.不适用带负权值的带权图求单源最短路径
2.不适用求最长路径长度
3.最短路径长度是递增
4.顶点u加入S后,不会再修改源点v到u的最短路径长度
5.按Dijkstra算法,找第一个距离源点S最远的点A,这个距离在以后就不会改变。但A与S的最远距离一般不是直连 -
Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码
-
Dijkstra算法本质上是贪心算法,下一条路径都是由当前更短的路径派生出来的更长的路径。不存在回溯的过程。*
伪代码:
初始化dist数组,path数组,s数组
遍历图中所有节点
{
for(i=0;i<g.n;i++)//找最短dist
{
若s[i]!=0,则dist数组找最短路径,顶点为u
}
s[u]=1//加入集合S,顶点已选
for(i=0;i<g.n;i++)//修正dist
{
若s[i]!=0&&dist[i]>dist[u]+g.edges[u][i]
则修正dist[i]=dist[i]>dist[u]+g.edges[u][i]
path[i]=u;
}
代码块:
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++)//找最小路径长度顶点u
{
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算法的时间复杂度,使用什么图结构,为什么。
时间复杂度为O(n^2)
存储结构:邻接矩阵存储
适用范围:不适用带负权值的带权图求单源最短路径,也不适用于求最长路径长度
1.4.2 Floyd算法求解最短路径
每一个顶点都是出发访问点 ,所以需要将每一个顶点看做被访问顶点,求出从 每一个顶点到其他顶点的最短路径
- Floyd算法解决什么问题?
解决多源最短路问题
-
Floyd算法需要哪些辅助数据结构
用一维数组dist[j]存储(distance):源点V0到每个终点的最短路径长度
源点v默认,dist[j]表示源点→顶点j的最短路径长度
eg. dist[2]=12 表示源点→顶点2的最短路径长度为12
-
如何存放最短路径
-
用一维数组path[j]存储(path):最短路径序列的前一个顶点的序号;初值或无路径用-1表示
一条最短路径用一个一维数组表示
从顶点0→5的最短路径为0、2、3、5,表示为path[5]={0,2,3,5}
- 从源点到其他顶点的最短路径有n-1条,二维数组path[] []存储
Floyd算法优势
Floyd算法适用于APSP(AllPairsShortestPaths),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法。可以算出任意两个节点之间的最短距离,代码编写较为简单
* Floyd算法是解决任意两点间的最短路径的一种算法,可以正确处理有向图或
* 无向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包。
- Floyd算法时间复杂度及适用范围
时间复杂度:O(n³)
适用范围:弗洛伊德算法可以解决负权值的带权图,也可以解决求最长路径长度问题。。
1.5 拓扑排序
将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。在一个有向图中找一个拓扑序列的过程称为拓扑排序。序列必须满足条件:
(1)每个顶点出现且只出现一次。若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面
(2)有向无环图才有拓扑排序,图中有回路,无法拓扑排序.拓扑排序可以用来检测图中是否有回路.实现拓扑排序代码,结构体如何设计?
拓扑排序为:acbfde
- 伪代码
遍历邻接表
计算每个顶点的入度,存入头结点count成员中;
遍历图顶点
找到一个入度为0的顶点,入栈/队列/数组;
while(栈不为空)
出栈结点v,访问;
遍历v的所有邻接点
{
所有邻接点的入度-1;
若有邻接点入度为0,入栈/队列/数组;
}
- 代码块:
- 实现拓扑排序代码,结构体如何设计
typedef struct {
Vertex data;//顶点信息
int count;//增加数据域:存放顶点入度
AreNode *firstarc;//指向第一个邻接点
}VNode;
- 书写拓扑排序伪代码,介绍拓扑排序如何删除入度为0的结点?
/拓扑排序伪代码/
Status TopologicalSort{
邻接表构图
if(图G没有形成回路)
则输出图G的顶点的一个拓扑序列并返回OK,否则返回ERROR
FindInDegree(G, indegree); /*对各顶点求入度*/
InitStack(S);/*对栈进行初始化*/
for i:0~G.vernum
if(入度为0)
则进栈Push(S, i)
对输出顶点计数count = 0
while当栈不为空
{
Pop(S,i);
输出i号顶点
计数++count
for(p=G.vertices[i].firstarc; p; p=p->nextarc)
{
k = p->adjvex;
对顶点i的每个邻接点的入度-1
if(入度减为 0)
入栈Push(S, k)
}
}
}
/*删除入度为0的结点*/
while(栈不为空)
{
出栈v,访问;
while//遍历v所有的邻接点
{
将所有邻接点的入度-1
当入度为0时,则入栈
}
}
-
使用邻接表 当某个顶点的入度为0时 输出顶点信息 设置栈来存放入度为0的顶点*
-
如何用拓扑排序代码检查一个有向图是否有环路?
-
每次找入度为0的点 进入输出队列 然后将与此点相连的节点入度减1 重复做
当做n-1 次后还有点没进输出队列 那么这些点就是环上的 因为环上的各点入度都为1,没有0的 就不能更新。
1.6 关键路径
AOE网(Activity On Edge Network)是边表示活动的网,AOE网是带权有向无环图。边代表活动,顶点代表所有指向它的边所代表的活动 均已完成这一事件。由于整个工程只有一个起点和一个终点,网中只有一个入度为0的点(源点)和一个出度为0的点(汇点)。
用顶点表示事件,用有向边e表示活动,边的权表示活动时间,是一个带权的有向无环图。在AOE网中不应该出现有向环。
- 整个工程完成的时间为:从有向图的源点到汇点的最长路径,又叫关键路径
关键路径的边称为关键活动
关键词含义 - AOE网一一带权的有向无环图
- 顶点--事件或状态
- 弧(有向边)---活动及发生的先后关系权---活动持续的时间
- 起点--入度为0的顶点(只有一一个)终点--出度为0的顶点( 只有一一个)
2.PTA实验作业(4分)
2.1 六度空间(2分)
- 思路:根据题意,我们需要找到关系网在 6 层以内的结点
多开一个成员来记录层次的信息。那么层次的信息怎么操作
首先我们先初始化为 0,接下来每进行一轮 BFS,添加入队列的顶点就从它的上一层继承层数
2.1.1 伪代码
- 伪代码:
传入 AdjGraph 类型邻接表 G ;
传入 int 类型被判断顶点序号 v ;
定义 AVertex 类型数组 a que 为 BFS 所用队列;
定义 int 类型变量 count 记录6层之内结点数;
顶点 v 入队列;标记顶点 v 已访问,层次为第0层;
while (队列不为空) do if (队列头 level ==6) do break ;
end if 将队列头的所有没访问过的后继顶点入队列且 level 为队列头的 level +1;
count ++;队列头出队列;
end while return count ;
2.1.3 本题知识点
1.用深度遍历解决
2.使用队列
2.2 村村通或通信网络设计或旅游规划(2分)
2.2 村村通
现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
该题求公路连通村庄所需要的最低成本,即建造最小生成树,并求最小生成树中权值的和。使用邻接矩阵更利于得到两点顶点之间的关系,因此我选择本题使用邻接矩阵和Prime算法来计算最小生成树所得最低成本。
- 伪代码
邻接矩阵建图;
最小生成树Prime算法
int Prim(MGraph* g)
{
建立边权值lowcost,顶点编号clostest两个数组;
给lowcost和clostest初始化置初值;
从顶点1开始lowcost[1] = 0;
for i = 0 to n
{
将最小值min置初值INF;
if 顶点i未被访问过且二者最小边小于最小值min
最小值min等于该最小边;
k记录最小边对应点编号;
end if;
花费cost等于最小边结点lowcost相加;该节点置为已标记状态;
//修正数组lowcost和clostest
for i = 1 to n
{
if 未被访问且结点i与k权值小于二者最小边lowcost
修正lowcost等于edges[i][k];
end if;
}
}
//判断是否连通
for i = 1 to n
{
if lowcost[i] != 0
不连通 return-1;
end if;
}
连通 return cost;
}
- 本题知识点
1.邻接矩阵要使用二维指针数组,动态开辟空间方法为g->edges = new int* [n + 1];
2.以当前情况为基础做最优解,在每轮循环之后需要根据当前情况进行数组lowcost和clostest的修正
3.在做题时需要注意lowcost和clostest两数组的含义与用处,不能混淆 - clostest:记录最小生成树的边在U中的顶点编号
- lowcost:顶点i到最小生成树中的点的边权重,取最小边权重加入最小生成树,在最小生成树中的点lowcost置为0
最后要判断该图是否能够连通,若遍历完成但lowcost未完全置0,说明该图不流通