DS博客作业04--图
0.PTA得分截图
1.本周学习总结(0-5分)
1.1 总结图内容
图存储结构
比较常见和重要的图的存储结构:邻接矩阵和邻接表。
邻接矩阵:
内容:
邻接矩阵,顾名思义,是一个矩阵,一个存储着边的信息的矩阵,而顶点则用矩阵的下标表示。对于一个邻接矩阵M,如果M(i,j)=1,则说明顶点i和顶点j之间存在一条边,对于无向图而言,M(i,j)=M(j,i),所以其邻接矩阵是一个对称的矩阵;对于有向图而言,则未必是一个对称的矩阵。邻接矩阵的对角线元素都为0.如下图是一个无向图与其对应的邻接矩阵:
注意点:
图的邻接矩阵存储方式,结构由顶点数量、边数量、顶点集合和边集合组成。
其中顶点集合一维数组,根据顶点的数量动态分配数组大小。
边集合是二维数组,根据顶点的数量来动态分配数组大小,对于无向图来说,该邻接矩阵是对称矩阵。
邻接矩阵比较适用于稠密图。
结构体定义:
#define MAXV <最大顶点个数>
typedef struct
{ int no; //顶点编号
InfoType info; //顶点其他信息
} VertexType;
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,边数
VertexType vexs[MAXV]; //存放顶点信息
} MatGraph;
MatGraph g;//声明邻接矩阵存储的图
应用:
6-1 jmu-ds-邻接矩阵实现图的操作集
邻接表:
对于顶点数很多但是边数很少的图来说,用邻接矩阵显得略为“奢侈”,因为矩阵元素为1的很少,即其中的有用信息很少,但却占了很大的空间。所以邻接表的存在就很有必要了。
内容:
邻接表,存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
对于无向图来说,使用邻接表进行存储也会出现数据冗余,表头结点A所指链表中存在一个指向C的表结点的同时,表头结点C所指链表也会存在一个指向A的表结点。
沿用上面邻接矩阵的无向图来构建邻接表,如下图所示:
作为顶点0,它的邻接顶点有1,3,4,形成的边有(0,1),(0,3)和(0,4),所以顶点0将其指出来了;
对于顶点1,它的邻接顶点有0,2,4,所以顶点1将其指出来了,以此类推。
他们的边没有先后顺序之分。
左边的节点称为顶点节点,其结构体包含顶点元素和指向第一条边的指针;
右边的为边节点,结构体包含边的顶点对应的下标,和指向下一个边节点的指针。
对于有权值的网图,只需要在边节点增加一个权值的成员变量即可。
结构体定义:
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
应用:
6-2 jmu-ds-图邻接表操作
图遍历及应用。
图的遍历算法有两种:深度优先搜索和广度优先搜索
深度优先搜索:
内容:
深度优先搜索算法所遵循的策略是尽可能“深”地搜索一个图,它的基本思想是首先访问图中某一个起始定点v,然后由v出发,访问与v邻接且为被访问的任一个顶点w,再访问与w邻接且未被访问的任一顶点...重复上述过程,当不能再继续向下访问时,一次退回到最近被访问的顶点,若它还有邻接顶点未被访问,则从该点开始继续上述搜索过程,直到图中所有顶点均被访问过为止
算法实现:
void DFS(ALGraph *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;
}
}
时间复杂度:
邻接表:O(n+e)
邻接矩阵:O(n2)
应用:
7-1 图着色问题
7-2 六度空间
判断图是否连通:
思想:
采用某种遍历方式来判断无向图G是否连通。先给visited[]数组(为全局变量)置初值0,然后从0顶点开始遍历该图。
在一次遍历之后,若所有顶点i的visited[i]均为1,则该图是连通的;否则不连通。
代码实现:
int visited[MAXV];
bool Connect(AdjGraph *G) //判断无向图G的连通性
{ int i;
bool flag=true;
for (i=0;i<G->n;i++) //visited数组置初值
visited[i]=0;
DFS(G,0); //调用前面的中DSF算法,从顶点0开始深度优先遍历
for (i=0;i<G->n;i++)
if (visited[i]==0)
{ flag=false;
break;
}
return flag;
}
应用:
7-5 通信网络设计 (10分)
查找图路径
如图
从图中的v1找到到v4的所有路径:
1.从v1出发,将v1标记,并将其入栈。
2.找到v0,将其标记,将其入栈。
3.找到v4,将其标记,入栈。v4是终点,将栈中的元素从栈底往栈顶输出,即为一条路径。v4出栈,并取消标记,回溯到v0。
4.v0除v4外无其它出度,将v0出栈,并取消标记,回溯到v1。
5.找到v2,将其标记并入栈。
6.找到v0,将其标记并入栈。
7.找到v4,将其标记,入栈。v4是终点,将栈中的元素从栈底往栈顶输出,即为一条路径。v4出栈,并取消标记,回溯到v0。
8.v0除v4外无其它出度,将v0出栈,并取消标记,回溯到v2。
9.找到v3,将其标记并入栈。
10.找到v4,将其标记,入栈。v4是终点,将栈中的元素从栈底往栈顶输出,即为一条路径。v4出栈,并取消标记,回溯到v3。
11.v3除v4外无其它出度,将v3出栈,并取消标记,回溯到v2。
12.v2除v0,v3外无其它出度,将v2出栈,并取消标记,回溯到v1。
13.v1除v0,v2外无其它出度,将v1出栈,栈空,结束遍历。
代码实现
void DFS(int start,int end)//深搜入栈查询所有路径
{
visited[start] = true;//visited数组存储各定点的遍历情况,true为已遍历(标记)
stack.Push(某个顶点);//入栈
for (int j = 0; j < list.Size(); j++) {
if (start== end) {//找到终点
for (int i=0; i < stack.Size()-1; i++) {
//输出从栈底到栈顶的元素,即为一条路径
}
stack.Pop();//出栈
visited[start] = false;
break;
}
if (!visited[j]) {//该顶点未被访问过
DFS(j,end);
}
if (j == list.Size() - 1 ) {//如果该顶点无其它出度
stack.Pop();
visited[start] = false;
}
}
}
广度优先搜索:
宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。
通俗来说,从某点开始,走四面可以走的路,然后在从这些路,在找可以走的路,直到最先找到符合条件的,这个运用需要用到队列。
代码实现:
邻接矩阵:
void BFS(AMGraph &G,char v0)
{
int u,i,v,w;
v = LocateVex(G,v0); //找到v0对应的下标
printf("%c ", v0); //打印v0
visited[v] = 1; //顶点v0已被访问
q.push(v0); //将v0入队
while (!q.empty())
{
u = q.front(); //将队头元素u出队,开始访问u的所有邻接点
v = LocateVex(G, u); //得到顶点u的对应下标
q.pop(); //将顶点u出队
for (i = 0; i < G.vexnum; i++)
{
w = G.vexs[i];
if (G.arcs[v][i] && !visited[i])//顶点u和w间有边,且顶点w未被访问
{
printf("%c ", w); //打印顶点w
q.push(w); //将顶点w入队
visited[i] = 1; //顶点w已被访问
}
}
}
}
邻接表:
void BFS(ALGraph &G, char v0)
{
int u,w,v;
ArcNode *p;
printf("%c ", v0); //打印顶点v0
v = LocateVex(G, v0); //找到v0对应的下标
visited[v] = 1; //顶点v0已被访问
q.push(v0); //将顶点v0入队
while (!q.empty())
{
u = q.front(); //将顶点元素u出队,开始访问u的所有邻接点
v = LocateVex(G, u); //得到顶点u的对应下标
q.pop(); //将顶点u出队
for (p = G.vertices[v].firstarc; p; p = p->nextarc) //遍历顶点u的邻接点
{
w = p->adjvex;
if (!visited[w]) //顶点p未被访问
{
printf("%c ", G.vertices[w].data); //打印顶点p
visited[w] = 1; //顶点p已被访问
q.push(G.vertices[w].data); //将顶点p入队
}
}
}
}
查找最短路径
有Dijkstra(迪杰斯特拉)算法和用Floyd(弗洛伊德)算法
Dijkstra(迪杰斯特拉)算法:
迪杰斯特拉(Dijkstra)算法主要是针对没有负值的有向图,求解其中的单一起点到其他顶点的最短路径算法。
算法思想:
设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
具体操作
1.初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。
2.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
3.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
4.重复步骤b和c直到所有顶点都包含在S中。
代码实现(仅仅只是思路,未进行测试)
const int MAXINT = 32767;
const int MAXNUM = 10;
int dist[MAXNUM];
int prev[MAXNUM];
int A[MAXUNM][MAXNUM];
void Dijkstra(int v0)
{
bool S[MAXNUM]; // 判断是否已存入该点到S集合中
int n=MAXNUM;
for(int i=1; i<=n; ++i)
{
dist[i] = A[v0][i];
S[i] = false; // 初始都未用过该点
if(dist[i] == MAXINT)
prev[i] = -1;
else
prev[i] = v0;
}
dist[v0] = 0;
S[v0] = true;
for(int i=2; i<=n; i++)
{
int mindist = MAXINT;
int u = v0; // 找出当前未使用的点j的dist[j]最小值
for(int j=1; j<=n; ++j)
if((!S[j]) && dist[j]<mindist)
{
u = j; // u保存当前邻接点中距离最小的点的号码
mindist = dist[j];
}
S[u] = true;
for(int j=1; j<=n; j++)
if((!S[j]) && A[u][j]<MAXINT)
{
if(dist[u] + A[u][j] < dist[j]) //在通过新加入的u点路径找到离v0点更短的路径
{
dist[j] = dist[u] + A[u][j]; //更新dist
prev[j] = u; //记录前驱顶点
}
}
}
}
Floyd(弗洛伊德)算法:
是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题。时间复杂度为O(N3),空间复杂度为O(N2)。
算法思想:
从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j。所以,假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。
具体操作:
1.从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
2.对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。
代码实现:
typedef struct
{
char vertex[VertexNum]; //顶点表
int edges[VertexNum][VertexNum]; //邻接矩阵,可看做边表
int n,e; //图中当前的顶点数和边数
}MGraph;
void Floyd(MGraph g)
{
int A[MAXV][MAXV];
int path[MAXV][MAXV];
int i,j,k,n=g.n;
for(i=0;i<n;i++)
for(j=0;j<n;j++)
{
A[i][j]=g.edges[i][j];
path[i][j]=-1;
}
for(k=0;k<n;k++)
{
for(i=0;i<n;i++)
for(j=0;j<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;
}
}
}
应用:
7-6 旅游规划
6-4 jmu-ds-最短路径
最小生成树:
prim算法:
普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。
具体操作:
输入:一个加权连通图,其中顶点集合为V,边集合为E;
初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
重复下列操作,直到Vnew = V:
在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
将v加入集合Vnew中,将<u, v>边加入集合Enew中;
输出:使用集合Vnew和Enew来描述所得到的最小生成树。
代码实现:
void prim()
{
for (int i=1;i<=n;i++) //以1作为根节点
{
if(edge[1][i]!=INF)
p[i]=1;
d[i]=edge[1][i];
}
while (1)
{
int maxx=INF;
int u=-1;
for (int i=1;i<=n;i++) //找到距离生成树最短距离的节点
{
if(!vis[i]&&d[i]<maxx)
{
maxx=d[i];
u=i;
}
}
if(u==-1)
break;
vis[u]=1;
for (int i=1;i<=n;i++)
{
if(!vis[i]&&d[i]>edge[u][i])
{
d[i]=edge[u][i];
p[i]=u;
}
}
}
int sum=0;
for (int i=1;i<=n;i++)
if(p[i]==-1)
{
printf("不存在最小生成树\n");
return ;
}
else
sum+=d[i];
printf("最短生成树的长度为%d\n",sum);
return ;
}
时间复杂度:
邻接矩阵:O(n2)
邻接表:O(elog2n)
Kruskal算法
克鲁斯卡尔算法的基本思想是以边为主导地位,始终选择当前可用的最小边权的边。每次选择边权最小的边链接两个端点是kruskal的规则,并实时判断两个点之间有没有间接联通。
具体操作:
输入:图G
输出:图G的最小生成树
(1)将图G看做一个森林,每个顶点为一棵独立的树
(2)将所有的边加入集合S,即一开始S = E
(3)从S中拿出一条最短的边(u,v),如果(u,v)不在同一棵树内,则连接u,v合并这两棵树,同时将(u,v)加入生成树的边集E'
(4)重复(3)直到所有点属于同一棵树,边集E'就是一棵最小生成树
代码实现:
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;
}
应用:
7-4 公路村村通
拓扑排序:
介绍:
对一个有向无环图G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行(对于数据流来说就是死循环)。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列,由AOV网构造拓扑序列的过程叫做拓扑排序。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
实现步骤:
在有向图中选一个没有前驱的顶点并且输出;
从图中删除该顶点和所有以它为尾的弧,即删除所有和它有关的边;
重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。
举例:
如下图:
首先,我们发现V6和v1是没有前驱的,所以我们就随机选去一个输出,我们先输出V6,删除和V6有关的边,得到下图:
然后,我们继续寻找没有前驱的顶点,发现V1没有前驱,所以输出V1,删除和V1有关的边,得到下图:
再然后,我们又发现V4和V3都是没有前驱的,那么我们就随机选取一个顶点输出,我们输出V4,得到下图:
再再再然后,我们输出没有前驱的顶点V3,得:
没有然后了,最后我们分别输出V5和V2,最后全部顶点输出完成,
该图的一个拓扑序列为:v6–>v1—->v4—>v3—>v5—>v2。
结构体定义:
typedef struct //表头节点类型
{ vertex data; //顶点信息
int count; //存放顶点入度
ArcNode *firstarc; //指向第一条弧
} VNode;
代码实现:
void TopSort(AdjGraph *G) //拓扑排序算法
{ int i,j;
int St[MAXV],top=-1; //栈St的指针为top
ArcNode *p;
for (i=0;i<G->n;i++) //入度置初值0
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;
}
}
应用:
6-3 jmu-ds-拓扑排序 (20分)
关键路径:
介绍:
关键路径:AOE-网中,从起点到终点最长的路径的长度(长度指的是路径上边的权重和)。
假设起点是v0,则我们称从v0到vi的最长路径的长度为vi的最早发生时间,同时,vi的最早发生时间也是所有以vi为尾的弧所表示的活动的最早开始时间,使用e(i)表示活动ai最早发生时间,除此之外,我们还定义了一个活动最迟发生时间,使用l(i)表示,不推迟工期的最晚开工时间。我们把e(i)=l(i)的活动ai称为关键活动,因此,这个条件就是我们求一个AOE-网的关键路径的关键所在了。
具体步骤:
1.输入顶点数和边数,已经各个弧的信息建立图
2.从源点v1出发,令ve[0]=0;按照拓扑序列往前求各个顶点的ve。如果得到的拓扑序列个数小于网的顶点数n,说明我们建立的图有环,无关键路径,直接结束程序
3.从终点vn出发,令vl[n-1]=ve[n-1],按逆拓扑序列,往后求其他顶点vl值
4.根据各个顶点的ve和vl求每个弧的e(i)和l(i),如果满足e(i)=l(i),说明是关键活动。
1.2.谈谈你对图的认识及学习体会。
图的存储结构有两种,邻接矩阵和邻接表。邻接矩阵可以用二维数组来做,邻接表结构是好理解,但结构体的定义还是有一些复杂。
图是一种比线性表和树更为复杂的数据结构,在这种结构中,任意两个元素之间可能存在关系。
图的遍历分为深度遍历和广度遍历。算法上有Dljkstra算法,Floyd算法。
不知怎么的感觉最近学习总是有点赶,可能是最近学得比较理论化的东西且算法多而杂,有点迷糊。
2.阅读代码(0--5分)
2.1 题目及解题代码
题目:
解题代码:
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
const int INF = 0x3f3f3f3f;
vector<int> dist(n, INF);
vector<int> backup(n);
dist[src] = 0;
for (int i = 0; i<= K; i++){
backup.assign(dist.begin(), dist.end());
for (auto &f: flights){
dist[f[1]] = min(dist[f[1]], backup[f[0]] + f[2]);
}
}
if (dist[dst] > INF /2) return -1;
return dist[dst];
}
};
2.1.1 该题的设计思路
本题用的是Bellman-Ford算法(似乎是一种高大上的算法,作者只说用了这个算法,没说这个算法怎么用,头大)
Bellman-Ford算法最关键的部分是松弛操作。即:
for i = 0 to i<= K
backup.assign(dist.begin(), dist.end());
for 枚举所有边
更新最短路
end for;
end for;
查了一下Bellman-Ford算法的应用:
处理有负权边的图;
循环次数的含义:循环K次后,表示不超过K条边的最短距离;
有边数限制的最短路;
如果有负权回路,最短路不一定存在;
Bellman-Ford算法可以求出是否有负环;
第n循环后,还有更新,说明路径上有n+1个点,也就是存在环,还有更新,说明环是负环;
循环n次后, 所有的边u->v,权w满足三角不等式:dist[v]<=dist[u]+w;
时间复杂度:O(n2);
空间复杂度:O(n2);
2.1.2 该题的伪代码
伪代码:
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
const int INF = 0x3f3f3f3f;
定义vector<int> dist(n, INF)表示到起点的最短距离
定义vector<int> backup(n)为了防止串联
dist[src] = 0;
for i = 0 to i<= K
backup.assign(dist.begin(), dist.end());
for 枚举所有边
更新最短路
end for;
end for;
if (dist[dst] > INF /2) return -1;
return dist[dst];
}
};
2.1.3 运行结果
2.1.4分析该题目解题优势及难点。
优势点,同时也是难点:对Bellman-Ford算法的理解和使用,使代码量大大减少;
当然有个较大的缺点:复杂度高,运行效率略低。
2.2 题目及解题代码
题目:
解题代码:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites)
{
vector<int> inDegree(numCourses, 0);
vector<vector<int>> lst(numCourses, vector<int>());
for (auto v : prerequisites)
{
inDegree[v[0]]++;
lst[v[1]].push_back(v[0]);
}
queue<int> que;
for (auto i = 0; i < inDegree.size(); i++)
{
if (inDegree[i] == 0) que.push(i);
}
vector<int> ans;
while (!que.empty())
{
auto q = que.front();
que.pop();
ans.push_back(q);
for (auto l : lst[q])
{
if (--inDegree[l] == 0) que.push(l);
}
}
return ans.size() == numCourses;
}
2.2.1 该题的设计思路
本题应用拓扑排序,具体操作如下:
入度:设有向图中有一结点 v ,其入度即为当前所有从其他结点出发,终点为 v 的的边的数目。
出度:设有向图中有一结点 v ,其出度即为当前所有起点为 v ,指向其他结点的边的数目。
每次从入度为 0 的结点开始,加入队列。入度为 0 ,表示没有前置结点。
处理入度为 0 的结点,把这个结点指向的结点的入度 -1 。
再把新的入度为 0 的结点加入队列。
如果队列都处理完毕,但是和总结点数不符,说明有些结点形成环。
空间复杂度:O(N+e);
时间复杂度:O(N+e)。
2.2.2 该题的伪代码
bool canFinish(int numCourses, vector<vector<int>>& prerequisites)
{
遍历边缘列表,计算入度;
有向无环DAG图的存储,使用邻接表;
for (auto v : prerequisites)
初始化入度列表
初始化邻接表
end for;
queue<int> que;
for auto i = 0 to i < inDegree.size()
将入度为 0 的结点放入队列
end for;
vector<int> ans;
while (!que.empty())
将入度为0的进行出队;
ans.push_back(q);
for (auto l : lst[q])
if --inDegree[l] == 0
将入度=0的node进队,直到队列为空,且入度都为0了
end if;
end for;
end while;
return ans.size() == numCourses;
}
2.2.3 运行结果
2.2.4分析该题目解题优势及难点。
优势点同样也是难点:对拓扑排序的引用。拓扑排序的算法简洁程度比BFS和DFS要好,但是在时间和空间复杂度上比较糟糕。
2.3 题目及解题代码
题目:
解题代码:
class Solution {
private:
static constexpr int mod = 1000000007;
public:
int numOfWays(int n) {
int fi0 = 6, fi1 = 6;
for (int i = 2; i <= n; ++i) {
int new_fi0 = (2LL * fi0 + 2LL * fi1) % mod;
int new_fi1 = (2LL * fi0 + 3LL * fi1) % mod;
fi0 = new_fi0;
fi1 = new_fi1;
}
return (fi0 + fi1) % mod;
}
};
2.3.1 该题的设计思路
本题,作者是应用数学公式来求符合该题条件的递推式;
经计算得到的递推式:
时间复杂度:O(N);
空间复杂度:O(1)。
2.3.2 该题的伪代码
class Solution {
private:
static constexpr int mod = 1000000007;
public:
int numOfWays(int n) {
int fi0 = 6, fi1 = 6;
for i = 2 to i <= n
带入递推式求解;
fi0 = new_fi0;
fi1 = new_fi1;
end for;
return (fi0 + fi1) % mod;
}
};
2.3.3 运行结果
2.3.4分析该题目解题优势及难点。
优势点,同样也是难点:应用数学知识求解递推式,大大降低了时间和空间复杂度,在代码精简度方面自然也是不言而喻。
与用普通的递推求解相比,该题解(强到爆炸!!!)
下面是普通递推求解:
class Solution {
private:
static constexpr int mod = 1000000007;
public:
int numOfWays(int n) {
vector<int> types;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
if (i != j && j != k) {
types.push_back(i * 9 + j * 3 + k);
}
}
}
}
int type_cnt = types.size();
vector<vector<int>> related(type_cnt, vector<int>(type_cnt));
for (int i = 0; i < type_cnt; ++i) {
int x1 = types[i] / 9, x2 = types[i] / 3 % 3, x3 = types[i] % 3;
for (int j = 0; j < type_cnt; ++j) {
int y1 = types[j] / 9, y2 = types[j] / 3 % 3, y3 = types[j] % 3;
if (x1 != y1 && x2 != y2 && x3 != y3) {
related[i][j] = 1;
}
}
}
vector<vector<int>> f(n + 1, vector<int>(type_cnt));
for (int i = 0; i < type_cnt; ++i) {
f[1][i] = 1;
}
for (int i = 2; i <= n; ++i) {
for (int j = 0; j < type_cnt; ++j) {
for (int k = 0; k < type_cnt; ++k) {
if (related[k][j]) {
f[i][j] += f[i - 1][k];
f[i][j] %= mod;
}
}
}
}
int ans = 0;
for (int i = 0; i < type_cnt; ++i) {
ans += f[n][i];
ans %= mod;
}
return ans;
}
};
时间复杂度:O(NT2);
空间复杂度:O(T2+TN)。