DS博客作业04-图
0.PTA得分截图
1.本周学习总结(0-5分)
1.1 总结图内容
图存储结构
图
之前的学习中,我们将图归为非线性结构中,同时,图的非线性体现于它的多对多关系
如图,图中每一个圆对应的圆都不止一个,这就是多对多关系
我们在之前学习树结构时,我们可以将它看做一种特殊的线性结构,它的一对一是特殊的一对一,而对于树,我们也可以称之为特殊的图结构,所以我们可以知道,图结构是最普遍的一种数据结构。
有向图,无向图:
我们对于图的描述主要在于对其顶点与顶点之间的描述,如下图
它的顶点集为V={A,B,C,D,E}
边为:E={<A,B>,<A,C>,<A,D>,<B,C>,<B,D>,<B,E>,<C,E>,<D,E>}
图中所示为无向图,即边没有方向,所以<A,B>也可以表示为<B,A>,所以此时边表示应为(A,B),
e而如果为有向图则边存在方向,如图
则边表示应变为:E={<A,B>,<A,C>,<A,D>,<C,B>,<B,D>,<B,E>,<E,C>,<E,D>}
即此时,边需要注意方向
图的存储结构:
邻接矩阵:使用邻接矩阵作为存储图的方法时,我们需要存储的主要有
1.顶点信息
2.边或弧信息
此时我们采用数组来记录这些内容,一维数组记录顶点内容,二维数组记录边内容
结构体定义如下
typedef struct
{
int no;//顶点编号
Infotype info;
}VertexType;
typedef struct
{
int edges[][];
int n,e;//顶点个数,边数
VertexType vex[][];//存放顶点信息
}MatGraph
对于无向图
邻接矩阵表示为
而对于有向图,邻接矩阵为:
对于邻接矩阵我们很容易求得每个顶点的度,每一列的数表示入度,每一行的数表示出度;
邻接表:其实领接表就是对每个顶点建一条单链表,以此将所有邻接点连起来
如图
这就是图的邻接表
结构体定义
typedef struct
{
Vertex data;
AreNode *firstarc;
}VNode;
typedef struct ANode
{
int adjvex;
struct ANode *nextarc;'
InfoType info;
}ArcNode;
typedef struct
{
VNode adjlist[MAXV];
int n,e;
}AdjGraph;
创建图
我们在进行图的存储时有两种,所以我们建图也有两种方法
邻接矩阵建图
void CreateMGraph(MGraph& g, int n, int e)
{
int i, j,k;
int v1, v2;
for (i = 1; i <= n; i++)
{
for (j = 1; j <= n; j++)
{
g.edges[i][j] = 0;
g.edges[j][i] = 0;
}
}
for (k = 0; k <e; k++)
{
cin >> v1 >> v2;
g.edges[v1][v2] = 1;
g.edges[v2][v1] = 1;
}
g.n = n;
}
邻接矩阵建图的关键在于出度入度的计算
邻接表建图
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 = 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 = n;
}
领接表建图时我们要考虑一个问题,是有向图还是无向图,如果是有向图那我们建链时就要注意方向,而无向图则也可以说明链的方向是相互的,所以建链时需要将链指向相互相指;
图遍历及应用。包括DFS,BFS.如何判断图是否连通、如何查找图路径、如何找最短路径。
DFS遍历
深度优先遍历我们并不陌生,在之前学习树时我们就有用到,它是基于一个点不断向下深入去遍历,如图
对于这个有向图我们进行深度遍历后得出的顺序为:A B D E C 或A C D E B等
我们顺着A出发一个接一个向下找,直到不能往下继续走后回溯去找未找的点
DFS代码为
void DFS(ALGaph* 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;
}
}
我们进行深度遍历时有时遍历的未必是连通图,而从图中的任意一个顶点开始进行深度优先遍历搜索,如图中所有点都可以遍历到,即只有一个连通分量,就是连通图,无法遍历完毕则是非连通图
在对非连通图进行深度遍历时,我们可以借助数组将每个顶点的数组值置为0,如果之后未被访问则就将这个顶点变为初始顶点,故非连通图进行深度遍历主要就在于多次调用。
BFS
广度优先遍历,也就是对层进行遍历,在迷宫问题中他就是一个分散遍布遍历,在图中广度优先遍历就是对一个顶点的所有连接点进行遍历,还是上面那幅图,进行广度遍历的结果为:A B C D E
BFS代码
void BFS(ALGraph G, int v)
{
visit(G, v);
visited[v]=true; //设已访问标记
Q.push(v); //顶点v入队
while(!Q.empty())
{
v=Q.front();
Q.pop();
for(int w=FirstNeighbor(G, v); w>=0; w=NextNeighbor(G, v, w))
{ //检查v所以邻接点
if(!visited[w]) //w为v的尚未访问的邻接顶点
{
visit(G, w);
visited[w]=true; //设已访问标记
Q.push(w); //顶点w入队
}
}
}
}
广度优先遍历判断非连通图:从一点出发只要能遍历完所有结点则是连通图
广度优先遍历非连通图时调用BFS次数刚好等于连通分量的个数
void BFS1(AdjGraph *G)
{
int i;
for(i=0;i<G->n;i++)
{
if(visited[i]==0)
BFS(G,i)
}
}
判断无向图是否连通:对于上面提出的DFS,BFS算法,都是针对连通图的,同时判断是否连通也是基于有向图,而无向图的判断如下
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);
for(i=0;i<G->n;i++)
{
if(visited[i]==0)
{
flag=false;
break;
}
}
return flag;
}
前面迷宫问题我们知道可以通过深度优先遍历来找到一条路径,同样的这里我们也可以用深度优先遍历来找图的一条路径
void ExistPath(AGraph *G,int u,bool &has)
{
int w;
ArcNode* p;
visited[u]=1;
if(u==v)
{
has=true;
return;
}
p=G->adjlist[u].firstarc;
while(p!=NULL)
{
w=p->adjvex;
if(visited[w]==0)
ExistPath(G,w,v,has);
p=p->nextarc;
}
}
同样的我们知道,如果要寻找最短路径,那么就是我们的广度优先遍历
typdef struct
{
int data;
int parent;
}QUERE;
void ShordPath(AdjGraph *G,int u,int v)
{
qu[rear].data=u;
while(front!=rear)
{
front++;
w=qu[front].data;
if(w==v)
while()
{
rear++;
qu[rear].data=p->adjvex;
qu[rear].parent=front;
}
}
}
通过以上所有例子,我们可以知道:广度优先遍历找到的一定是最短路径,深度优先遍历则不一定,深度优先遍历能找到所有路径,广度难以实现
最小生成树相关算法及应用
生成树:
一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边。不存在回路
由深度遍历得到的生成树交深度优先生成树,由广度优先遍历得到的生成树叫广度优先生成树,根据不同方法遍历得到的生成树不一样,所以连通图的生成树不一定是唯一的
最小生成树:
对于生成树我们不同遍历有不同的生成树,而遍历情况中权值和最小的生成树称为图的最小生成树。
普里姆算法:
既然存在最小生成树,那么就有一个问题,我们如何去构建最小生成树?
我们知道,最小生成树的条件就是权值和最小,所以我们构建中以找权值最小边为目标,于是有了普里姆算法
普里姆算法实现过程
1.初始化U={v},选v到其他顶点的所有边为候选边
2.挑选所有侯选边中权值最小的边,并加入对应顶点
3.考察所有顶点j,修正侯选边
4.重复2,3
普里姆算法图片展示
普里姆算法中,我们需要两个辅助数组
1.closest[i]:记录最小生成树的边依附的顶点
2.lowcost[i]:记录已存在的顶点
普里姆算法代码
void prime(int v, int n)
{
int lowcost[MAX], closest[MAX];
int min, count = 0;
int i, j, k;
for (i = 0; i < n; i++)
{
lowcost[i] = road[v][i];
closest[i] = v;
}
for (i = 1; i < n; i++)
{
min = N;
k = 0;
for (j = 0; j < n; j++)
{
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;
}
}
if (k != 0)
{
count += min;
b lowcost[k] = 0;
}
else
{
count = -1;
break;
}
for (j = 0; j < n; j++)
{
if (lowcost[j] != 0 && road[k][j] < lowcost[j])
{
lowcost[j] = road[k][j];
closest[j] = k;
}
}
}
}
当然,由生成树构建可知,我们构建最小生成树不一定是唯一的,但是权值和一定是唯一的
普里姆算法与贪心算法:
我们知道贪心算法也是一种求最小的方法,但是它找到的却不一定是权值和最小,贪心算法注重眼前,只去找最小,而不考虑边数,不回溯修改,所以算法效率高但是却不一定找到最优解
普里姆算法相当于贪心算法来说具有大局观,它在选择后还会进行修正,以确定找到的是最优解,所以贪心是效率加运气,普里姆是致力找最优
克鲁斯卡尔算法
我们构建最小生成树除了普里姆算法外,还有克鲁斯卡尔算法,而克鲁斯卡尔算法构建最小生成树的方法是按权值的递增次序选择合适的边来构建
克鲁斯卡尔算法实现过程
1.置U的初值等于V,最小生成树的边集初值为空
2.将图中的边按权值从小到大的顺序依次选取,若选取边未造成回路则加入边集否则舍弃
克鲁斯卡尔算法图展示
克鲁斯算法代码
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;
k++;
p=p->nextarc;
}
}
Sort(E,g.e);
for(i=0;i<g.n;i++)
vset[i]=i;
k=1;
j=0;
while(k<g.n)
{
u1=E[j].u;
v1=E[j].v;
sn1=vset[u1];
sn2=vset[v1];
if(sn1!=sn2)
{
k++;
for(i=0;i<g.n;i++)
{
if(vset[i]==sn2)
vset[i]=sn1;
}
}
j++;
}
}
上述克鲁斯卡尔算法我们可以看出步骤太繁琐,所以我们需要改进克鲁斯卡尔算法,同时根据克鲁斯卡尔算法的步骤我们发现,这个步骤很像之前的并查集,根据权值合并成树,于是改进后的克鲁斯卡尔算法代码如下
void Kruskal(AdjGraph *g)
{
int i,j,k,u1,v1,sn1,sn2;
UFSTree t[MAXSize];//并查集
ArcNode *p;
Edge E[MAXSize];
k=1;
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;
}
HeapSort(E,g.e);
MAKE_SET(t,g.n);
k=1;
j=1;
while(k<g.n)
{
u1=E[j].u;
v1=E[j].v;
sn1=FIND_SET(t,u1);
sn2=FIND_SET(t,v1);
if(sn1!=sn2)
{
k++;
UNION(t,u1,v1);
}
j++;
}
}
}
最小生成树算法应用
PTA 7-4公路村村通
根据题意我们知道,这题其实就是在求带权值和最小,所以就是最小生成树的应用,而这题和单纯的最小生成树又有一些不一样,相当于在找到多条权值最小路径时我们要进行抉择。
公路村村通核心代码
void prime(int v, int n)
{
int lowcost[MAX], closest[MAX];
int min, count = 0;
int i, j, k;
lowcost[1] = 0;
closest[1] = 0;
for (i = 2; i <= n; i++)
{
lowcost[i] = road[v][i];
closest[i] = v;
}
for (i = 2; i <= n; i++)
{
min = N;
k = 0;
for (j = 1; j <= n; j++)
{
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;
}
}
if (k != 0)
{
count += min;
lowcost[k] = 0;
}
else
{
count = -1;
break;
}
for (j = 2; j <= n; j++)
{
if (lowcost[j] != 0 && road[k][j] < lowcost[j])
{
lowcost[j] = road[k][j];
closest[j] = k;
}
}
}
printf("%d", count);
}
从核心代码看出,这是普里姆算法的应用,只是略加修改,由此看出,最小生成树同样应用于生活中。
最短路径相关算法及应用,可适当拓展最短路径算法
我们在求最短路径时要思考一个问题:我们是找一个点到其他所有顶点的最短,还是两个顶点之间最短?
两个问题对应两种求最短路径的算法
单源最短路径--迪杰斯特拉算法
简单来说迪杰斯特拉算法就是通过一个一个添加顶点从而找到一个顶点到其他所有顶点的路径,同时在查找时根据路线长度修正路径,已达到目的
因为我们记录数据时需要数组进行辅助,同时加上存储的值用邻接表来不方便,所以在运用迪杰斯特拉算法时我们选择邻接矩阵
dist数组:记录原地到每个终点的最短路径长度
path数组:记录顶点前驱
迪杰斯特拉算法代码
void Dijkstra(MatGraph g,int v)
{
int dist[MAXV],path[MAXV];
int s[MAXV];
int minds,i,j,u;
for(i=0;i<g.n;i++)
{
dist[i]=g.edges[v][i];
s[i]=0;
if(g.edges[v][i]<INF)
path[i]=v;
else
path[i]=-1;
}
s[v]=1;
for(i=0;i<g.n;i++)
{
minds=INF;
for(j=0;j<g.n;j++)
{
if(s[j]==0&&dist[j]<minds)
{
u=j;
minds=dist[j];
}
s[u]=1;
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])
{
dist[j]=dist[u]+g.edges[u][];
path[j]=u;
}
}
}
}
}
迪杰斯特拉算法存在缺陷,那就是它不能用于求带负值的带权路径图,不适合求最长路径
弗洛伊德算法
相当于迪杰斯特拉算法来说,弗洛伊德算法形式上简单一些,同时不需要每次求dist中最短路径
弗洛伊德算法代码
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];
if(i!=j&&g.edges[i][j]<INF)
path[i][j]=i;
else
path[i][j]=-1;
}
}
for(k=0;k<g.n;k++)
{
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;
}
}
}
最短路径算法应用 PTA7-7 旅游规划
由题意,我们需要求的就是顶点间最短距离,按道理弗洛伊德算法也是可以的,但是这里我使用了迪杰斯特拉算法, 方法就在于寻找最短路径,但是这里其实不需要path数组
核心代码如下
void Dijkstra(MGraph& g, int s)
{
int p[MAXV] = {0};
int mindis, i, j, u;
money[s] = 0;
dist[s] = 0;
for (i = 0; i < g.n; i++)
{
dist[i] = g.edges[s][i];
p[i] = 0;
money[i] = cost[s][i];
if (g.edges[s][i] < INF)
path[i] = s;
else
path[i] = -1;
}
p[s] = 1;
for (i = 0; i < g.n; i++)
{
mindis = INF;
for (j = 0; j < g.n; j++)
{
if (p[j] == 0 && dist[j] < mindis)
{
u = j;
mindis = dist[j];
}
}
p[u] = 1;
if (u == -1) return;
for (j = 0; j < g.n; j++)
{
if (p[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;
money[j] = money[u] + cost[u][j];
}
else if (dist[j] == dist[u] + g.edges[u][j]) {
if (money[j] > money[u] + cost[u][j])
{
money[j] = money[u] + cost[u][j];
path[j] = u;
}
}
}
}
}
}
拓扑排序、关键路径
拓扑排序
在一个有向无环图中找一个拓扑序列的过程称为拓扑排序。
排序条件
1.每个顶点只能出现一次
2.若存在A到B的路径,则A出现在B之前
有向无环图就表示,如果图存在回路,则无法进行拓扑排序,同时我们也可以用拓扑排序来检验图中是否有回路
拓扑排序方法
1.从有向图中选取一个没有前驱的顶点(如果有回路,则顶点一直存在前驱,故无法拓扑排序),并输出
2.删去此顶点及所有以它为尾的弧
3.重复1,2,直至图空,或不空但找不到无前驱的顶点
拓扑排序图展示
根拓扑排序的找无前驱顶点则输出为C1-C2-C3-C4-C5-C7-C9-C10-C11-C6-C12-C8,当然,每一次选择顶点不同,输出的拓扑排序就不相同。
结构体代码
typedef struct
{
vertex data;
int count;
ArcNode *firstarc;
}VNode;
拓扑排序代码
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)
{
top++;
St[top]=i;
}
}
while(top>-1)
{
i=St[top];
top--;
p=G->adjlist[i].firstarc;
while(p!=NULL)
{
j=p->adjvex;
G->adjlist[i].count--;
if(G->adjlist[j].count==0)
{
top++;
St[top]=j;
}
p=p->nextarc;
}
}
}
关键路径
整个工程完成的时间:从有向图的源点到汇点的最长路径,又叫关键路径
前面我们学习了求最小路径,同时我们也说到求最小路径的方法不能求最大路径,而在现实中,对于工程我们不能去求最小,而且去求最大,所以这里我们引入了关键路径。
求关键路径的过程
如果想要得到关键路径,那么我们需要的就是两个步骤
1.找事件最早开始时间
2.找事件最迟开始时间
对有向图进行拓扑排序
根据拓扑序列计算时间的ve,vl数组
计算关键活动的e[],l[]。即最早的时间,最迟的时间
找e=l
关键活动连起来
所以找关键路径概括起来就是:进行拓扑排序逆排序后,找最早和最晚时间相等的事件,这些事件连起来就是关键路径
1.2.谈谈你对图的认识及学习体会。
图的学习其实我们就是围绕着遍历图进行的,一开始的深度广度就是进行图的遍历的基础方法,而我们图的建立也都是为了遍历服务的,如最小生成树,最小路径,拓扑排序就是根据不同的要求进行遍历;
同时图的遍历涉及了很多新的算法,所以图这部分我们需要的不只是学会去创建它,遍历它,还要能对算法熟练运用,不然我们的遍历很难进行下去;
最后图的运用在生活中很广泛,很多工程问题都是运用到图的知识点,同时图中还融入了树的应用,所以我们的非线性结构其实是相通的。
2.阅读代码(0--5分)
2.1 题目及解题代码
class Solution {
public:
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
vector<int> rp(1001);
int sz = edges.size();
// 初始化各元素为单独的集合,代表节点就是其本身
for(int i=0;i<sz;i++)
rp[i] = i;
for(int j=0;j<sz;j++){
// 找到边上两个节点所在集合的代表节点
int set1 = find(edges[j][0], rp);
int set2 = find(edges[j][1], rp);
if(set1 == set2) // 两个集合代表节点相同,说明出现环,返回答案
return edges[j];
else // 两个集合独立,合并集合。将前一个集合代表节点戳到后一个集合代表节点上
rp[set1] = set2;
}
return {0, 0};
}
// 查找路径并返回代表节点,实际上就是给定当前节点,返回该节点所在集合的代表节点
// 之前这里写的压缩路径,引起歧义,因为结果没更新到vector里,所以这里改成路径查找比较合适
// 感谢各位老哥的提议
int find(int n, vector<int> &rp){
int num = n;
while(rp[num] != num)
num = rp[num];
return num;
}
};
作者:Zhcode
链接:https://leetcode-cn.com/problems/redundant-connection/solution/tong-su-jiang-jie-bing-cha-ji-bang-zhu-xiao-bai-ku/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.1.1 该题的设计思路
该题的思路其实就是根据并查集,不断去合并“子树”,从而完成图的连接,一开始对各个节点单独赋值后根据题目给出的边我们一步一步去合并各个集合,同时边合并边判断是否存在环,因为题目要就形成的是无环图,所以判断造成环的边后我们去返回
2.1.2 该题的伪代码
定义动态数组rp存储结点
取边长数,以便对节点数组进行初始化
for(int i to sz)
初始化所有节点数组值为节点本身
for(int j=0 to sz){
找边上节点所在集合的父节点
if(父节点相同)
出现环,返回边
else
两个集合独立,合并集合。将前一个集合代表节点戳到后一个集合代表节点上
}
return {0, 0};
}
};
2.1.3 运行结果
2.1.4分析该题目解题优势及难点
优势:其实一开始我自己看到题目时第一反应就是先建完图再依次去遍历,没有考虑过并查集的使用,该解题思路并查集的使用降低了解题繁琐度,同时对于如何引入并查集,将节点初始化为自己这个做法也降低了题目难度,同时对于环的判断也简单易懂
难点:这题的难点主要就是如何对节点进行并查集合并,因为这是本题的核心,也是实现起来需要细细思考的地方,如果没有理清楚合并思路,则题目就会出现问题。
2.2 题目及解题代码
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> indegree(numCourses);
vector<vector<int>> graph(numCourses);//构建临接表(用vector储存临接点,方便访问)
vector<int> v;
for (int i = 0; i < numCourses; i++)
{
indegree[i] = 0;
graph.push_back(v);
}
for (int i = 0; i < prerequisites.size(); i++)
{
indegree[prerequisites[i][0]]++;
graph[prerequisites[i][1]].push_back(prerequisites[i][0]);//存的是出边
}
//将入度为0的顶点入队
queue<int> myqueue;
for (int i = 0; i < numCourses; i++)
{
if (indegree[i] == 0)
myqueue.push(i);
}
int cnt = 0;
while (!myqueue.empty())
{
int temp = myqueue.front();
myqueue.pop();
cnt++;
//更新:
for (int i = 0; i < graph[temp].size(); i++)
{
indegree[graph[temp][i]]--;
if (indegree[graph[temp][i]] == 0)//放在这里做!只判断邻接点。
myqueue.push(graph[temp][i]);
}
}
return cnt == numCourses;
}
};
作者:Smart_Shelly
链接:https://leetcode-cn.com/problems/course-schedule/solution/c-jian-ji-yi-dong-de-ke-cheng-biao-tuo-bu-pai-xu-b/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.2.1 该题的设计思路
该题其实就是判断是否存在环或者说,前驱不存在的点而这个解题最经典的就是拓扑排序,该解题代码其实就是在使用拓扑排序进行环判断,以此完成题目要求
2.2.2 该题的伪代码
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
定义一个记录入度得动态数组indegree
定义数组graph以构建临接表
vector<int> v;
for (int i = 0 to numCourses)
{
将所有入度赋值为0以方便构建邻接表
}
for (int i to prerequisites.size())
{
构建邻接表,修改入度值
}
将入度为0的顶点入队
for (int i = 0 to numCourses)
{
if (入度值== 0)
入队
}
定义cnt = 0;
while (队不空)
{
取队首
cnt++;
for (int i = 0 to graph[temp].size())
{
修改入度判断
if (修改后入度为0)
将此时的边入队
}
}
return cnt == numCourses;
}
};
作者:Smart_Shelly
链接:https://leetcode-cn.com/problems/course-schedule/solution/c-jian-ji-yi-dong-de-ke-cheng-biao-tuo-bu-pai-xu-b/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.2.3 运行结果
2.2.4分析该题目解题优势及难点。
课程表是拓扑排序很经典的题目,该解题方法在运行时间上也就是时间复杂度上其实不高,但是占用的内存在数据过大时会很多,它的思路其实很明显就是在拓扑排序修改前驱,以此判断是不是存在环,只是这个方法可能如果不是能完全理解写起来就会不是很清晰。
2.3题目及解题代码
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int K) {
const int INF = 0x3f3f3f3f;
vector<vector<int>> g(n+1, vector<int>(n+1, INF));
for (auto &v: times){
g[v[0]][v[1]] = v[2];
}
vector<int> dist(n+1, INF); // 距离起始点的最短距离
vector<bool> st(n+1, false); // 是否已经得到最优解
dist[K] = 0; // 起始点
for (int i = 0; i< n - 1; i++ ){
int t = -1;
for (int j = 1; j <=n; j++){ // 在还未确定最短路的点中,寻找到起始点距离最小的点 的点
if (!st[j] && (t == -1 || dist[t] > dist[j])){
t = j;
}
}
st[t] = true; // t号点的最短路已经确定
for (int j = 1; j<=n; j++){ // 用t更新其他点的距离
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
int ans = *max_element(dist.begin()+1, dist.end());
return ans == INF ? -1: ans;
}
};
作者:muyids
链接:https://leetcode-cn.com/problems/network-delay-time/solution/dan-yuan-zui-duan-lu-po-su-de-dijkstra-dui-you-hua/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.3.1该题的设计思路
其实这题看起来在找延迟,实际上就是在找最短路径,而说到最短路径,我们学到的算法有两种:迪杰斯特拉算法以及弗洛伊德算法,而该解题则是通过迪杰斯特拉算法,不断寻找最短路径,进行修正,已到达找到最短路径的目的
2.3.2该题的伪代码
int networkDelayTime(vector<vector<int>>& times, int n, int K) {
定义一个无穷数方便修改最小路径
定义二维动态数组g存储边
dist数组记录距离起始点的最短距离
st数组进行修正
dist[起始点] = 0
for (int i = 0 to n-1){
int t = -1;
for (int j = 1 to n){
寻找到起始点距离最小的点
}
st[当前点] = true;
for (int j = 1 to n){
用顶点t更新其他点的距离
}
}
};
2.3.3运行结果
2.3.4分析该题目解题优势及难点
优势:迪杰斯特拉算法是很简单易懂的,而这里的修正也保证了找到路径是最短路径
难点:我们知道,迪杰斯特拉算法存在问题,并不是完美的算法而这里的的解法就限制了题目的衍生性