JAVA-数据结构与算法-最短路径问题(迪杰斯特拉算法和弗洛伊德算法)

539 阅读5分钟

写在前面

区别

  • 迪杰斯特拉算法,通过选定的被访问顶点,求出从出发访问顶点到其他各个顶点的最短路径
  • 弗洛伊德算法,每个顶点都是出发访问点,求出每个顶点到其他顶点的最短路径

最短路径问题(迪杰斯特拉算法)

  • 用于计算某一个节点到其他节点的最短路径,主要特点是以起始点为中心向外层层扩展,运用到了广度优先搜索思想,直到扩展到终点为止
  • 调用
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]也有了值,且值<65535dis数组最开始均被初始化为65535

第三步,扫描index的邻接点的邻接点,那么这里就可以解释,为什么dis[i]<min保证这个点是可以被访问过的点,因为在第二步中,已经通过index的邻接点计算出了到邻接点的邻接点的距离了,此时就可以证明这个点是被遍历过的

最小路径的多重判断,算法中对最小路径进行了两次判断。这里进行举例说明,假如现在的目标是找出G-A,G只能访问A和C的最小路径,有两条路G-A=4G-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标记为以访问,不可再次访问或者修改,这里为的是不让遍历走回头路,否则如果没有这个标记,其他点就会扫描回来

所以,updateArrfor循环保证了每一次新的访问节点都是距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,下一个要访问的点显然是AG-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]; 意思是,将后半部分的前驱节点,作为新的前驱节点