写在前面
区别
- 迪杰斯特拉算法,通过选定的被访问顶点,求出从出发访问顶点到其他各个顶点的最短路径
- 弗洛伊德算法,每个顶点都是出发访问点,求出每个顶点到其他顶点的最短路径
最短路径问题(迪杰斯特拉算法)
- 用于计算某一个节点到其他节点的最短路径,主要特点是以起始点为中心向外层层扩展,运用到了广度优先搜索思想,直到扩展到终点为止
- 调用
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, 6};
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[]{5, 3, N, N, 4, 6, N};
Graph graph = new Graph(vertex, matrix);
graph.showGraph();
graph.dsj(3);
- 图类
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 showGraph() {
for (int[] ints : matrix) {
System.out.println(Arrays.toString(ints));
}
}
//算法,index表示出发顶点对应的下标
public void dsj (int index) {
vv = new VisitedVertex(vertex.length, index);
update(index); //更新index到周围顶点的距离和前驱顶点
int adjoinPoint;
for (int i = 1; i < vertex.length; i++) {
adjoinPoint = vv.updateArr(); //选择并返回新的访问顶点
update(adjoinPoint);
}
vv.show();
}
//更新index下标顶点到周围顶点的距离和周围顶点的前驱节点
private void update (int index) {
int len = 0;
//遍历邻接矩阵的 index顶点跟其他顶点的关系
for (int j = 0; j < matrix[index].length; j++) {
//已经有的距离+现在新的距离,为了处理 G->A->D 的情况,因为G不能直接到D,所以经过A再到D,要加上A->D的距离
len = vv.getDis(index) + matrix[index][j];
//经过扫描会出现G->A->D 但是实际上,存在G->D的最短路径,
//如果j顶点没有被访问过,而且len小于出发顶点到j顶点的距离,据需要更新
if (!vv.in(j) && len < vv.getDis(j)) {
vv.updatePre(j, index); //更新j顶点的前驱节点为index节点
vv.updateDis(j, len); //更新出发顶点到j顶点的距离
}
}
}
}
- 顶点类
class VisitedVertex {
public int[] already_arr; //记录顶点是否访问过
public int[] pre_visited; //记录下标对应的值的前一个顶点下标
public int[] dis; //记录从顶点出发到其他所有顶点的距离,动态变化
/**
* @param length 顶点的个数
* @param index 出发顶点对应的下标
*/
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;
//访问自己为0
this.dis[index] = 0;
}
//是否被访问过
public boolean in(int index) {
return already_arr[index] == 1;
}
//更新出发顶点到index节点的距离
public void updateDis(int index, int len) {
dis[index] = len;
}
//设置节点pre的前驱节点为index
public void updatePre(int pre, int index) {
pre_visited[pre] = index;
}
//返回出发顶点到index顶点的距离
public int getDis(int index) {
return dis[index];
}
//继续选择并返回新的访问顶点,访问好G之后,将A作为新的访问顶点
public int updateArr() {
int min = 65535;
int 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() {
System.out.println(Arrays.toString(already_arr));
System.out.println(Arrays.toString(pre_visited));
System.out.println(Arrays.toString(dis));
}
}
解析
- 下面的解析围绕,核心方法解释
public void dsj (int index) {
vv = new VisitedVertex(vertex.length, index);
update(index); //更新index到周围顶点的距离和前驱顶点
int adjoinPoint;
for (int i = 1; i < vertex.length; i++) {
adjoinPoint = vv.updateArr(); //选择并返回新的访问顶点
update(adjoinPoint);
}
vv.show();
}
//继续选择并返回新的访问顶点,访问好G之后,将A作为新的访问顶点
public int updateArr() {
int min = 65535;
int 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;
}
vv
对象针对的是index起点
,也就是说初始化完成之后,vv对象中的dis
数组要针对index
,这个数组的意思,从index
出发到其他点的距离,比如dis[0]
的意思就是,从index->下标为0的距离,不管中间经过了多少个点,保存的就是最小路径,而这个最小路径是经过多重判断的,下面会进行解析
第一步,
update(index);
,扫描index并记录其直接邻接点的距离。len = vv.getDis(index) + matrix[index][j];
,此时循环遍历index周边的每一个点,并获取其距离,vv.getDis(index)
这个值显然为0,因为传入的index
就是起点本身,那么就为0。经过这一轮判断后,非邻接点都为65535,而邻接点都有距离值
第二步,扫描
index
的邻接点,并将邻接点作为下一层进行扫描。updateArr
方法中,通过already_arr[i] == 0 && dis[i] < min
判断,首先这个点是没有被访问过的,结合广度遍历的思想,dis[i]<min
保证这个点是可以被访问过的点,可以访问的点指的是是min的初始值为65535
,一旦小于65535即表示,这个点是被访问过的点,这个地方结合第三步进行解释。那么获取了一个点,就可以进行index
邻接点的扫描了,判断这个邻接点的相邻个点与index
的关系,此时len = vv.getDis(index) + matrix[index][j];
中,vv.getDis(index)
由于参数名的问题,其实是邻接点的距离,那么意思就是index与邻接点的距离加上+邻接点与邻接点的邻接点的距离
。假如最开始的index为G
,而G只与A和B相邻
,那么第二步扫描的就是A、B的邻接点
,假如此时通过A
扫描到C
,那么此时G-A-C
的距离就是vv.getDis(A)(G到A的距离了)+matrix[A][C](A与C的距离)
的距离,那么最重要的是,此时dis[C]也有了值,且值<65535
,dis数组
最开始均被初始化为65535
第三步,扫描
index
的邻接点的邻接点,那么这里就可以解释,为什么dis[i]<min
保证这个点是可以被访问过的点,因为在第二步中,已经通过index的邻接点
计算出了到邻接点的邻接点
的距离了,此时就可以证明这个点是被遍历过的
最小路径的多重判断,算法中对最小路径进行了两次判断。这里进行举例说明,假如现在的目标是找出
G-A,G只能访问A和C
的最小路径,有两条路G-A=4
,G-C=1 C-A=1
,经过第一步的判断得出dis[A]=4 dis[C]=1
,接下来进行第二步判断,在updateArr
方法中,for循环中有一层if判断dis[i] <min
,选出了目前dis数组
中最小路径的那个点,那么自然找到了C
,那么进行第二步扫描C
点,发现此时len=2<dis[A]=4
,自然就替换了原来的dis[A]
,每次获取新的访问节点都取最小的,保证了后面获取的总长度是最小的,再通过遍历新的访问节点的节点,可以更新上一层遍历中比较大的值,类似处理G-C-A替换G-A的过程
;且遍历完C
后,将C
标记为以访问,不可再次访问或者修改,这里为的是不让遍历走回头路
,否则如果没有这个标记,其他点就会扫描回来
所以,
updateArr
的for循环
保证了每一次新的访问节点都是距index
最近的点,这个点距index
就是最近的,在update
中并不改变这个点距离index的距离,只通过此时的点,更新index到个点的距离
,那么,这里就有一个问题,为什么这样就能保证这个点此时是最近的点,经过几轮后仍是呢,这里举例说明,假设G-A=4 G-C-B-A=3
,这也就是意味着最开始dis[A]=10 dis[C]=1
,但是第二步开始判断时,只能是C作为新的访问节点
,因为要选出距index最近的点
,而后续扫描出B
,也只能将B作为新的访问节点
,最后得出dis[A]=3
,接着扫描A
,但是此时G-A=4
,并不能替换原来的dis[A]
;第二个例子,G-A=4 G-C=5
,下一个要访问的点显然是A
,G-A比G-C小
,所以无论怎么访问,G-A
永远是从G出发到A的最短路径,因此可以标记A为已访问。
最短路径问题(弗洛伊德算法)
- 各个顶点之间的最短路径
- 调用
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, N, 7, N, N, N, N};
matrix[1] = new int[]{N, 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[]{N, 3, N, N, 4, 6, 0};
Graph graph = new Graph(vertex.length, matrix, vertex);
graph.showGraph();
graph.floyd();
graph.showGraph();
- 图
class Graph {
private char[] vertex; //存放顶点数组;
private int[][] dis; //保存从各个顶点出发到其他顶点的距离
private int[][] pre; //保存到达目标顶点的前驱顶点
public void floyd() {
int len = 0; //变量保存距离
//k对中间顶点的遍历,就是中间顶点的下标
for (int k = 0; k < vertex.length; k++) {
//从i顶点开始出发
for (int i = 0; i < vertex.length; i++) {
//到达j终点
for (int j = 0; j < vertex.length; j++) {
len = dis[i][k] + dis[k][j]; //求出i-k k-j
if (len < dis[i][j]) { //小于直连的距离
dis[i][j] = len;
//pre[k][j] 第k行第j列的元素最开始是其本身
pre[i][j] = pre[k][j]; //更新前驱节点
}
}
}
}
}
/**
* @param length 大小
* @param martix 邻接矩阵
* @param vertex 顶点数组
*/
public Graph(int length, int[][] martix, char[] vertex) {
this.vertex = vertex;
this.dis = martix;
this.pre = new int[length][length];
//初始化pre数组,没有处理之前,各个顶点的前驱节点都是其自身
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
public void showGraph() {
for (int i = 0; i < vertex.length; i++) {
for (int j = 0; j < vertex.length; j++) {
System.out.print(vertex[i] + "-" + vertex[pre[i][j]] + "-" + vertex[j] + " ");
}
System.out.println();
}
System.out.println("------------------");
for (int i = 0; i < vertex.length; i++) {
for (int j = 0; j < vertex.length; j++) {
System.out.print(vertex[i] + "-" + vertex[pre[i][j]] + "-" + vertex[j] + "-" + dis[i][j] + " ");
}
System.out.println();
}
System.out.println("------------------");
}
}
小结
三层循环,由外到内分别为
中间顶点
,出发顶点
,结束顶点
,并且只能是这个顺序,例如如果B-A的路径只有B-G-E-C-A
,此时如果最外层循环为出发顶点
,遍历顺序则为B-B-G B-C-A B-E-C B-G-A B-G-C
,按照这个顺序,永远无法找到B-A的路径
最外层为中间顶点
,保证了某一出发顶点到达结束顶点的路径都是能被扫描出来的
。假如A-C的路径只有A-G-B-D-F-E-C
,顺序扫描为A-A-G G-B-D G-D-F F-E-C G-F-C
,最后可以得出A-G-C
,也就是说无论怎么样的路径,只要是让中间顶点在最外层
,让起点和终点
充分遍历,就可以找到任意点之间的通路,并通过切换中间顶点
进行连接
pre[i][j] = pre[k][j];
意思是,将后半部分的前驱节点,作为新的前驱节点