这个作业属于哪个班级 | 数据结构-网络20 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 余智康 |
目录
0. 展示PTA总分
1. 本章学习总结
2. PTA实验作业
0.PTA得分截图
1.本周学习总结
本次所有总结内容,请务必自己造一个图(不在教材或PPT出现的图),围绕这个图展开分析。建议:Python画图展示。图的结构尽量复杂,以便后续可以做最短路径、最小生成树的分析。
该图引自被B站 "《空洞骑士》:人物关系图谱",地址:https://www.bilibili.com/read/cv6403674/
修改后的关系图(部分):
1.1 图的存储结构
1.1.1 邻接矩阵
造一个图,展示其对应邻接矩阵
邻接矩阵的结构体定义
建图函数
- 结构体:
- 1)顶点的结构体:顶点编号、其它信息
- 2)图的结构体:二维数组(存放权值)、一维数组(存放顶点信息)、整型变量(存放顶点数、边数)
#define MAXV 100 //最大顶点数
typedef struct
{
int no; //顶点编号
InfoType info; //其它信息
}VertexType; //顶点结构体
typedef struct
{
int weight; //边
string relation; //关系
}EDGES;
typedef struct
{
EDGES edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数、边数
VertexType vexs[MAXV]; //存放顶点信息
}MatGraph; //图的结构图
-
存储:
- 以 “空洞骑士关系图(部分)” 为例
- 以 “空洞骑士关系图(部分)” 为例
-
申请空间:
1)因为0不使用,所以申请 node+1 个。
2)先给二级指针申请 node+1 个存放一级指针的空间,再 循环 给每一个一级指针申请 node+1个 存放结点数据的空间
g.edges = new EdgesType * [node+1];
for (i = 0; i <= node; i++)
{
g.edges[i] = new EdgesType[node+1];
}
- 建图(有向图):
- void CreatNode(),输入结点的信息:编号、名称,存放在 g.vexs[]数组中
- void CreatMGraph(),
1)创建邻接矩阵 :①邻接矩阵初始化 -> ②输入结点编号,(输入权值),修改对应位置的权值 -> ③ g.e = e, g.n = n;
1)建邻接矩阵的循坏条件,for i = 0 to e-1
void CreatNode(MGraph &g, int n) //输入结点信息
{
int No;
for(int i=1; i<=n; i++)
{
cin >> No;
g.vexs[No].no = No;
cin >> g.vexs[No].info;
}
}
void CreatMGraph(MGraph &g, int n, int e)
{
int i,j; //循环计数变量
int a,b;
//二维数组初始化
for(i=0; i<=n; i++)
for(j=0; j<=n; j++)
g.edges[i][j] = 0;
for(i=0; i<e; i++)
{
cin >>a >>b ;
cin >>g.edges[a][b].relation >>g.edges[a][b].weight;
}
g.n = n;
g.e = e;
}
1.1.2 邻接表
造一个图,展示其对应邻接表(不用PPT上的图)
邻接矩阵的结构体定义
建图函数
-
存储:
- 以 “空洞骑士关系图(部分)” 为例
- 以 “空洞骑士关系图(部分)” 为例
-
结构体:
typedef string InfoType,Vertex;
typedef struct ANode //边的结构体
{
int adjvex; //终点编号
struct ANode *nextarc; //下一条边的指针
InfoType info; //该边信息
double weight; //该边权值
}ArcNode;
typedef struct Vnode //结点的结构体
{
Vertex data; //顶点信息
ArcNode *firstarc; //第一条边
}VNode;
typedef struct
{
VNode adjlist[MAXV]; //邻接表
int n, e;
}AdjGraph;
- 建图:
void CreatAdj(AdjGraph *&G, int n, int e)
{
定义 p 为 指向ArcNode数据类型的指针
G = new AdjGraph G动态申请内存空间
for i=o to n do
G->adjlist[i].firstarc = NULL 初始化
end for
for i=0 to e-1 do
输入结点编号,a,b
p 申请内存空间,
p -> adjvex = b; b为终点编号
头插法将 p 插入 G->adjlist[a].fistarc
end for
G->n = n; G->e = e;
}
1.1.3 邻接矩阵和邻接表表示图的区别
各个结构适用什么图?时间复杂度的区别。
- 邻接矩阵:
- 适用于稠密图,稀疏图使用邻接矩阵会有较大的空间浪费
- 因为需要遍历二维数组,时间复杂度为 O(n^2)
- 邻接矩阵:
- 适用于稀疏图
- 时间复杂度为 O(n+e)
1.2 图遍历
1.2.1 深度优先遍历
选上述的图,继续介绍深度优先遍历结果
深度遍历代码
深度遍历适用哪些问题的求解。(可百度搜索)
-
"Wyrm"开始深度遍历结果:
- Wyrm(1) -> The Pale King(3) -> Herrah the Beast(2) -> Hornet(5) -> The White Lady(4) -> Dryya(6) -> Hegemol(7) -> False Knight(13) -> Maggot(12) -> Isma(8) -> Ogrim(9) -> Ze'mer(10) -> The daught of The Lord(11)
-
程序运行截图:
-
深度遍历代码:
void DFS(MGraph &g, int v)
{
if(v > g.n) return;
cout输出结点信息
visited[v] = 1; //标记为已读
while (未遍历完 v 的出度结点)
{
if 未访问过的结点 && 有权值 do
DFS(g, i) 进行递归
end if
移动到 v 的下一个出度结点
}
}
1.2.2 广度优先遍历
选上述的图,继续介绍广度优先遍历结果
广度遍历代码
广度遍历适用哪些问题的求解。(可百度搜索)
-
"Wyrm"开始广度遍历结果:
- Wyrm(1) -> The Pale King(3) -> Herrah the Beast(2) -> The White Lady(4) -> Hornet(5) -> Dryya(6) -> Heqemol(7) -> Isma(8) -> Ogrim(9) -> Ze'mer(10) -> False Knight(11) -> The daught of The Lord(13) -> Maggot(12)
-
程序运行截图:
-
广度遍历伪代码:
void BFS(MGraph g, int v)
{
if (v 结点没有出度 ) do 输出 v结点的信息 return
queue<int>que; //邻接表和临界矩阵中,队列类型均为 int型,之前邻接表犯过将其设为 ArcNode* 型的错误
v 入队列
记录 v 已经访问
while(队列不空)
{
取队头,赋值到 p中; 并出队;
cout 输出 p的信息
now 赋值为 p第一个出度结点(邻接表中ArcNode*)
while( p 的出度结点 未遍历 完) do
if 该出度结点未访问过 && 有权值 do
该结点 编号 入队列;并记录为已访问
end if
end while
}
}
- 问题的求解
-
- 只要找到问题的一种解决方式时,深度遍历比较快速,但不一定找到问题的最优解;
-
- 在问题存在多种解决方式时,广度遍历能给出全部可行的解决方案,可用于比较,从而选出最可能的解决方案;
-
- 深度遍历如本学期学习数据结构时,在线性表章节的迷宫寻路问题,一开始便是深度搜索找到一条路径;当不仅仅要找到走出迷宫的路径,还要比较多种路径,从而找出最短路径时,则使用了广度搜索。
-
1.3 最小生成树
用自己语言描述什么是最小生成树。
- 自己的语言解释最小生成树:
- 生成树:由图的 n 个顶点去掉多余的边由(n-1)条边连接的树。
- 最小生成树:权值之和最小的生成树
- 生成树是树,不是图,没有箭头。
1.3.1 Prim算法求最小生成树
基于上述图结构求Prim算法生成的最小生成树的边序列
实现Prim算法的2个辅助数组是什么?其作用是什么?Prim算法代码。
分析Prim算法时间复杂度,适用什么图结构,为什么?
-
Prim算法:
- Prim算法构造最小生成树的过程就是将右边圆圈中“候选区”的结点不断选中并放入左边矩形的“选中区”中;
-
辅助数组:
- lowcost[i]: lowcost也就是“候选区”,表示以i为出度的边的最小权值。当lowcost[i] = 0时,表示 i 结点已经加入到了最小生成树中;
- closet[i]: closet[i] 对应lowcost[i]的入度,则<closet[i],i>是最小生成树的一条边。其中 closet[i] = 0 表示以 i 结点作为树的根节点。
-
Prim算法操作:
-
初始化lowcost[], 其中lowcost[i]的值为v为入度,i为出度的权
值,权值为0 则置为INF(∞); -
初始化closet[],其中closet[i] = v。
-
从lowcost[]中找到最小的点 j ,进入最小生成树。并将 lowcost[j] = 0;
-
遍历顶点,若 weight<j,i>小于 lowcost[i] 则修改 lowcost[i],并修改closet[i]的入度为j。
-
循环3、4 总共进行 n-1 次,则n个顶点全部进入最小生成树;
- Prim代码:
void Prim(MGraph g, int v)
{
int* lowcost = new int[g.n + 1];
int* closet = new int[g.n + 1];
int min, node_min;
int i, k;
//1. 初始化lowcost[], 其中lowcost[i]的值为v为入度,i为出度的权值,权值为0 则置为INF(∞);
//2. 初始化closet[],其中closet[i] = v。
for (i = 0; i <= g.n; i++)
{
if (g.edges[v][i].weight != 0)
{
lowcost[i] = g.edges[v][i].weight;
}
else
{
lowcost[i] = INF;
}
closet[i] = v;
}
lowcost[v] = 0;
for (k = 0; k < g.n - 1; k++)
{
//3. 从lowcost[]中找到最小的点 j ,进入最小生成树。并将 lowcost[j] = 0;
min = INF;
for (i = 0; i <= g.n; i++)
{
if (lowcost[i] > 0 && lowcost[i] < min)
{
min = lowcost[i];
node_min = i;
}
}
lowcost[node_min] = 0;
cout << closet[node_min] << " and " << node_min << " weight-> " << min << endl;
//4. 遍历顶点,若 weight<j, i>小于 lowcost[i] 则修改 lowcost[i],并修改closet[i]的入度为j。
for (i = 0; i <= g.n; i++)
{
if (g.edges[node_min][i].weight != 0 && g.edges[node_min][i].weight < lowcost[i])
{
lowcost[i] = g.edges[node_min][i].weight;
closet[i] = node_min;
}
}
}
//5.循环3、4 总共进行 n - 1 次,则n个顶点全部进入最小生成树;
delete[] lowcost;
delete[] closet;
}
-
敲Prim代码时的 错误日志
- 错误一: 333 行 if (lowcost[i] != 0 && lowcost[i] < min) 误写为 if (lowcost[i] != 0 && min < lowcost[i])
- 错误二: 误将 330行的 min = INF放入 for循环中,导致找不到最小值
- 错误三: 339行、340行的 i 改为 node_min
- 错误四: 343行-351行的 for循环 内使用了未初始化的 j,改为node_min
- 错误五: 304、305行 new的空间 误写成 new lowcost = int[v+1],导致申请的空间不足,出错。改为new lowcost = int[g.n + 1]
- 错误六: 345行 if语句无需判断 lowcost[i] 是否为0, 改为判断 g.edges[node_min][i].weight 是否为0
- 错误七: 312行 lowcost[g.n]最后一个结点没有初始化
-
Prim算法得到的边序列
-
Prim时间复杂度:
- 两层for循环嵌套,时间复杂度 O(n^2)
-
Prim适用的图:
- 由于Prim算法和边数无关,执行的次数和顶点个数 n 有关,故比较适合稠密图。
1.3.2 Kruskal算法求解最小生成树
基于上述图结构求Kruskal算法生成的最小生成树的边序列
实现Kruskal算法的辅助数据结构是什么?其作用是什么?Kruskal算法代码。
分析Kruskal算法时间复杂度,适用什么图结构,为什么?
-
Kruskal算法:
- 1)ST 为空集
- 2)按边的权值 从小到大 放入边集 E 中
- 3)依次从 E 选择边放入 ST 中
- 3)筛选时,若所选择的边放入 ST 中构成了回路,则 舍弃 该边
- 4)返回步骤 3),直到 ST 中包含(n-1)条边
- 备注:工具树 并查集 同一个根 回路
-
辅助数组:
- 1)边集合 E,按权值 从小到大 存放边,E的结构体里的数据项: 初始顶点、终止顶点、边的权值
- 2)vset数组,用于并查集。存放每个结点的根,判断两个结点的根是否相同,从而判断是否在 同一个工具树 上,若在,则这两个结点会构成回路,舍弃该边。
-
Kruskal伪代码:
for i=0 to G.n do
vset[i] = i; //初始化
end for
for i=1 to G.n do
p = G.adjlist[i].firstacr;
while p!=NULL do
将 p 中的终点编号 和 权值 以及 i(起始结点编号)放入 E[]中
p = p-> nextarc;
end while
end for
调用函数,将 E[]排序
重置i,j未0
while i < G.n-1 do
取出 E[j]的结点编号,权值
检查两个结点是否存在于同一个集合
if vset[node_1] != vset[node_2]
for k=0 to G.n
修改集合编号
end for
输出
end if
end while
-
Kruskal算法得到的边序列
-
Kruskal时间复杂度:
- 上面那个的 Kruskal 代码的时间复杂度应该为 O(n^2)。
- 若将排序改为 堆排序,而且使用 并查集 ,则时间复杂度为 O(e log2 e)
-
Kruskal适用的图:
- Kruskal 算法的时间复杂度为 O(e log2 e),与图的结点数 n 无关,仅与边数 e 有关。则适合于稀疏图
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
基于上述图结构,求解某个顶点到其他顶点最短路径。(结合dist数组、path数组求解)
Dijkstra算法需要哪些辅助数据结构
Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码。
Dijkstra算法的时间复杂度,使用什么图结构,为什么。
-
**基于上图,求 结点 “Wyrm(1)”到 “Ogrim(9)”的最短路径。(
图选的不成,这最短路径0.0)- Dijkstra:
- S 已经选入的点的集合; T 为选入点的集合;
- 1)若存在 <V0,vi>, 距离的值为<V0,Vi>弧上的权值;
- 2)若不存在,则距离为 INF(无穷);
- 3)从 T 中选择一个距离值最小的顶点W,加入 S;
- 4)在 W 加入 S 后,修改 T 中的距离值;
- 重复 3)4)直到 S 中包含所有顶点;
- Dijkstra:
-
辅助数组:
-
存放最短路径长度:一维数组 dist[],源点 v 默认
)其中dist[j] 表示源点 -> 顶点 j 的最短路径长度。
)dist[2]=12 表示源点 -> 顶点 2 的最短路径长度为12 -
存放最短路径:一维数组 path[]
) 一条最短路径用一个一维数组表示,如顶点1 -> 9的 最短路径为 1、3、9。表示为 path[] = {1,3,9}
)最短路径序列的前一项的顶点编号,无路径用 -1 表示
-
-
结果: Wyrm(1) -> The Pale King(3) -> Ogrim(9)
-
过程:
-
Dijkstra如何解决贪心算法的问题
- 通过修改 dist[]数组,使每次选中的结点到源点的距离均是最短;
- 代码:
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] do
{
dist[j]=dist[u]+g.edges[u][j];
path[j]=u;
}
}
- Dijkstra的时间复杂度,适合的图结构:
- 两层 for循环 嵌套, O(n^2)
- 适合 邻接矩阵 存储
- 备注:Dijkstra算法 不适用于带负数权值的带权图求最短;
—————不适用求最长路径:由于找到一个当前距离源点S最远的A点,则该段路径固定,但可能存在源点 S 到 B点,B点到A点,这两段加起来比 直接 S到A点更远。
1.4.2 Floyd算法求解最短路径
Floyd算法解决什么问题?
Floyd算法需要哪些辅助数据结构
Floyd算法优势,举例说明。
最短路径算法还有其他算法,可以自行百度搜索,并和教材算法比较。
-
Floyd算法的辅助数组:
- 二维数组 A 存放当前顶点之间的最短路径长度,如 A[i][j]表示当前顶点i到顶点j的最短路径长度
- 完了这个蛤子Floyd看不懂,来不及了,来不及了,这个就放着吧(;′⌒`)
-
Floyd算法解决哪些问题:
-
Floyd算法的优势:
-
其它最短路径的算法:
1.5 拓扑排序(
找一个有向图,并求其对要的拓扑排序序列
实现拓扑排序代码,结构体如何设计?
书写拓扑排序伪代码,介绍拓扑排序如何删除入度为0的结点?
如何用拓扑排序代码检查一个有向图是否有环路?
-
找一个有向图,并求其对要的拓扑排序序列
-
拓扑排序序列:
- 1)有向图中删去一个 没有前驱 的顶点,输出;
- 2)删去 以该顶点为 尾 的弧;
- 3)重复 1)2)直至 图空 ,或 图不空但找不到无前驱顶点 为止
- 备注: 若图不空 但找不到无前驱顶点,则是 存在环
-
上图 拓扑排序 结果:
-
拓扑排序结构体:
- !表头结点增加了 顶点入度的数据项
typedef struct
{
vertex data; //顶点信息
int count; //入度
ArcNode* firstarc; //第一条弧
}VexNode;
- 拓扑排序伪代码:
遍历邻接表
计算每个顶点的入度,存放在 表头结点的 count 中
遍历图顶点
将 入度为0 的顶点,入栈, 删除该结点
while 栈不空 do
出栈,v 结点 p = G->adjlist[v].firstarc
while p != NULL do
v 所有邻接点的入度 -1;
若 此时 入度为0 则入栈, 并在图中删去该结点
p = p->nextarc;
end while
end while
-
拓扑排序删去结点:
- 类似链表的删除操作
- 1)先判断 v 结点的第一条弧是否为空
- 2)判断第一个邻接点 的入度在 -1后 是否为 0
- 2) ① 若 -1后入度为0,则将该邻接点入栈。 并修改表头结点的 firstarc 为 该邻接点的nextarc,再delete掉该邻接点。最后重复 2)直到第一个邻接点的入度不为 0 或是 第一个邻接点不存在
- 2) ② 若 -1后入度不为0,则 p = firstarc,以 while(!p->nextarc)为循环条件,if(G->adjlist[p->nextarc.adjvex].count == 0) 则 用 temp存放 p->nextarc, 再 p = p->nextarc->nextarc,最后delete temp
-
检查一个有向图是否有环路:
* 1)在拓扑排序时,把有向图的顶点 真正上删除 ,然后再拓扑排序完遍历一下,看看图是否为空。若图不为空,则有环。
* 1)或者,假的删除,将拓扑排序中删去的顶点的 count 项赋值为 -1.拓扑排序完,统计是否有结点的 count 项不为 -1,则有环
1.6 关键路径(
什么叫AOE-网?
什么是关键路径概念?
什么是关键活动?
-
AOE-网:
- AOE网 为带权值的有向无环图;
- 顶点 为事件或状态;
- 弧 为活动发生的先后关系;
- 权值 为活动持续的时间
- 起点 入度为 0 的顶点 (仅有一个)
- 终点 出度为 0 的顶点 (仅有一个)
-
关键路径概念:
- 关键路径 为源点到汇点的 最长 路径, -> 转变为图的最长路径问题
- 备注: 求图的最长路径 不能 使用求最短路径的 Dijkstra算法实现。
-
关键活动:
- 关键活动: 在关键路径上的活动 都是关键活动
- 关键活动不存在富余时间
2.PTA实验作业
2.1 六度空间
选一题,介绍伪代码,不要贴代码。请结合图形展开分析思路。
2.1.0 思路
- 计数,距离不超过6
- 遍历
- 层次遍历! 如树的层次遍历并输出高度, 队列!lastNode!
2.1.1 伪代码(贴代码,本题0分)
建图;
for i=1 to G->n do
//CountNode(G,6,i)用于依次求得 每个顶点距离6以内的顶点数,并返回
proportion = (double)CountNode(G, 6, i) * 1.0 / node * 100;
输出
end for
int CountNode(AdjGraph* G, int distance, int v)
v 入队列,并标记为已读,同时count++ //(每次有数据进入队列,则count++)
while 队列不空 do
取队头,出队
取队头 的第一条弧 // p = firstarc
while p 不为空
若p->No结点未访问过,入队列,已访问,count++
p移动到 p->nextarc
end while
end while
2.1.2 提交列表
2.1.3 本题知识点
- 图的层次遍历
- 层次遍历中使用 lastNode 确定距离
2.2 旅游规划
2.2.1 思路:
- 最短路径 -> Dijkstra算法 -> 邻接矩阵 -> 无向图还是有向图 (思考:先试用有向图)
- 相同距离 取费用最低 -> Dijkstra算法使用时遇到多个相同的路径长度,path用二维数组(思考:string是否有一维数组的数据类型,可否使用),dist使用二维数组,与path对应。同时新引入一维cost数组,用于比较费用 -> 得出适合路径。
- (思考:邻接矩阵的二维数组是否可以使用结构体,用来存放距离和花费)
2.2.1 伪代码(贴代码,本题0分)
伪代码为思路总结,不是简单翻译代码。
部分结构体
typedef struct
{
int dist;
int cost;
}DIST_COST;
typedef struct
{
DIST_COST** edges;
int n,e;
}MGraph;
建图
......
2.2.2 提交列表
2.2.3 本题知识点