最小生成树
应用场景:
通信网络各个服务器之间的连接等等
最小生成树:
构造连通图(连接所有节点)的最小代价生成树(即权值和最小)
算法
1.Prim算法
核心思路:
从任意一个节点出发,每次都找到达下一个节点的最小权值的边,同时检查此时是否构成了环(最小生成树构成环会浪费),直至所有节点都连通
①找最小权值的边
遍历当前节点的所有邻接点的权值即可
②如何判断是否会构成环
借助集合的角度来理解 ,刚开始每个节点都是独立的,把他们都看做是一个集合,每次连通都是把两个集合并在一起,如果在把某个节点加入的时候,发现原先那个集合中已经有了该节点即代表出现了环。
实际代码实现上感觉用一个数组来记录节点是否已经接入该集合就好了。从这里来看的话,最小生成树本身的目标就是连接所有节点,而按照prime算法从一个节点往下走的话,出现环必然代表访问到了一个原先已经来过的节点,所以只需记录节点是否被接入即可。
优势场景:
稠密图,边较多时较有优势。
2.Kruskal 算法
核心思路:
针对边来构建,所有边中逐个找权值最小的边,将其边两边的两个节点都标记为访问过,而如果某个当时权值最小的边两个节点都已经被访问过,则认为这条边的加入会带来环,所以不选,选下一条边权值最小的。
代码实现:
①逐个找权值最小的边
构建一个边集数组(结构体),其包含边的两个节点,和它的权重,然后按权重大小排序。
②判断是否会构成环
还是构建一个数组记录节点是否被访问过,如果要加入一条边时,该边两个节点都已经被访问过了就代表会构成环。
优势场景:
稀疏图,边较少时速度比较快。
最短路径
“最短”:
对非网图(即没有权值):最短路径即指两顶点之间经过的边数最少的路径
对网图(即边上有权值):最短路径即指两顶点之间权值和最小的路径
算法
1.Dijkstra
核心思路:
按照离起点的远近实现一层一层往外拓展确定到起点距离的远近(每一层其实只有一个节点),并非直接找到到最远的某个点的最短路径,而是类似于不断摸索,每次近一点的感觉。这里,就其中我在学习时产生的困惑进行一些解释和记录,希望有帮助。
1.初始化,构建辅助数组
void Dijkstra(MyGraph* G, int v, ShortPathTable* D)
{
int path[MAXVER] = { -1 };//记录该节点最短路径的前驱点
//初始化每个点到起点的最短路径权值
for (int i = 0; i < MAXVER; i++)
{
D[i]._value = G->_matrix[v][i];
}
D[v]._value = 0;
D[v]._visit = true;
……
}
这里ShortPathTable* D是一个结构体数组,结构体中包含两个数据元素,一个是从起点V到某个节点是否找到了最短路径(true/false),一个记录最短路径大小。此外,path数组记录节点最短路径的前驱点,方便回溯出整个最短路径。
2.核心算法(将拆开解释)
整体代码(这是特定定义下的代码,具体定义不展示,思路是一样的):
int k=0, min;
for (int i = 1; i < MAXVER; i++)
{
min = INT_MAX;
for (int j = 0; j < MAXVER; j++)//找到距离起点最近的点
{
if (D[j]._visit != true &&D[j]._value < min)
{
k = j;
min = D[j]._value;
}
}
D[k]._visit = true;
for (int h = 0; h < MAXVER; h++)
{
if (D[h]._visit != true && min + G->_matrix[k][h] < D[h]._value)
{
D[h]._value = min + G->_matrix[k][h];
path[h] = k;
}
}
}
①外部大循环:用来寻找起点到每一个节点的最短路径
for (int i = 1; i < MAXVER; i++)
{
……
}
这里解释一下为什么i=1,这是因为起点到起点的最短路径就是0,不用再进行寻找,所以只需要寻找n-1(n为节点数)个最短路径。
会有疑问说,如果起点不是0号点,那会不会导致遗漏?
这里有这个问题是没有认识到这个循环跟起点是哪个点是无关的,这是个单纯的计数用的循环(用来代表要找到n-1个节点的最短路径),它的i并不是节点的索引,之所以能够达到这个效果,是通过前面已经将D[v]._visit = true,即已经将起点本身标记为已经找到最短路径,后面我们可以看到在循环时都会判断该节点是否已经找到了最短路径,这样就可以避免再次寻找起点的最短路径导致错误,也不会出现遗漏。
②按层寻找距离起点最近的点+确定已经找到最短路径
min = INT_MAX;
for (int j = 0; j < MAXVER; j++)//找到距离起点最近的点
{
if (D[j]._visit != true &&D[j]._value < min)
{
k = j;
min = D[j]._value;
}
}
D[k]._visit = true;
这里我当时有的疑问时为什么需要D[j]._visit != true 这个判断条件?
后来认识到就像我前面说的,这个算法的本质是一层一层按离起点的距离远近来构建最短路径的,所以这个循环想要能够完成一层一层往远处走,就必须要把之前已经找到最短路径,离起点较近的点都pass掉,不然就是一个死循环,始终都是离起点最近的一个点在进行,那就毫无意义了。
同时这里解释一下这段代码的工作原理,当时也是挺困惑的。
第一次循环,找到的是起点的邻接点中路径最短的那个点。所有点钟,其他与起点不邻接的刚开始都是无穷大,要想通过其他节点再转过去路径必然也会比邻接点的路径长,所以肯定不是离起点最近的。而由于邻接点中该点路径最短,所以这个点就是离起点最近的点。
而且也已经是到这个节点的最短路径了。因为如果要通过其他节点转,则必然要经过起点的其他邻接点,而又因为不存在负权值,所以必然路径会变长,所以已经是最短路径了,所以可以进行赋值了,D[k]._visit = true;
在后面代码对其他节点的最短路径更新后,下一次循环再找最近节点是就会跳过这个节点而找到离起点第二近的节点,同时由于到这个第二近的点已经是直接到达和通过其余点周转达到了,不可能会有更短的路径了,因为达到这个第二近的点有两种方式,一种是通过上一层最近的点周转达到,一种是直接到达,如果是直接达到,那么其他通过与起点邻接点周转的路径必然更长,如果是第一种方式,那是否存在一种可能是通过其他与起点邻接点周转呢,那么从起点到达这个周转点的路径肯定比达到这个“第二近”的点的路径要短吧(因为加上这个周转点到第二近的点的路径还是比原来路径短),那就矛盾了,所以经过寻找更短路径后,下一次循环的时候的离起点最近的点的路径必然是最短路径了。
③寻找是否有已经达到的点的更短路径和拓展下一层点
for (int h = 0; h < MAXVER; h++)
{
if (D[h]._visit != true && min + G->_matrix[k][h] < D[h]._value)
{
D[h]._value = min + G->_matrix[k][h];
path[h] = k;
}
}
这里解释一下什么叫寻找是否有已经达到的点的更短路径和拓展下一层点。
寻找是否有已经达到的点的更短路径并更新:
已经达到的就是与起点直接邻接的点,以第一次循环为例,在我们在找到离起点最近的点后,达到这些与起点直接邻接的点就有两种方式,一种就是直接到达,一种就是这个最近的节点周转,所以这个循环就是考虑通过这个当前最近的节点周转使得到这些节点的路径比直接到达要更短,而通过这个循环,所有到这些与起点直接邻接的点的路径都被更新了,但要注意,未必就已经是最短的了!
那就有个问题了,在第一个循环以后,明显还有很多节点是没有和起点建立路径的(即他们之间的距离是无穷大),那是否存在一种可能说我们可以从起点出发,经过这些未建立路径的节点然后到达这些与起点直接邻接的点,使得路径更短呢,这个想法看似是有可能,因为我们不知道未知的节点之间的联系,但是细想你就会发现这个想法和整个算法思想是违背的,该算法就是按照离起点远近一层一层摸索最短路径,因此对于这些离起点更远的节点,想要达到他们本身就已经比到达这些与起点直接邻接的点的距离要更远了(直接按照算法逻辑就可以推出),更不用说还要加上这两者之间的路径,所以这是荒谬的。
拓展下一层点:
所谓的拓展下一层点,其实就是与起点邻接的最近的点,他的邻接点的最短路径必然会更新,因为原先与起点不邻接,距离是无穷大,而现在是有限的了,所以他们就是新拓展出来的点,在普林斯顿算法课上,老师曾提到说这些点加入队列,这是非常形象的说法,并不存在一个实际的队列,而是说这些拓展出来的点按照离起点的距离就像排好队一个一个轮流寻找最短路径一样。
2.FloydWarshall算法
核心思路:不断的更新只允许经过1……n-1个节点时各个点之间的最短路径,最后达到最短路径。
//Floyd-Warshall算法求所有节点对最短路径,D是输出
void FloydWarshall(MyGraph* G, DistanceMatrix(*D)[MAXVER])
{
//初始化最短路径数组
for (int i = 0; i < MAXVER; i++)
{
for (int j = 0; j < MAXVER; j++)
{
D[i][j]= G->_matrix[i][j];
}
}
//不断地要求经过别的节点来达到,看是否有更短的路径
for (int k = 0; k < MAXVER; k++)//只允许经过1-k个节点
{
for (int i = 0;i< MAXVER; i++)
{
for (int j = 0; j < MAXVER; j++)
{
if (D[i][k]<1000&& D[k][j]<1000&&i!=j&&D[i][j] > D[i][k] + D[k][j])
{
D[i][j] = D[i][k] + D[k][j];
}
}
}
}
}
不做详细解释,比较简单,具体可以参考《啊哈算啊》啊哈磊这本书。