DS博客作业04--图
0.PTA得分截图
1.本周学习总结(0-5分)
1.1 总结图内容
-
图
-
定义:顶点集V和顶点间的关系:边集合E组成的数据结构
-
基本术语:
·无向图:没方向边(如图)
·有向图:由顶点集和弧集(有方向的边)构成的图(如图)
·完全图:
·完全无向图:每两个顶点之间都存在这一条边
·完全有向图:每两个顶点之间都存在着方向相反的两条边
·稠密图:当一个图接近完全图时
·稀疏图:当一个图含有较少边数是
·度,入度,出度:
度:无向图以顶点i为端点的边数
入度:以顶点i为终点的入边的数目
出度:以顶点i为始点的出边的数目
·路径长度:一条路径上经过的边的数目
·简单路径:一条路径上除开始点和结束点可以相同外,其余顶点均不相同
·回路,环:一条路径上的开始点与结束点为同一个顶点
·简单回路,简单环:开始点与结束点相同的简单路径
·连通,连通图和连通分量:
·连通图:图中任意两个顶点都相通
·连通分量:无向图G中的极大连通子图
注:任何连通图的连通分量只有一个,为连通图本身;而非连通图由多个连通分量。
·权和网:
·图中每一条边都可以附有一个对应的数值,即为权
·网:也称为带权图,即边上带有权的图。
-
图的存储结构:
邻接矩阵
适用于稠密图
存储类型定义如下:
typedef struct { int no; InfoType info; } VertexType; typedef struct { int edges[MAXV][MAXV]; int n,e; VertexType vexs[MAXV]; } MatGraph; MatGraph g;
注:图的邻接矩阵表示是唯一的
邻接表
对于图中的每一个顶点i建立一个单链表,将顶点i所有的邻接点都连起来
是一种顺序分配和链式分配相结合的方法
存储类型定义如下:
typedef struct Vnode
{ Vertex data;
ArcNode *firstarc;
} VNode;
//表明邻接表头结点类型
typedef struct ANode
{ int adjvex;
struct ANode *nextarc;
InfoType info;
} ArcNode;
//表明边结点类型
typedef struct
{ VNode adjlist[MAXV] ;
int n,e;
} AdjGraph;
//声明图邻接表类型
AdjGraph *G;//声明一个邻接表存储的图G
-
图的遍历
-
定义:从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次
-
深度优先遍历
·实质:对每个顶点查找邻接点的过程
·伪代码:
访问v节点 遍历v的邻接点w 若w未被访问,递归访问w节点
-
广度优先遍历
·实质:利用队列,类似于层次遍历进行遍历
·伪代码
建一个访问队列q 访问v节点,加入队列q while(队列不空) 取队头元素w 遍历w的邻接表 取邻接点j 若j未被访问,则加入队列q,并访问j。 end while
-
判断图是否连通
·利用深度优先遍历
·思路:给定一个visited[]数组并将其中所有元素初始化为0,从0开始进行DFS,结束遍历后若所有顶点i的visited[i]都为1 则说明图连通
·伪代码
bool Connect(AdjGraph *G) { 定义i; 定义flag为true; for i=0 to G.n visited[i]=0;//赋初值 调用DFS算法从0进行遍历; for i=0 to G.n if 顶点没有被访问到 flag=false; return true; }
-
查找图简单路径
·简单路径:即路径上的顶点不重复
·利用深度优先遍历
·思路:在DFS基础上加上v和has两个形参,has表示u到v是否有路径,若有,has赋值为true进行返回。
·伪代码:
void ExistPath(AdjGraph *G,int u,int v,bool &has) { 定义结点p; 定义w; visited[u]=1;//置u已访问标记 if u等于v //找到一条路径 has=true; return; DFS遍历; }
-
找最短路径
·利用深度优先遍历
·思路:给定一个数组path[]存放路径,给定形参d了解遍历到了哪里即递归深度。利用DFS从顶点u遍历到顶点v,结束遍历后输出path。
·伪代码:
void FindaPath(AdjGraph *G,int u,int v,int path[],int d) { 定义结点p; 定义w,i; d++; path[u]=1;//置u已访问标记 if u等于v //找到一条路径 输出并返回; DFS遍历; }
-
生成树和最小生成树
-
生成树
-
定义:一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边,不能回路。
-
广度优先生成树:由BFS遍历所得的生成树
-
深度优先生成树:由DFS遍历所得的生成树
-
最小生成树
-
定义:图的所有生成树中具有边上的权值之和最小的数
-
普里姆(Prim)算法
·一种增量算法,一步一步的选择最小边,并在U集合中添加相应的顶点。即利用贪心算法的原理,局部最优经过调整后成为全局最优,这种算法不需要回溯。
·图存储结构:邻接矩阵(需要频繁的取一条条的边)
·新建两个数组:closest[]和lowcost[],用于记录V-U中顶点j到U顶点的最小边(为节省空间)
·时间复杂度:O(n^2) 空间复杂度:O(n^2)
·伪代码:
初始化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] 修正closest[j]=k; end
-
克鲁斯卡尔(Kruskal)算法
·一种按权值的递增次序选择合适的边来构造最小生成树的方法
·图存储结构:邻接矩阵(频繁的取一条条边)
·新建一个数组:vest[]用于记录一个顶点i所在的连通分量编号
·时间复杂度:O(eloge) 空间复杂度:O(n)
·伪代码:
void Kruskal(MatGraph g) { 定义 i,j,u1,v1,sn1,sn2,k; 定义辅助数组vest[]; 定义边结构体数组 E[]; for i=0 to g.n for j=0 to g.n 对图中的边赋值;//起始顶点,终止顶点,权值 采用直接插入排序对E按权值排序; 初始化vest[]; k=1;//表示当前构造生成树的第几条边 j=0;//E边的下标 while k<g.n { 取一条边的两个顶点赋予u1,v1; 得到两个顶点所属的集合编号于sn1,sn2; if sn1不等于sn2 { 生成边数k++; for i=0 to g.n 集合编号为sn2的改为sn1; } j++; } }
·改进克鲁斯卡尔(Kruskal)算法
·一:将边集排序改为堆排序 二:采用并查集进行连通分量合并
·改进后时间复杂度:O(log2 n)
·改进后的伪代码:
void Kruskal(MatGraph g) { 定义 i,j,u1,v1,sn1,sn2,k; 定义并查集 t[]; 定义边结构体数组 E[]; for i=0 to g.n for j=0 to g.n 对图中的边赋值;//起始顶点,终止顶点,权值 采用堆排序对E数组按权值递增排序; 初始化并查集; k=1;//表示当前构造生成树的第几条边 j=0;//E边的下标 while k<g.n { 取一条边的两个顶点赋予u1,v1; 得到两个顶点所属的集合编号于sn1,sn2; if sn1不等于sn2 { 生成边数k++; 将u1和v1两个顶点合并; } j++; } }
-
普里姆(Prim)算法&克鲁斯卡尔(Kruskal)算法
·当一个图有多个最小生成树时,两个算法的结果未必是相同的。
·Prim算法适用于稠密图,Kruskal算法适用于稀疏图
-
应用
-
公路村村通
·伪代码:
nt prim(MG g, int& count) { 定义数组lowcost[]; 定义数组closest[]; 定义总费用sumCost; for i=0 to g.n//赋初值 让lowcost[i]=矩阵中第一行的权值//从1开始生成 closest[i]=1; end for for i=1 to g.n min赋值极致大 for j=1 to g.n 选出最小的成本赋值至min; 将min累加至sumCount 改变最小成本所在下标在lowcost[]中的值=0 for j=1 to g.n 将lowcost[]和closest[]的值进行调整修改; end for return sumCost }
-
最短路径
-
从一个顶点到其余各顶点的最短路径
-
Dijkstra算法
·主要思想:
·图存储结构:邻接矩阵(需要频繁地取一条条边及其权值)
·数组dist[]:存放最短路径长度,数组path[]:最短路径序列的前一顶点的序号
·特点
1、一个顶点一旦加入中,最短路径长度不再改变
2、不适合含有负权值的带权图求单源最短路径
·伪代码:
void Dijkstra(MatGraph g,int v)
{
初始化dist数组、path数组、s数组;
遍历图中所有节点;
for i=0 to g.n
if s[i]!=0
则dist数组找最短路径,顶点为u;
s[u]=1; //加入集合S,顶点已选
for i=0 to g.n
if s[i]!=0 && dist[i]>dist[u]+g.edges[u][i]
则修正dist[i]= dist[i]>dist[u]+g.edges[u][i];
path[i]=u;
}
-
每对顶点之间的最短路径
-
每次以一个顶点为源点,重复执行Dijkstra算法n次
-
Floyd算法
·主要思想:递推产生一个矩阵序列A0,A1……,Ak[i][j]代表i->j的路径上所经过的顶点编号不大于k的最短路径长度。
·二维数组A:存储各顶点最短路径长度 , 二维数组path[]:存储最短路径
·伪代码:
void Floyd(MatGraph g)
{
定义二维数组A[][],path[][];
定义i,j,k;
for i=0 to g.n
for j=0 to g.n
A[i][j]赋值;
if i到j有边时
path[i][j]=i;
else //若i到j无边
path[i][j]=-1;
for k=0 to g.n
for i=0 to g.n
for j=0 to g.n
if A[i][j]>A[i][k]+A[k][j]
修改最短路径长度和最短路径;
}
-
应用
-
旅游规划
·主要伪代码:
void Dijkstra(MG g, int departure, int destination) { 定义总路径长度,总收费金额; 定义结构体数组d; 定义数组path[],s[]; for i=0 to g.n 对结构体数组d进行赋值; 对path[],s[]数组赋初值; end for 将出发顶点放入集合中; for i=1 to g.n min值赋为极大; for j=0 to g.n 比较获取长度最小的下标值k,并改变min值 end for end for 将k顶点放入集合中; for j=0 to g.n if 顶点不在集合中 then if 路径长度相加是小于原本的路径长度 then 修改路径长度以及收费金额; if 路径长度相等 只需修改收费金额; end for 输出 目的地的路径长度和收费金额; }
-
拓扑排序
·定义:在一个有向无环图中找一个拓扑序列的过程
·图存储结构:邻接表
·在定义邻接表结构体时可以加入顶点入度情况,从而遍历邻接表时可以计算每个顶点的入度
·多个入度为0的结点,入栈,之后进行出栈访问
·选择一个拓扑结点后,将其入度-1就可以实现连接边删除
·伪代码:
void TopSort(AdjGraph *G) { 遍历邻接表 计算每个顶点的入度,存入头结点count成员; 遍历图顶点 if 顶点入度为0 入栈st; while(栈不空) 出栈节点v,访问; 遍历v的所有邻接点 { 所有邻接点的入度-1 if 邻接点入度为0 入栈st; } }
-
关键路径
·定义:在AOE网中,从源点到汇点的所有路径中具有最大路径长度的路径
·事件的最早开始时间
·事件的最迟开始时间
1.2.谈谈你对图的认识及学习体会。
图的认识:
这一章的内容
1、对图相关概念的一个理解,图,有向图/无向图,度/入度/出度,完全图,子图,连通图……
2、了解图的存储结构,邻接表/邻接矩阵
3、图的基本运算,创建/销毁/输出……
4、图的遍历,DFS/BFS
5、生成树与最小生成树,Prim算法/Kruskal算法
6、最短路径,Dijkstra算法/Floyd算法
7、拓扑排序
8、关键路径
学习体会:
总结下来,这一章有四个算法但在学习的时候我总觉得有十个算法围绕着我。。。有点难...有些东西是通过我这次的博客总结后重新认识和了解的。这一章节的pta,有一丢丢困难,困难也是疫情在家上网课懈怠了的原因。这一章节需要了解算法的内容需要,看到题目的时候才能合理的运用到他们身上,不然可能在创建图时是要邻接表还是邻接矩阵都在发愁……果然学习内容是需要总结,总结下来,头脑里面就会有一个框架也就不会那么乱。
2.阅读代码(0--5分)
2.1 判断二分图
题目:
代码:
2.1.1 设计思路
时间复杂度:O(n^3)
空间复杂度:O(n^2)
2.1.2 伪代码
枚举color中元素 unknown 0,white 1,black 2;
bool isBipartite(int **graph,int graphSize,int *graphColSize)
{
为pVisited申请空间;
初始化pVisited中元素都为0;
为pQueue申请空间;
定义 front,rear,curr,cow,col,neighborColor;
for row=0 to graphSize-1
{
if pVisited[row]!=0 //还未访问过
continue;
把row放入队列pQueue中;
将pVisited[row]数值更改为1;
while 队列不为空
{
将队头元素赋予curr;
若pVisited[curr]为白,则改成黑色,若为黑,改为白色并赋予neighborColor;
for col=0 to graphColSize[curr]-1
if graph[curr][col]未被访问
将neighborColor赋予pVisited[graph[curr][col]];
graph[curr][col]入队;
if pVisited[graph[curr][col]] 与 neighborColor不同
return false;
}
}
}
2.1.3分析该题目解题优势及难点。
解题优势:剖析本质为图染色问题,分别利用队列完成深度优先搜索,存储下一个要访问结点的顺序,先对每个未着色邻接点着色放入队列中。使用哈希数组记录每个结点的颜色,在搜索结点时如果存在当前点和邻接点颜色相反,则着色失败。
难点:在于对这道题目的理解,如果没有较好的理解,也不会想到用图染色问题进行解决。还有是对哈希数组的利用,也很独到。
2.2 阈值距离内邻居最少的城市
题目:
代码:
2.2.1 设计思路
2.2.2 伪代码
int findTheCity(int n, vector <vector<int>> &edges, int distanceThreshold)
{
定义二维数组D,并初始化为INT_MAX(无穷);
根据题目所给edges[][]初始化D[][];
for k=0 to n
{
for i=0 to n
{
for j=0 to n
{
if i等于j 或 矩阵中数值为INT_MAX
continue;//防止两个INT_MAX相加溢出;
D[i][j]等于(D[i][k]+D[k][j],D[i][j])中较小的那一个;
}
}
}
for i=0 to n
{
for j=0 to n
{
if i不等于j且路程小于distanceThreshold
cnt++;
}
if cnt小于等于minNum
minNum=cnt;
编号为i;
}
return 编号i;
}
2.2.3 运行结果
2.2.4分析该题目解题优势及难点。
解题优势:利用Floyd算法进行解题,最后再从小到大进行判断邻居最少的城市。就可以取较大编号邻居最少的城市了。
难点:题目解读很重要,该代码利用邻接矩阵存图减少算法难度,利用Floyd算法计算便于查找邻居最少城市。
2.3 节点间通路
题目:
代码:
2.3.1 设计思路
2.3.2 伪代码
bool bfs(int n, vector<vector<int>>& edge, int start, int target)
{
定义队列que;
定义标记访问数组vis,初始化false;
start入队;
更改vis[start]为true;
while 队列不为空
{
取队头元素cur;
删去队头;
for i=0 to edge[cur].size()
if edge[cur][i] 未被访问过
if edge[cur][i] 为目标终点
return true;
edge[cur][i]入队;
更改vis[edge[cur][i]] = true;
}
return false;
}
2.3.3 运行结果
2.3.4分析该题目解题优势及难点。
解题优势:利用队列,从0的链表访问能到达的节点入队。也利用了数组vis[],对已经入列的节点不再进行处理。
难点:对于这个广度遍历的应用吧,其实这道题DFS和BFS都可以进行解题。