十一期间看到了以下几个类似的问题,最开始也是有点混淆的状态,这里做一下简单的学习记录,希望可以为有同样问题的小伙伴提供帮助,篇幅比较长,建议收藏后再阅读。
问题描述如下(这里以简单得连接图表示各个连接信息):
1.有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通,各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里,问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
2.与1的问题是一样的,某城市新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通,各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里,问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
3.有7个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄,各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
问:如何计算出G村庄到 其它各个村庄的最短距离? 如果从其它点出发到各个点的最短距离又是多少?
4.有7个村庄(A, B, C, D, E, F, G),各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
问:如何计算出各村庄到 其它各村庄的最短距离?
其实以上四个问题的描述可以简述成以下三个类别:
1.求各个点都连接且权值最小的路线(问题1和问题2);
2.以某一个点作为出发点,求这个点到其它各个点的权值最小的路线(问题3,可以看成是问题4的一个子集);
3.以每一个点作为出发点,求这些点到其它各个点的权值最小的路线(问题4,可以看成是问题3 的一个全集)。
问题的描述很简单,其实都可以归类为修路问题,需要考虑采用什么方式对问题进行求解。修路问题本质就是就是最小生成树问题,什么是最小生成树(Minimum Cost Spanning Tree,简称MST)?给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树。最小生成树有以下特点:
(1)N个顶点,一定有N-1条边
(2)包含全部顶点
(3)N-1条边都在图中
求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法。
普里姆算法
1)普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图;
2)普利姆的算法如下:
(1)设G=(V,E)是连通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合;
(2)若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1;
(3)若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中,将边(ui,vj)加入集合D中,标记visited[vj]=1;
(4)重复步骤(2),直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边。
以此算法来分析问题1。假设从点A 开始,获取所有连接的路线,每次取最小的路线及此路线的另一个点加入,以此循环,步骤大致如下:
①从A顶点开始处理 A-C [7] A-G[2] A-B[5]=> <A,G> 2
② <A,G> 开始 , 将A 和 G 顶点和他们相邻的还没有访问的顶点进行处理 A-C[7] A-B[5] G-B[3] G-E[4] G-F[6]=》<A,G,B>
③ <A,G,B> 开始,将A,G,B 顶点 和他们相邻的还没有访问的顶点进行处理 A-C[7] G-E[4] G-F[6] B-D[9]=><A,G,B,E>
④{A,G,B,E}->F加入, 对应 边<E,F> 权值:5
⑤{A,G,B,E,F}->D加入 , 对应 边<F,D> 权值:4
⑥{A,G,B,E,F,D}->C加入 , 对应 边<A,C> 权值:7 ===> <A,G,B,E,F,D,C>
有了解决问题的思路,接下来用代码直接对问题进行求解。
/**
* @Author likangmin
* @create 2020/10/9 16:46
*/
public class PrimAlgorithm {
public static void main(String[] args) {
char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int dataNum = data.length;
//邻接矩阵的关系使用二维数组表示,10000这个大数,表示两个点不联通
int[][] weight = new int[][]{
{10000, 5, 7, 10000, 10000, 10000, 2},
{5, 10000, 10000, 9, 10000, 10000, 3},
{7, 10000, 10000, 10000, 8, 10000, 10000},
{10000, 9, 10000, 10000, 10000, 4, 10000},
{10000, 10000, 8, 10000, 10000, 5, 4},
{10000, 10000, 10000, 4, 5, 10000, 6},
{2, 3, 10000, 10000, 4, 6, 10000},};
Graph graph = new Graph(dataNum);
MinTrees minTrees = new MinTrees();
minTrees.createGraph(graph, data, weight);
minTrees.showGraph(graph);
int num = 0;
while (true) {
System.out.println("请输入从哪个点开始('A', 'B', 'C', 'D', 'E', 'F', 'G'):");
Scanner sc = new Scanner(System.in);
String str = sc.next();
switch (str) {
case "A":
num = 0;
break;
case "B":
num = 1;
break;
case "C":
num = 2;
break;
case "D":
num = 3;
break;
case "E":
num = 4;
break;
case "F":
num = 5;
break;
case "G":
num = 6;
break;
default:
break;
}
minTrees.prim(graph, num);
}
}
}
class MinTrees {
public void createGraph(Graph graph, char[] data, int[][] weight) {
for (int i = 0; i < data.length; i++) {
graph.data[i] = data[i];
for (int j = 0; j < data.length; j++) {
graph.weight[i][j] = weight[i][j];
}
}
}
public void showGraph(Graph graph) {
for (int[] link : graph.weight) {
System.out.println(Arrays.toString(link));
}
}
public void prim(Graph graph, int index) {
//h1 和 h2 记录两个顶点的下标
int h1 = -1;
int h2 = -1;
int[] visited = new int[graph.dataNum];
//把当前这个结点标记为已访问
visited[index] = 1;
int min = 10000;
//因为有 graph.verxs顶点,普利姆算法结束后,有 graph.verxs-1边
for (int k = 1; k < graph.dataNum; k++) {
//这个是确定每一次生成的子图 ,和哪个结点的距离最近
for (int i = 0; i < graph.dataNum; i++) {
for (int j = 0; j < graph.dataNum; j++) {
if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < min) {
//min(寻找已经访问过的结点和未访问过的结点间的权值最小的边)
min = graph.weight[i][j];
h1 = i;
h2 = j;
}
}
}
//找到一条边是最小
System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + "> 权值:" + min);
//将当前这个结点标记为已经访问
visited[h2] = 1;
min = 10000;
}
}
}
class Graph {
//表示图的节点个数
int dataNum;
//存放结点数据
char[] data;
//存放边,就是我们的邻接矩阵
int[][] weight;
public Graph(int dataNum) {
this.dataNum = dataNum;
this.data = new char[dataNum];
this.weight = new int[dataNum][dataNum];
}
}
我们看一下输出结果(只选取了A和B,结果正确,其他的读者可以自行验证):
请输入从哪个点开始('A', 'B', 'C', 'D', 'E', 'F', 'G'):
A
边<A,G> 权值:2
边<G,B> 权值:3
边<G,E> 权值:4
边<E,F> 权值:5
边<F,D> 权值:4
边<A,C> 权值:7
请输入从哪个点开始('A', 'B', 'C', 'D', 'E', 'F', 'G'):
B
边<B,G> 权值:3
边<G,A> 权值:2
边<G,E> 权值:4
边<E,F> 权值:5
边<F,D> 权值:4
边<A,C> 权值:7
当然,问题2依然可以使用普里姆算法进行求解,读者有需要可以自行使用该方法求解,这里给大家介绍另外一种求解修路问题的方法:克鲁斯卡尔算法,我们将对问题2使用克鲁斯卡尔算法进行求解。
克鲁斯卡尔算法
基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路。
具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。
例如,对于如问题2的所示的连通网可以有多棵权值总和不相同的生成树:
而我们要选取的是权值最小的一个路线。以问题2中的图为例,来对克鲁斯卡尔进行演示(假设,用数组R保存最小生成树结果):
第1步:将边<E,F>加入R中。
边<E,F>的权值最小,因此将它加入到最小生成树结果R中。
第2步:将边<C,D>加入R中。
上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果R中。
第3步:将边<D,E>加入R中。
上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果R中。
第4步:将边<B,F>加入R中。
上一步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳过边<C,F>。将边<B,F>加入到最小生成树结果R中。
第5步:将边<E,G>加入R中。
上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果R中。
第6步:将边<A,B>加入R中。
上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路;因此,跳过边<F,G>。同理,跳过边<B,C>。将边<A,B>加入到最小生成树结果R中。
此时,最小生成树构造完成!它包括的边依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。
根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问题:
问题一,对图的所有边按照权值大小进行排序;问题二,将边添加到最小生成树中时,怎么样判断是否形成了回路。
问题一很好解决,采用排序算法进行排序即可。问题二处理方式是:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。
以上图为例,在将<E,F> <C,D> <D,E>加入到最小生成树R中之后,这几条边的顶点就都有了终点:
C的终点是F, D的终点是F,E的终点是F,F的终点是F。将所有顶点按照从小到大的顺序排列好之后,某个顶点的终点就是"与它连通的最大顶点"。 因此,接下来,虽然<C,E>是权值最小的边。但是C和E的终点都是F,即它们的终点相同,因此,将<C,E>加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。
有了解决问题的思路,接下来用代码直接对问题进行求解。
/**
* @Author likangmin
* @create 2020/10/9 19:24
*/
public class KruskalCase {
private int edgeNum; //边的个数
private char[] vertexs; //顶点数组
private int[][] matrix; //邻接矩阵
//使用 INF 表示两个顶点不能连通
private static final int INF = Integer.MAX_VALUE;
public static void main(String[] args) {
char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//克鲁斯卡尔算法的邻接矩阵
int matrix[][] = {
/*A*//*B*//*C*//*D*//*E*//*F*//*G*/
/*A*/ { 0, 12, INF, INF, INF, 16, 14},
/*B*/ { 12, 0, 10, INF, INF, 7, INF},
/*C*/ { INF, 10, 0, 3, 5, 6, INF},
/*D*/ { INF, INF, 3, 0, 4, INF, INF},
/*E*/ { INF, INF, 5, 4, 0, 2, 8},
/*F*/ { 16, 7, 6, INF, 2, 0, 9},
/*G*/ { 14, INF, INF, INF, 8, 9, 0}};
//创建KruskalCase 对象实例
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
kruskalCase.kruskal();
}
//构造器
public KruskalCase(char[] vertexs, int[][] matrix) {
this.vertexs=vertexs;
this.matrix=matrix;
//统计边的条数
for(int i =0; i < vlen; i++) {
for(int j = i+1; j < vlen; j++) {
if(this.matrix[i][j] != INF) {
edgeNum++;
}
}
}
}
public void kruskal() {
int index = 0; //表示最后结果数组的数目
int[] ends = new int[edgeNum]; //用于保存"已有最小生成树" 中的每个顶点在最小生成树中的终点
//创建结果数组, 保存最后的最小生成树
EData[] rets = new EData[edgeNum];
//获取图中 所有的边的集合
EData[] edges = getEdges();
System.out.println("图的边的集合=" + Arrays.toString(edges) + " 共"+ edges.length); //12
//按照边的权值大小进行排序(从小到大)
sortEdges(edges);
//遍历edges 数组,将边添加到最小生成树中时,判断是准备加入的边否形成了回路,如果没有,就加入 rets, 否则不能加入
for(int i=0; i < edgeNum; i++) {
//获取到第i条边的第一个顶点(起点)
int p1 = getPosition(edges[i].start);
//获取到第i条边的第2个顶点
int p2 = getPosition(edges[i].end);
//获取p1这个顶点在已有最小生成树中的终点
int m = getEnd(ends, p1);
//获取p2这个顶点在已有最小生成树中的终点
int n = getEnd(ends, p2);
//是否构成回路
if(m != n) { //没有构成回路
ends[m] = n; // 设置m 在"已有最小生成树"中的终点
rets[index++] = edges[i]; //有一条边加入到rets数组
}
}
//<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。
//统计并打印 "最小生成树", 输出 rets
System.out.println("最小生成树为");
for(int i = 0; i < index; i++) {
System.out.println(rets[i]);
}
}
/**
* 功能:对边进行排序处理, 冒泡排序
* @param edges 边的集合
*/
private void sortEdges(EData[] edges) {
for(int i = 0; i < edges.length - 1; i++) {
for(int j = 0; j < edges.length - 1 - i; j++) {
if(edges[j].weight > edges[j+1].weight) {//交换
EData tmp = edges[j];
edges[j] = edges[j+1];
edges[j+1] = tmp;
}
}
}
}
/**
*
* @param ch 顶点的值,比如'A','B'
* @return 返回ch顶点对应的下标,如果找不到,返回-1
*/
private int getPosition(char ch) {
for(int i = 0; i < vertexs.length; i++) {
if(vertexs[i] == ch) {//找到
return i;
}
}
//找不到,返回-1
return -1;
}
/**
* 功能: 获取图中边,放到EData[] 数组中,后面我们需要遍历该数组
* 是通过matrix 邻接矩阵来获取
* EData[] 形式 [['A','B', 12], ['B','F',7], .....]
* @return
*/
private EData[] getEdges() {
int index = 0;
EData[] edges = new EData[edgeNum];
for(int i = 0; i < vertexs.length; i++) {
for(int j=i+1; j <vertexs.length; j++) {
if(matrix[i][j] != INF) {
edges[index++] = new EData(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edges;
}
/**
* 功能: 获取下标为i的顶点的终点(), 用于后面判断两个顶点的终点是否相同
* @param ends : 数组就是记录了各个顶点对应的终点是哪个,ends 数组是在遍历过程中,逐步形成
* @param i : 表示传入的顶点对应的下标
* @return 返回的就是 下标为i的这个顶点对应的终点的下标, 一会回头还有来理解
*/
private int getEnd(int[] ends, int i) {
while(ends[i] != 0) {
i = ends[i];
}
return i;
}
}
//创建一个类EData ,它的对象实例就表示一条边
class EData {
char start; //边的一个点
char end; //边的另外一个点
int weight; //边的权值
//构造器
public EData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "EData [<" + start + ", " + end + ">= " + weight + "]";
}
}
我们看一下输出结果,最小生成树为:
EData [<E, F>= 2]
EData [<C, D>= 3]
EData [<D, E>= 4]
EData [<B, F>= 7]
EData [<E, G>= 8]
EData [<A, B>= 12]
即为问题2的求解。
问题1和问题2虽然问题一样,但是两种解法却有不同的地方。使用普里姆算法时,我们可以指定从哪个点开始,但不管从哪个点开始,最终形成的路线是确定的;而克鲁斯卡尔算法在解决问题的时候不用指定从哪个点开始,最终给出问题的解决路线。两种方法虽然略有差别,但最后的性质是一样的,读者可以依据自己的需求和实际使用场景进行选择。问题1和问题2只需要求出可以联通的一个最短路线。如果是对于某一个点,要求这个点可以到达其他的任何一个点,求出满足这样条件下的最短路线又该如何求解呢?
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想,不了解的同学可以自行百度,这里不做扩展),直到扩展到终点为止。
迪杰斯特拉(Dijkstra)算法
迪杰斯特拉(Dijkstra)算法过程是这样的:
①设置出发顶点为v,顶点集合V{v1,v2,vi…},v到V中各顶点的距离构成距离集合Dis,Dis{d1,d2,di…},Dis集合记录着v到图中各顶点的距离(到自身可以看作0,v到vi距离对应为di)
②从Dis中选择值最小的di并移出Dis集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径
③更新Dis集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为vi,表明是通过vi到达的)
④重复执行②③步骤,直到最短路径顶点为目标顶点即可结束
看步骤也许还不明白,这里我们依然以图解的方式进行说明。这里假设以问题3中的G作为出发顶点,这里需要定义三个集合的类:
//已访问顶点集合
class VisitedVertex{
//记录各个顶点是否访问过 1表示访问过,0未访问,会动态更新
public int[] already_arr;
//每个下标对应的值为前一个顶点下标, 会动态更新
public int[] pre_visited;
//记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其它顶点的距离,会动态更新,求的最短距离就会存放到dis
public int[] dis;
}
在还没有进行访问前,需要初始化各个变量:
以G为出发顶点访问过一次后的,变量的值变为:
将以上图转化以下,得到如下形式:
第一行表示already_arr,当前数据表示G已经被访问过,第二行表示pre_visited,当前数据表示A,B,E,F的前驱结点都是G,第三行表示 dis,当前数据表示A到G距离2,B到G距离3,E到G距离4,F到G距离6,N表示当前点与G没有直接相连。G 访问完后,得到最小的距离点A,接下来 A点作为新的访问顶点(注意不是出发顶点)重复上述步骤。这样通过遍历所有的顶点,即可得到问题的求解。
有了解决问题的思路,接下来用代码直接对问题进行求解。
/**
* @Author likangmin
* @create 2020/10/10 9:28
*/
public class DijkstraAlgorithm {
public static void main(String[] args) {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
//邻接矩阵
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;// 表示不可以连接
matrix[0]=new int[]{N,5,7,N,N,N,2};
matrix[1]=new int[]{5,N,N,9,N,N,3};
matrix[2]=new int[]{7,N,N,N,8,N,N};
matrix[3]=new int[]{N,9,N,N,N,4,N};
matrix[4]=new int[]{N,N,8,N,N,5,4};
matrix[5]=new int[]{N,N,N,4,5,N,6};
matrix[6]=new int[]{2,3,N,N,4,6,N};
//创建 Graph对象
Graph graph = new Graph(vertex, matrix);
//测试迪杰斯特拉算法
graph.dsj(6);
graph.showDijkstra();
}
}
class Graph {
private char[] vertex; // 顶点数组
private int[][] matrix; // 邻接矩阵
private VisitedVertex vv; //已经访问的顶点的集合
// 构造器
public Graph(char[] vertex, int[][] matrix) {
this.vertex = vertex;
this.matrix = matrix;
}
//显示结果
public void showDijkstra() {
vv.show();
}
//迪杰斯特拉算法实现
/**
*
* @param index 表示出发顶点对应的下标
*/
public void dsj(int index) {
vv = new VisitedVertex(vertex.length, index);
update(index);//更新index顶点到周围顶点的距离和前驱顶点
for(int j = 1; j <vertex.length; j++) {
index = vv.updateArr();// 选择并返回新的访问顶点
update(index); // 更新index顶点到周围顶点的距离和前驱顶点
}
}
//更新index下标顶点到周围顶点的距离和周围顶点的前驱顶点,
private void update(int index) {
int len = 0;
//根据遍历我们的邻接矩阵的 matrix[index]行
for(int j = 0; j < matrix[index].length; j++) {
// len 含义是 : 出发顶点到index顶点的距离 + 从index顶点到j顶点的距离的和
len = vv.getDis(index) + matrix[index][j];
// 如果j顶点没有被访问过,并且 len 小于出发顶点到j顶点的距离,就需要更新
if(!vv.in(j) && len < vv.getDis(j)) {
vv.updatePre(j, index); //更新j顶点的前驱为index顶点
vv.updateDis(j, len); //更新出发顶点到j顶点的距离
}
}
}
}
// 已访问顶点集合
class VisitedVertex {
// 记录各个顶点是否访问过 1表示访问过,0未访问,会动态更新
public int[] already_arr;
// 每个下标对应的值为前一个顶点下标, 会动态更新
public int[] pre_visited;
// 记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其它顶点的距离,会动态更新,求的最短距离就会存放到dis
public int[] dis;
//构造器
/**
*
* @param length :表示顶点的个数
* @param index: 出发顶点对应的下标, 比如G顶点,下标就是6
*/
public VisitedVertex(int length, int index) {
this.already_arr = new int[length];
this.pre_visited = new int[length];
this.dis = new int[length];
//初始化 dis数组
Arrays.fill(dis, 65535);
this.already_arr[index] = 1; //设置出发顶点被访问过
this.dis[index] = 0;//设置出发顶点的访问距离为0
}
/**
* 功能: 判断index顶点是否被访问过
* @param index
* @return 如果访问过,就返回true, 否则访问false
*/
public boolean in(int index) {
return already_arr[index] == 1;
}
/**
* 功能: 更新出发顶点到index顶点的距离
* @param index
* @param len
*/
public void updateDis(int index, int len) {
dis[index] = len;
}
/**
* 功能: 更新pre这个顶点的前驱顶点为index顶点
* @param pre
* @param index
*/
public void updatePre(int pre, int index) {
pre_visited[pre] = index;
}
/**
* 功能:返回出发顶点到index顶点的距离
* @param index
*/
public int getDis(int index) {
return dis[index];
}
/**
* 继续选择并返回新的访问顶点, 比如这里的G 完后,就是 A点作为新的访问顶点(注意不是出发顶点)
* @return
*/
public int updateArr() {
int min = 65535, index = 0;
for(int i = 0; i < already_arr.length; i++) {
if(already_arr[i] == 0 && dis[i] < min ) {
min = dis[i];
index = i;
}
}
//更新 index 顶点被访问过
already_arr[index] = 1;
return index;
}
//显示最后的结果
//即将三个数组的情况输出
public void show() {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
int count = 0;
for (int i : dis) {
if (i != 65535) {
System.out.print(vertex[count] + "("+i+") ");
} else {
System.out.println("N ");
}
count++;
}
System.out.println();
}
}
我们看一下输出结果:
A(2) B(3) C(9) D(10) E(4) F(6) G(0)
问题3只是针对某一个点到其他点的最短距离的求解,如果我们不满足于某一个点,而是希望所有的点都需要一条到其他点的最短路径,可以怎么处理呢?当然我们可以利用以上方式,每个点都使用一遍方法进行求解,但这样不方便,如果点的数量足够多的话,操作起来就会很麻烦。那么有没有什么方法可以一次性得到每一个点到其他点的最短路径的集合呢?这里就需要使用弗洛伊德算法。
弗洛伊德算法
和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。
弗洛伊德算法与迪杰斯特拉算法有什么区别呢?弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径,迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。弗洛伊德算法的大致步骤如下:
①设置顶点vi到顶点vk的最短路径已知为Lik,顶点vk到vj的最短路径已知为Lkj,顶点vi到vj的路径为Lij,则vi到vj的最短路径为:min((Lik+Lkj),Lij),vk的取值为图中所有顶点,则可获得vi到vj的最短路径;
②至于vi到vk的最短路径Lik或者vk到vj的最短路径Lkj,是以同样的方式获得。
以问题4为例,初始各点之间的距离表如图所示:
将A作为中间顶点,可以有三种路线:. C-A-G [9],C-A-B [12],G-A-B [7]。把A作为中间顶点的所有情况都进行遍历, 具体转换过程为:以A顶点作为中间顶点,B->A->C的距离由N->9,同理C到B;C->A->G的距离由N->12,同理G到C;G->B的距离为3小于7,所以不用变。更换中间顶点,循环执行操作,直到所有顶点都作为中间顶点更新后,计算结束就会得到更新距离表 和 前驱关系。第一次更新后距离表和前驱关系为:
有了解决问题的思路,接下来用代码直接对问题进行求解。
/**
* @Author likangmin
* @create 2020/10/10 10:35
*/
public class FloydAlgorithm {
public static void main(String[] args) {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
//创建邻接矩阵
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[] { 0, 5, 7, N, N, N, 2 };
matrix[1] = new int[] { 5, 0, N, 9, N, N, 3 };
matrix[2] = new int[] { 7, N, 0, N, 8, N, N };
matrix[3] = new int[] { N, 9, N, 0, N, 4, N };
matrix[4] = new int[] { N, N, 8, N, 0, 5, 4 };
matrix[5] = new int[] { N, N, N, 4, 5, 0, 6 };
matrix[6] = new int[] { 2, 3, N, N, 4, 6, 0 };
//创建 Graph 对象
Graph graph = new Graph(vertex.length, matrix, vertex);
//调用弗洛伊德算法
graph.floyd();
graph.show();
}
}
// 创建图
class Graph {
private char[] vertex; // 存放顶点的数组
private int[][] dis; // 保存,从各个顶点出发到其它顶点的距离,最后的结果,也是保留在该数组
private int[][] pre;// 保存到达目标顶点的前驱顶点
// 构造器
/**
* @param length 大小
* @param matrix 邻接矩阵
* @param vertex 顶点数组
*/
public Graph(int length, int[][] matrix, char[] vertex) {
this.vertex = vertex;
this.dis = matrix;
this.pre = new int[length][length];
// 对pre数组初始化, 注意存放的是前驱顶点的下标
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
// 显示pre数组和dis数组
public void show() {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
for (int k = 0; k < dis.length; k++) {
// 输出dis数组的一行数据
for (int i = 0; i < dis.length; i++) {
System.out.print("("+vertex[k]+"->"+vertex[i]+":" + dis[k][i] + ") ");
}
}
}
//弗洛伊德算法, 比较容易理解,而且容易实现
public void floyd() {
int len = 0; //变量保存距离
//对中间顶点遍历, k 就是中间顶点的下标 [A, B, C, D, E, F, G]
for(int k = 0; k < dis.length; k++) { //
//从i顶点开始出发 [A, B, C, D, E, F, G]
for(int i = 0; i < dis.length; i++) {
//到达j顶点 // [A, B, C, D, E, F, G]
for(int j = 0; j < dis.length; j++) {
len = dis[i][k] + dis[k][j];// => 求出从i 顶点出发,经过 k中间顶点,到达 j 顶点距离
if(len < dis[i][j]) {//如果len小于 dis[i][j]
dis[i][j] = len;//更新距离
pre[i][j] = pre[k][j];//更新前驱顶点
}
}
}
}
}
}
最后结果为:
A到其他点的最短路径集合
(A->A:0) (A->B:5) (A->C:7) (A->D:12) (A->E:6) (A->F:8) (A->G:2)
B到其他点的最短路径集合
(B->A:5) (B->B:0) (B->C:12) (B->D:9) (B->E:7) (B->F:9) (B->G:3)
C到其他点的最短路径集合
(C->A:7) (C->B:12) (C->C:0) (C->D:17) (C->E:8) (C->F:13) (C->G:9)
D到其他点的最短路径集合
(D->A:12) (D->B:9) (D->C:17) (D->D:0) (D->E:9) (D->F:4) (D->G:10)
E到其他点的最短路径集合
(E->A:6) (E->B:7) (E->C:8) (E->D:9) (E->E:0) (E->F:5) (E->G:4)
F到其他点的最短路径集合
(F->A:8) (F->B:9) (F->C:13) (F->D:4) (F->E:5) (F->F:0) (F->G:6)
G到其他点的最短路径集合
(G->A:2) (G->B:3) (G->C:9) (G->D:10) (G->E:4) (G->F:6) (G->G:0)
至此,以上问题已全部得到解决,同时对于普里姆算法,克鲁斯卡尔算法,弗洛伊德算法与迪杰斯特拉算法的原理和解法进行了详细的分析,希望对大家有所帮助。