DS博客作业04--图
0.PTA得分截图
1.本周学习总结(0-5分)
1.1 总结图内容
基本概念特点
-
图分为有向图和无向图
-
若一个图中有n个顶点和e条边,每个顶点的度为di(0≤i≤n-1),则有:
图的存储结构
邻接矩阵
-
顶点表存放顶点信息
-
邻接矩阵存放边的信息
-
特点:
邻接矩阵取边直接,因此对顶点出入度的情况也较易掌握。
邻接矩阵适合用于稠密图
结构定义
#define MAXV <最大顶点个数>
typedef struct
{ int no; //顶点编号
InfoType info; //顶点其他信息
} VertexType;
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵,存放边的信息
int n,e; //顶点数,边数
VertexType vexs[MAXV]; //顶点表,存放顶点信息
} MatGraph;
MatGraph g;//声明邻接矩阵存储的图
此为最基本的邻接矩阵结构定义,该邻接矩阵定义简单栈区申请,当顶点数较大时,我们需要到堆区申请。
- 注意二级指针的写法如下:
为二级指针申请完地址后,要利用循环依次为每个一级指针申请地址
以上邻接矩阵的基本结构定义中,顶点的相关信息有另外定义结构体存储信息,我们可以根据需求自行定义。
*如通常如果顶点只有编号区分,甚至直接以数字顺序为编号时,而无其他信息时,我们也可以选择像上述邻接矩阵一样,直接用一维数组表示顶点。
而经常,当边的信息不止包含长度,如带权值,带费用等其他信息时,此时我们也需要,另外定义一个结构体,来专门存储边的相关信息
- 对边另定义结构体,且无需另外的顶点信息写法参考如下:
typedef struct
{
int length;
int cost;
}edge;
typedef struct
{
int edge[MAXV][MAXV];
int n, e;
}MGraph;
- 将上述注意点结合起来,对边另定义结构体,且动态申请的方法这样写:
邻接表
基本概念特点
-
邻接表是一种顺序分配和链式分配相结合的存储结构。
(即利用数组和链表相结合) -
用数组存储每条链头结点(头结点即每个顶点),链结点存储的是边的内容(存储该顶点的邻接表或出边)。
头结点结构定义,需存储顶点信息即其后继边,保存后继关系。
链表结点保存边内容,包含该边所连终点编号,以及链的后继关系(但要注意,并非是边与边的后继关系。实际上是头结点顶点与边终点的后继关系)
利用一维数组保存头结点,称为邻接表
结构体定义
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; //图中顶点数n和边数e
} AdjGraph;
AdjGraph *G;//声明一个邻接表存储的图G
- 结合图像理解:
蓝框部分的头结点紧密相连,就如数组地址相邻
而橙框部分,则是链式结构,指针相连。
构造图
构造邻接矩阵
void CreateMGraph(MGraph& g, int n, int e)//建无向图
{
int a,b;
for (int i = 1; i <= e; i++)
{
cin >> a >> b;
g.edges[a][b] = 1;
g.edges[b][a] = 1;
}
g.e = e; g.n = n;
}
-
由代码可知
-
此邻接矩阵沿着对角线对称,因此为无向图
-
若建有向图则直接:
g.edges[a][b] = 1;
建邻接表
void CreateAdj(AdjGraph*& G, int n, int e)//创建图邻接表
{
int i, a, b;
G = new AdjGraph;
ArcNode* p;
for (i = 1; i <= n; i++)//初始化头结点都指向空
{
G->adjlist[i].firstarc = NULL;
G->adjlist[i].data = i;
}
for (i = 1; i <= e; i++)//根据输入的边
{
cin >> a >> b;
p = new ArcNode;
p->adjvex = b;
p->nextarc = G->adjlist[a].firstarc;//头插入邻接表
G->adjlist[a].firstarc = p;
p = new ArcNode;//无向图则需要双向指向
p->adjvex = a;
p->nextarc = G->adjlist[b].firstarc;
G->adjlist[b].firstarc = p;
}
G->n = n; G->e = e;
}
- 同样此为无向图,若建有向,则如注释忽略
邻接表与邻接矩阵的相互转换
图的遍历
-
概念:从图中的某个顶点开始,以某种搜索方式,对图中的其他顶点仅访问一次
-
无论是广度优先遍历还是深度优先遍历,通常使用数组visited[]来标记顶点是否被访问,通常定义成全局变量。
深度优先遍历(DFS)
-
类似迷宫问题:利用栈或者递归
-
这里我们使用递归的方法对其邻接点不断遍历。
-
遍历过程思路
邻接表的深度遍历
邻接表的深度遍历
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
ArcNode* p;
visited[v] = 1;
cout << v;
p = G->adjlist[v].firstarc;
while (p)
{
if (visited[p->adjvex] == 0)
{
DFS(G, p->adjvex);
}
p = p->nextarc;
}
}
邻接矩阵的深度遍历
邻接矩阵的深度遍历
void DFS(MGraph g, int v)//深度遍历
{
int i;
visited[v] = 1; cout << v;
for (i = 1; i <= g.n; i++)//循环查找是否有边
{
if (visited[i]==0&&g.edges[v][i] == 1)
{
DFS(g, i);
}
}
}
- 注意由于邻接矩阵的写法:
无法知道那些边与v结点是直接相连,因此只能通过循环找点判断边是否存在,并且判断点是否被访问过
由遍历思路我们可知,我们是顺着连通的顶点不断遍历下去,因此**一旦图为非连通图,我们调用函数,就只能遍历该起点顶点所在的部分连通图**
因此特别的,考虑到图可能为非连通图的情况,我们做出以下改善:
非连通图调用过一次函数后,仍有其他点未被遍历。我们通常在**主函数中循环判断各顶点是否被遍历过,再决定是否再次调用深度遍历函数**
- 注意判断在主函数中做,DFS函数仅做连通图的深度遍历。
广度优先遍历(BFS)
-
同样类似迷宫问题,利用队列保存每层结点。
-
广度优先遍历思路
邻接表的广度优先遍历
邻接表的广度优先遍历
void BFS(AdjGraph* G, int v) //v节点开始广度遍历
{
queue<VNode>Q;
ArcNode* p; Vnode front;
visited[v]=1;
Q.push(G->adjlist[v]);
while (!Q.empty())
{
front = Q.front();
Q.pop();
cout << front.data ;
p = front.firstarc;
while (p!=NULL)
{
if (visited[p->adjvex] == 0)
{
Q.push(G->adjlist[p->adjvex]);
visited[p->adjvex] = 1;
}
p = p->nextarc;
}
}
}
邻接矩阵的广度优先遍历
邻接矩阵的广度优先遍历
void BFS(MGraph g, int v)//广度遍历
{
int Q[MAXV];
int f=0, r=0;//队的首尾指针
visited[v] = 1;
Q[++r] = v;//入队
while (f != r)//队不空
{
f++;
cout << Q[f];//出队
for (int i= 1; i <= g.n; i++)//同样需要循环每个顶点判断是否有边
{
if (visited[i]==0&&g.edges[Q[f]][i] == 1)//未被广度遍历过
{
visited[i] =1 ;
Q[++r] = i;//入队
}
}
}
}
- 注意邻接矩阵这里的队列,我们用了自己写的简易顺序队列:
首尾指针是: int f=0, r=0;
- 同样的与邻接表相比,无法直接知道于其有边的点,因此也是需要通过**循环查找每个点判断是否有边存在**
DFS,BFS遍历图判断图是否连通
!!多次调用函数遍历的,记得要先将visited[]数组初始化
DFS图不连通的问题已经在上面阐述过了:
我们只需在主函数中,第一次调用DFS函数后,若有出现仍未被遍历的点,则说明是非连通图。
并且只要继续调用DFS,就可做到将非连通图中的各个连通部分都分别遍历完毕。
关于BFS图是否连通问题:
判断BFS图是否连通的办法一样,一次执行后也是利用循环判断点是否全都被遍历。
但由于BFS函数并不是通DFS一样递归写法,因此也用循环判断点是否被遍历这个写法,
1.可以在BFS函数中,将所有内容都嵌套在循环内,主函数一次调用。
2.当然也可以还是一样,循环在主函数中写。
最小生成树
基本概念
-
生成树:一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边。不能回路。
-
且生成树并不唯一
利用深度优先遍历,我们可以得到深度优先生成树
利用广度优先遍历,我们可以得到广度优先生成树
图示:
- 最小生成树:对于带权值的图,其中权值之和最小的生成树称为图的最小生成树。
非连通图和最小生成树(容易忽略)
- 生成树的过程一定是在一个连通图才能完成,因此非连通图必须多次调用最小生成树函数,为其多个不同连通部分生成树
- 所有连通分量的生成树组成非连通图的生成森林。
普利姆算法(Prim)
设置2个辅助数组:
1.closest[i]:最小生成树的边依附在U中顶点编号。
2.lowcost[i]表示顶点i(i ∈ V-U)到U中顶点的边权重,取最小权重的顶点k加入U。
并规定lowcost[k]=0表示这个顶点在U中
每次选出顶点k后,要队lowcost[]和closest[]数组进行修正
伪代码设计过程
具体代码
void Prim(MGraph g, int v)
{
int lowcost[MAXV], closest[MAXV];//lowcost表示到该点最短距离,closest
int i, j, k, min ;// k记录最近顶点的编号
lowcost[1] = 1;//起点最近点为它本身
for (i = 1; i <= g.n; i++) //顶点从1开始,给lowcost[]和closest[]置初值
{
lowcost[i] = g.edges[v][i];//建图时未有直接相连的边,lowcost=edges为无穷大INF
closest[i] = v;
}
for (i = 1; i < g.n; i++) //找(n-1)次剩下的顶点
{
min = INF;
for (j = 1; j <= g.n; j++) // 在(V-U)中找出离U最近的顶点k
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j]; k = j; //
}
lowcost[k] = 0; //遍历所有点后找到距离最近点,标记k已经加入
for (j = 1; j <= g.n; j++) //修改数组lowcost和closest
if (lowcost[j] != 0 && g.edges[k][j] < lowcost[j])
{
lowcost[j] = g.edges[k][j];
closest[j] = k;
}
}
}
应用-公路村村通题目分析
1. 本题有所给数据造成不畅通情况,其实就是图不连通情况。因为选过的点我们用lowcost[]=0来表示,因此可通过遍历lowcost数组,若每个点都被选入即畅通,反之则不畅通。
2. 若不畅通根据题目要求直接输出返回,若畅通,此时我们就根据closest数组计算最低预算。因为通过closest[]数组可以知道包含全点的最短路径,closrst[]所表示的就应该是该点的前驱点
克鲁斯卡尔算法(kruscal)
- 按权值的递增次序选择合适的边来构造最小生成树的方法
- 克鲁斯卡尔算法过程:
(1)置U的初值等于V(即包含有G中的全部顶点),TE(最小生成树的边集)的初值为空集(即图T中每一个顶点都构成一个连通分量)。
(2)将图G中的边按权值从小到大的顺序依次选取:
若选取的边未使生成树T形成回路,则加入TE;
否则舍弃,直到TE中包含(n-1)条边为止。
-由于克鲁斯卡尔算法过程是对边的权重排序选边,因此我们需要另外一个存储结构来存储边的权重信息
typedef struct
{ int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
} Edge;
Edge E[MAXV];
- 我们利用树的并查集来解决判断是否有环路问题
具体代码实现
void Kruskal(AdjGraph *g)
{ int i,j,k,u1,v1,sn1,sn2;
UFSTree t[MAXSize];//并查集,树结构
ArcNode *p;
Edge E[MAXSize];
k=1; //e数组的下标从1开始计
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;
}
HeapSort(E,g.e); //采用堆排序对E数组按权值递增排序
MAKE_SET(t,g.n); //初始化并查集树t
k=1; //k表示当前构造生成树的第几条边,初值为1
j=1; //E中边的下标,初值为1
while (k<g.n) //生成的边数为n-1
{ u1=E[j].u;
v1=E[j].v; //取一条边的头尾顶点编号u1和v2
sn1=FIND_SET(t,u1);
sn2=FIND_SET(t,v1); //分别得到两个顶点所属的集合编号
if (sn1!=sn2) //两顶点属不同集合
{ printf(" (%d,%d):%d\n",u1,v1,E[j].w);
k++; //生成边数增1
UNION(t,u1,v1);//将u1和v1两个顶点合并
}
j++; //扫描下一条边
}
}
最短路径
-
最短路径与最小生成树不:
最小生成树需要包含所有顶点, 而最短路径只考虑路径最短,
Dijkstra算法
1.从T中选取一个其距离值为最小的顶点W, 加入S
2.S中加入顶点w后,对T中顶点的距离值进行修改:
若加进W作中间顶点,从V0到Vj的距离值比不加W的路径要短,则修改此距离值;
3.重复上述步骤1,直到S中包含所有顶点,即S=V为止。
伪代码设计思路
这里我们同Prim算法建最小生成树的过程来比较看看,方便记忆:
1.Prim建成最小生成树和Dijkstra求最短路径,这两种方法的大致过程框架看起来似乎很相似。似乎都是选出最近临近点,记录选入,然后修正数组值。
2.求最短路径多了s[]数组来记录已选入的点,而生成最小树时直接由lowcost[]=0来表示已选入的点。这是由于dist[]数组最终获得的数据是源点到各点的最短距离,记录过程中不能轻易改变,而生成最小树lowcost[],是一边走一边选并且修正未选入点的距离值。因此已选入点的lowcost[]信息就可以置空来表示,而求最短路径时只能另辟数组s[]来存。
3.关于修正过程都是注意选入新点后最短路径是否发生了改变。最小生成树时,是需要边选点k边修正两个顶点集合间的最短路径,修正时只要注意选入该点后两集合间的最短路径是否改变。
而相比最短路径,最短路径中不一定包含所有的点,因此即使选入点u后,是将带选入点u的路径与当前最短路径相比较取较小。
具体代码
void Dijkstra(MGraph g, int v)//源点v到其他顶点最短路径
{
int dist[MAXV], path[MAXV],s[MAXV];
int mindistance,u;//u为每次所选最短路径点
for (int i = 0; i < g.n; i++)//初始化各数组
{
s[i] = 0;//初始已选入点置空
dist[i] = g.edges[v][i];//初始化最短路径
if (dist[i] < INF) path[i] = v;
else path[i] = -1;//即无直接到源点V的边,因此初始化为-1
}
s[v] = 1;//源点入表示已选
for (int j = 0; j < g.n; j++)//要将所有点都选入需循环n-1次
{
mindistance = INF;//每次选之前重置最短路径
for (int i = 1; i < g.n; i++)//每次都遍历源点以外其他点来选入点
{
if (s[i] == 0 && dist[i] < mindistance)//在未选的点中找到最短路径
{
mindistance = dist[i];
u = i;//u记录选入点
}
}
s[u] = 1;//最后记录的u才为最后选入点
for (int i = 1; i < g.n; i++)//修正数组值
{
if (s[i] == 0)//!!仅需修改未被选入点的,已选入的既定
{
if (g.edges[u][i] < INF && dist[u] + g.edges[u][i] < dist[i])//先判断选入点到与该点存在时再比较判断
{
dist[i] = dist[u] + g.edges[u][i];
path[i] = u;
}
}
}
}
Dispath(dist, path, s, g.n, v);
}
Dijkstra算法特点:
1.不适用带负权值的带权图求单源最短路径。
2. 不适用求最长路径长度:
最短路径长度是递增
顶点u加入S后,不会再修改源点v到u的最短路径长度
(按Dijkstra算法,找第一个距离源点S最远的点A,这个距离在以后就不会改变。但A与S的最远距离一般不是直连。)
Floyd算法
- 该算法思路较简单:
就是不断将每个顶点都加入的过程中,同时不断更新最短路径矩阵**
算法思路
- 有向图G=(V,E)采用邻接矩阵存储
- 二维数组A用于存放当前顶点之间的最短路径长度,分量A[i][j]表示当前顶点i到顶点j的最短路径长度。
- 递推产生一个矩阵序列A0,A1,…,Ak,…,An-1
Ak+1[i][j]表示从顶点i到顶点j的路径上所经过的顶点编号k+1的最短路径长度。
具体代码
void Floyd(MatGraph g) //求每对顶点之间的最短路径
{
int A[MAXVEX][MAXVEX]; //建立A数组
int path[MAXVEX][MAXVEX]; //建立path数组
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.edges[i][j] < INF)
path[i][j] = i; //i和j顶点之间有一条边时
else //i和j顶点之间没有一条边时
path[i][j] = -1;
}
for (k = 0; k < g.n; k++) //求Ak[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; //修改经过顶点k
}
}
}
拓扑排序及关键路径
拓扑排序基本概念特点
-
拓扑序列:在一个有向图中找一个拓扑序列的过程称为拓扑排序。
-
序列必须满足条件:
每个顶点出现且只出现一次。
若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。 -
拓扑排序:在一个有向无环图中找一个拓扑序列的过程称为拓扑排序。
注意必须是有向无环图
排序过程
- 1.从有向图中选取一个没有前驱的顶点,并输出之;
- 2.从有向图中删去此顶点以及所有以它为尾的弧;
- 3.重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。
根据此排序规则,我们对下图进行拓扑排序
得到的拓扑序列为:C1--C2--C3--C4--C5--C7--C9--C10--C11--C6--C12--C8
或: C9--C10--C11--C6--C1--C12--C4--C2--C3--C5--C7--C8
由此可知,拓扑序列并未是唯一的
伪代码
- 需注意的是,我们不仅可以栈结构来保存要删除的前驱点,我们**同样可以使用队列**
- 但正是**由于栈和队列出入方式的不同**,所以同一个有向图,我们得到的可能就是不同的拓扑序列
具体代码
void TopSort(AdjGraph* G)//邻接表拓扑排序
{
ArcNode* p;
int stack[MAXV], top=-1;//顺序栈结构
int visitedcout = 0;//记录已得到的拓扑序列长度
int sequence[MAXV];//用于保存拓扑序列
for (int i = 0; i < G->n; i++)
{
G->adjlist[i].count = 0;
}
for (int i = 0; i < G->n; i++)//遍历每条链,记录每个节点的入度
{
p = G->adjlist[i].firstarc;
while (p)
{
G->adjlist[p->adjvex].count++;
p = p->nextarc;
}
}
for (int i = 0; i < G->n; i++)//先遍历图顶点,找出入度为0的点入栈
{
if (G->adjlist[i].count == 0)
{
top++; stack[top] = i;
}
}
int i = 0;
while (top!=-1)//接下来通过不断出栈过程中同时判断是否有点要入栈
{
sequence[i] = stack[top]; visitedcout++;//保存拓扑序列,并且记录已遍历点
p = G->adjlist[stack[top]].firstarc;//则该点的后继点入度都要减一
top--;//出栈
while (p)
{
G->adjlist[p->adjvex].count--;
if (G->adjlist[p->adjvex].count == 0)
{
top++; stack[top] = p->adjvex;
}
p = p->nextarc;
}
i++;
}
if (visitedcout == G->n)//则无环路得到拓扑序列,
{
cout << sequence[0];
for (int i = 1; i < G->n; i++)
{
cout << " " << sequence[i];
}
}
else cout << "error!";
}
关于如何判断图是否存在有环
- 举最简单的例子,由上图可知,在一个环路中,我们是没办法找到入度为0的顶点。
同样的全局图来说,即使利用拓扑排序可以得到一定的拓扑序列,但只要存在环路,就不可能得到完整的拓扑序列
- 因此判断是否存在有环,只需记录一下得到的序列长度,与图的顶点数相比即可知,序列是否完整,是否就是拓扑序列
- 具体实现也已经在上述具体代码中体现
关键路径基本概念
- AOE-网(Activity ON Edge Network):
用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是带权的有向无环图
整个工程完成的时间为:从有向图的源点到汇点的最长路径。又叫关键路径
如何求关键事件和关键路径
求关键事件
事件v最早开始时间ve(v):v作为源点事件最早开始时间为0。
由于v为源点事件最早开始时间一定是前驱事件已完成。因此:
当v为初始源点时: ve(v)=0
其余:ve(v)=MAX{ve(x)+a,ve(y)+b,ve(z)+c}
事件v的最迟开始时间vl(v):定义在不影响整个工程进度的前提下,事件v必须发生的时间称为v的最迟开始时间
由于最迟时间要保证后继事件能完成,因此取最小
当v为终点时:vl(v)=ve(v)
其他 vl(v)=MIN{vl(x)-a,vl(y)-b,vl(z)-c}
- 关键路径点:ve=vl
注意:计算:ve(i)最早开始和vl(i)最迟开始必须在拓扑有序和逆拓扑有序计算。
活动:边的最早开始时间和最迟开始时间
1.活动a(边)的最早开始时间e(a)指该活动起点x事件的最早开始时间e(a)=ve(x)
2.活动a的最迟开始时间l(a)指该活动终点y事件的最迟开始时间与该活动所需时间之差
l(a)=vl(y)-c
- 关键活动:d(a)=l(a)-e(a),若d(a)为0,则称活动a为关键活动。
求关键路径
1.2.谈谈你对图的认识及学习体会。
-
图的应用感觉比之前学的结构更加广了,实际应用在地图啊什么的比较多,好比村村通应用最小生成树问题,最短路径问题,还有关键路径。
-
练习编程的题目,感觉其实思路都是比较直接,实质明显,最小生成树问题啊,最短路径问题。
后面做阅读代码部分的时候,有一些题目的时候都是没什么思路,看了解法,才发现其实是所学知识的变化、延展。比如下面阅读代码的无向图的“拓扑”,所以阅读时候也对思路进行认真分析 -
还有一个感觉就是图部分的编程对前面所学知识应用的更多,结合的内容更多,对编程能力更有考验。前面学习完栈和队列后对STL库的应用就会比较多,然后在这次复习题的代码中也会经常发现,
对于一些简单的变量类型和简单的处理,有的时候我们自己写顺序栈,顺序队的其实也很简单。还有啊比如Kruscal算法的代码利用并查集,这就是并查集的应用结合。 -
学习体会:一开始学图,对于图的存储结构,邻接矩阵和邻接表,相比起树的存储结构,感觉其实更好理解。还有图的遍历,深度优先,广度优先遍历,在前面树已学的基础下,对这部分内容感觉更加得心应 手。但是后面关于最小生成树,最短路径,以及拓扑序列集关键路径,这三部分内容,自己预习的时候就是明显感觉到,有点难理解,然后经过课上老师讲解后理解了算法的执行过程后,最后主要的压力还是得要自己去编写,去细细品代码的具体细节。
2.阅读代码
2.1 题目及解题代码
题目:最小高度树
解题代码
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
vector<int> indegree(n);//入度数组
vector<vector<int>> graph(n);//图形表示
vector<int> result;
for (int i = 0; i < n; i++)
{
indegree[i] = 0;//初始化入度序列为0
graph.push_back(v);
}
for (int i = 0; i < edges.size(); i++)//构造图与入度数组:无向图,两个点都要处理
{
graph[edges[i][0]].push_back(edges[i][1]);
graph[edges[i][1]].push_back(edges[i][0]);
indegree[edges[i][0]]++;
indegree[edges[i][1]]++;
}
queue<int> myqueue;//装载入度为1的queue
for (int i = 0; i < n; i++)
{
if (indegree[i] == 1)
myqueue.push(i);
}
int cnt = myqueue.size();//!!令cnt等于myqueue.size(),一次性将入度为1的点全部删去。
while (n>2)
{
n -= cnt;//一次性将入度为一的点全部删去!!不能一个一个删!
while (cnt--)
{
int temp = myqueue.front();
myqueue.pop();
indegree[temp] = 0;
//更新temp的邻接点:若temp临接点的入度为1,则将其放入queue中。
for (int i = 0; i < graph[temp].size(); i++)
{
if (indegree[graph[temp][i]] != 0)
{
indegree[graph[temp][i]]--;
if (indegree[graph[temp][i]] == 1)//放在这里做!只判断邻接点。
myqueue.push(graph[temp][i]);
}
}
}
cnt = myqueue.size();
}
while (!myqueue.empty())
{
result.push_back(myqueue.front());
myqueue.pop();
}
return result;
}
};
2.1.1 该题的设计思路
1.删除入度为1的点
有向图的拓扑序列时,我们删除的是前驱为0的节点。
而对于无向图,由于无向图是双向,入度为1即可说明该点就是**图的边缘点**
2.关于一次性删除的问题
当我们按照有向图的拓扑序列一个个删除点并同时判断是否入队时
而当我们采用我们一次性删除的思路时;
解释说明:
- 由题意可知,根据几何想象,这其实就是一个图不断缩小的过程,即**不断地把边缘顶点删除,最终得到中间的根节点的过程**
- 对于该无向图来理解,当我们不断删减边缘点时,也在不断改变图,改变顶点入度,所以**每删除一个点,图的中心节点也可能发生了变化**
- 因此控制一次性删除,我们其实可以理解成,**一圈圈删减,这才叫缩小,所有方向的边缘点都同时删除,才能达到中心节点不偏移**
2.1.2 该题的伪代码
2.1.3 运行结果
示例1:
示例2:
2.1.4分析该题目解题优势及难点。
-
题目难点:其实一开始看该题目的时候,毫无思路,感觉要获得图中心节点,用遍历什么的都很难想到什么思路,所以我觉得该题难点就是思路难。
-
解题优势:正是因为对题目思路毫无头绪,所以对该题的解题思路感到很新奇-拓扑序列的变式
从有向无环图的拓扑序列,延展到无向图。
虽然主要目的并不是为了得到无向图的该序列,但也是按照该思路的过程,不断删除点缩图。 -
该思路无向图的拓扑,将各顶点都遍历了一遍,所以时间复杂度应为O(n).
-
但是同样无向图的该拓扑方式前提同样也是无环路,而题目给的样例也符合该条件:本题名为最小高度树
且
2.2题目及解题代码
题目:网络延迟时间
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
const int INF = 0x3f3f3f3f;
vector<int> dist(N+1, INF); // 保存到起点的距离
vector<bool> st(N+1, false); // 是否最短
typedef pair<int, int> PII;
unordered_map<int, vector<PII>> edges; // 邻接表
queue<int> q;
q.push(K);
dist[K] = 0;
st[K] = true; // 是否在队列中
for (auto &t: times){
edges[t[0]].push_back({t[1], t[2]});
}
while (!q.empty()){ // 当没有点可以更新的时候,说明得到最短路
auto t = q.front();
q.pop();
st[t] = false;
for (auto &e: edges[t]){ // 更新队列中的点出发的 所有边
int v = e.first, w = e.second;
if (dist[v] > dist[t] + w){
dist[v] = dist[t] + w;
if (!st[v]){
q.push(v);
st[v] = true;
}
}
}
}
int ans = *max_element(dist.begin()+1, dist.end());
return ans == INF ? -1: ans;
}
};
2.2.1设计思路
-
这道题其实就是求出点到各顶点的最短路径,求出点K到其他各点的最短距离后,再找出其中的最大距离
-
在题解中,关于最短路径解法有DisjKstra,Floyd,SPFA。由此对SPFA该种算法进行学习:
SPFA算法实现方法:
- 建立一个队列,初始时队列里只有起始点
- 在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。
- 然后执行松弛操作(更新数据),用队列里有的点去刷新起始点到所有点的最短路,**如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。**
2.2.2伪代码
SPFA算法
2.2.3运行结果
2.2.4题目难点及解题优势
-
题目难点:一个就是对题目的理解。
网络信号需要多久才能使所有节点都收到信息,到达每一个顶点时间应当都是以到该点的最短路径来记
而使所有节点都能接收到信息,则是所有顶点中,最晚收到,即该点的最短路径最长的。 -
算法优点:
通常可用于求含负权边的单源最短路径(DisjKstra权重是一定不能为负的)
以及判负权环(如果一个点进入队列达到n次,则表明图中存在负环,没有最短路径。) -
SPFA算法期望的时间复杂度:O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。
2.3题目及解题代码
题目
解题代码
class Solution {
public:
int findTheCity(int n, vector <vector<int>> &edges, int distanceThreshold) {
// 定义二维D向量,并初始化各个城市间距离为INT_MAX(无穷)
vector <vector<int>> D(n, vector<int>(n, INT_MAX));
// 根据edges[][]初始化D[][]
for (auto &e : edges) {
// 无向图两个城市间的两个方向距离相同
D[e[0]][e[1]] = e[2];
D[e[1]][e[0]] = e[2];
}
// Floyd算法
for (int k = 0; k < n; k++) {
// n个顶点依次作为插入点
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j || D[i][k] == INT_MAX || D[k][j] == INT_MAX) {
// 这些情况都不符合下一行的if条件,
// 单独拿出来只是为了防止两个INT_MAX相加导致溢出
continue;
}
D[i][j] = min(D[i][k] + D[k][j], D[i][j]);
}
}
}
// 选择出能到达其它城市最少的城市ret
int ret;
int minNum = INT_MAX;
for (int i = 0; i < n; i++) {
int cnt = 0;
for (int j = 0; j < n; j++) {
if (i != j && D[i][j] <= distanceThreshold) {
cnt++;
}
}
if (cnt <= minNum) {
minNum = cnt;
ret = i;
}
}
return ret;
}
};
2.3.1设计思路
-
该题其实还是对最短路径的应用,之所以采用floyd算法,是因为将各个城市间最短路径存在矩阵中,这样存储数据更加方便比较
-
之后利用矩阵中的数据进行比较,求出各个城市的邻城市即可,就可以找到最少的城市
2.3.2伪代码
2.3.3运行结果
示例1:
示例2:
2.3.4题目难点及解题优势
-
该题解题思路较简单,通过Floyd算法就可得到各个城市间的最短距离,统计一下即可得到各个城市在规定范围的城市数。
-
Floyd算法,一层循环控制每个点的选入,另外两层循环控制矩阵的更新,因此三层循环时间复杂度O(n^3)
-
不过忽然想到,要获取顶点在一定距离范围内的邻居点,其实就有点像PTA上的六度空间那道题目。
六度空间是求各点距离不超过6的点所占总点百分比。我当时写的是用广度遍历。
不过区别貌似在于,六度空间顶点间的边不带距离值,所以用广度也不算太麻烦,所以如果用Floyd算法解六度空间应该也是完全可以的,但边带值的这题使用广度遍历可能一些操作上还是有点麻烦