——本节内容为Bilibili王道考研《数据结构》P61~64视频内容笔记。
目录
一、最小生成树
1.最小生成树概念
(1)定义
对于一个带权连通无向图G=(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spanning-Tree,MST)。
(2)性质
①最小生成树可能有多个,但边的权值之和总是唯一且最小的;
②最小生成树的边数=顶点数-1;砍掉一条则不连通,增加一条边则会出现回路;
③如果一个连通图本身就是一棵树,则其最小生成树就是它本身;
④只有连通图才有生成树,非连通图只有生成森林。
2.最小生成树算法
(1)Prim算法
①思路:从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止;
②时间复杂度:,适用于边稠密图;
(2)Kruskal算法
①思路:每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通;
②时间复杂度:,适用于边稀疏图;
二、最短路径算法
1.问题
(1)只有单独一个源头,从该源头结点出发,到达其他任意一个顶点所需的最短路径;
(2)每对顶点之间的最短路径;
2.BFS算法(无权图)
(1)无权图
无权图可以视为一种特殊的带权图,只是每条边的权值都为1;
(2)代码实现
void BFS_MIN_Distance(MGraph G, int u) //求顶点u到其他顶点的最短路径
{
for (int i = 0; i < G.vexnum; ++i) //d[i]表示从u到i结点的最短路径
{
d[i] = INT_MAX; //初始化最短路径
path[i] = -1; //最短路径从哪个顶点过来
}
d[u] = 0;
visited[u] = true;
Enqueue(Q, u);
while (!isEmpty(Q)) //BFS算法主过程
{
Dequeue(Q, u); //队头元素u出队
for(int w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
if (!visited[w]) //w为u的尚未访问的邻接结点
{
d[w] = d[u] + 1; //路径长度+1
path[w] = u; //最短路径应该从u到w
visited[w] = true; //设已访问标记
Enqueue(Q, w); //顶点w入队
}
}
}
(3)代码解释
①需创建两个数组d[]、path[]分别代表最短路径和前驱顶点;
②由BFS算法改造而来。先对新增的两个数组进行初始化,再将BFS算法中的访问函数visit进行改造,变为:记录最短路径并更改访问顶点的前驱顶点;
(4)图示(以2顶点开始)
(5)补充
①如上图的例子,2到8的最短路径长度=d[8]=3;通过path数组可知,2到8的最短路径为:
2->6->7->8;
②这样生成的广度优先生成树一定是以2为根的,高度最小的生成树;
3.Dijkstra算法
(1)衍生
为解决BFS算法局限于不带权图的情况,对于带权图的最短路径,采用Dijkstra算法。
(2)示例
①初始:从V0开始,初始化三个数组信息如下:
其中,final数组标记各顶点是否已找到最短路径,初始化时,因为V0到V0的路径为0,也就意味着我们已经找到V0的最短路径,所以将final[0]标记为true;
dist数组标记最短路径长度,若能够找到一个暂时的最短路径长度,就标记为该长度,否则标记为∞;
path数组标记为路径上的直接前驱,初始化均为-1,V0没有直接前驱,就设为-1即可。
②第1轮:循环遍历所有结点,找到还没确定最短路径,且dist最小的顶点Vi,令final[i]=true;然后检查所有邻接自Vi的顶点,若其final值为false,则更新dist和path信息
首先能找到final为false中最小的dist顶点V4,令final[4]为true,即表示已经可以确定V4的最短路径为5,并且其直接前驱为V0;
判断邻接第一个final为false的V1,从V0到V4有一条长度为5的路径,而又可以找到一条V4到V1为3的路径,二者之和比最先得到的10要小,此时把dist[1]改为8,path[1]改为4;
判断邻接第二个final为false的V2,从V4到V2有一条长度为9的路径,加上V0到V4的5,可以暂时找到V2的最短路径为14,修改dist[2]=14,path[2]=4;
判断邻接第三个final为false的V3,从V4到V3有一条长度为2的路径,加上V0到V4的5,可以暂时找到V3的最短路径为7,修改dist[3]=7,path[3]=4;
③第2轮:与第1轮思路一样
首先能找到final为false中最小的dist顶点V3,令final[3]为true,即表示已经可以确定V3的最短路径为7,并且其直接前驱为V4;
判断邻接唯一一个final为false的V2,V3的最短路径为7,加上V3到V2的路径6,等于13,小于14,所以修改dist[2]=13,path[2]=3;
④第3轮:与第1轮思路一样
首先能找到final为false中最小的dist顶点V1,令final[1]为true,即表示已经可以确定V1的最短路径为8,并且其直接前驱为V4;
判断邻接唯一一个final为false的V2,V1的最短路径为8,加上V1到V2的路径1等于9,小于13,修改dist[2]=9,path[2]=1;
⑤第4轮:操作同上
此时没有final为false的顶点,算法结束。
(3)补充
①V0到V2的最短路径长度为dist[2]=9,且通过path[]可知,V0到V2的最短路径为V2<-V1<-V4<-V0;
②时间复杂度:;
③对于负权值带权图,Dijkstra算法并不适用,如下:
4.Floyd算法
(1)衍生
求出每一对顶点之间的最短路径;
(2)算法思想
动态规划思想,将问题的求解分为多个阶段;对于n个顶点的图G,求任意一对顶点Vi->Vj之间的最短路径可分为如下几个阶段:
#初始:不允许在其他顶点中转,最短路径是?
#0:若允许在V0中转,最短路径是?
#1:若允许在V0、V1中转,最短路径是?
#2:若允许在V0、V1、V2中转,最短路径是?
......
#3:若允许在V0、V1、V2......Vn-1中转,最短路径是?
(3)例子
#初始:
#0:
#0判断完后(#1):
#1判断完后(#2):
#2判断完后,可由最终矩阵判断最短路径已经具体路径经过顶点:
(4)代码实现
//......准备工作,根据图的信息初始化矩阵A和path
for (int k = 0; k < n; k++) //考虑以Vk作为中转点
{
for (int i = 0; i < n; i++) //遍历整个矩阵,i为行号,j为列号
{
for (int j = 0; j < n; j++)
{
if (A[i][j] > A[i][k] + A[k][j]) //以Vk为中转点的路径更短
{
A[i][j] = A[i][k] + A[k][j]; //更新最短路径长度
path[i][j] = k; //中转点
}
}
}
}
(5)复杂度
①时间复杂度:;
②空间复杂度:;
(6)补充
①在找完整路径时,需对path矩阵进行彻底的分解才能找到最终的具体最短路径,如下:
②虽然Floyd算法可以解决带负权值的图,但不能解决带负权回路的图,如下: