最短路径

最短路径

问题

  • 对于如下的图来说,每一个“ ”代表一个节点,节点与节点之间是他们之间相应的边权,由于这个图类似于矩阵的形式,所以当给定坐标 (x1,y1)(x2,y2) 时,求这两个节点之间的最短路径。
  • 在下面这个图中,最原始的图应该是每条边代表转移概率,这里将概率乘以10取整后得到,也就是每个节点相连边的权值和为10.

5445|||||51215|||||3331|||||23254|||||1111|||||75635|||||3225

如何有效存储图?

  • 最通常存储图的方式主要有两种,一种是邻接矩阵,邻接矩阵比较直观易懂,但是通常来说非常消耗空间;另一种是邻接表。
  • 如果矩阵是 mn 阶的话,横向的边数为 m(n1) ,纵向的边数为 (m1)n ,因此总共的边数为 m(n1)+(m1)n=2mnmn ,所以在这里至少需要 O(2mnmn)=O(mn) 的存储空间,如果使用邻接矩阵的形式存储的话则需要 O(mnmn)=O(m2n2) 的存储空间,但是这里的每个节点最多与4个节点相邻,也就是最多与4条边相连,所以邻接矩阵的空间消耗是非常大的。

    • 这里使用两种方式存储这种图。
    • 用两个矩阵分别存储水平方向的边和垂直方向的边,分别命名为rows和cols,并且为了避免重复,在水平方向上的边只存储每个节点往右的边,在垂直方向上只存储每个节点往下的边。这样,最后一列和最后一行实际上是不需要存储的,但是为了方便起见,存储0也无妨。因此上述的图有

      rows = 
              5 4 4 5 0
              3 3 3 1 0
              1 1 1 1 0
              3 2 2 5 0     
      
      cols = 
              5 1 2 1 5
              2 3 2 5 4
              5 1 2 1 5
              0 0 0 0 0
      
    • 另外一种存储方式为:将图看做一个有向图,将向右和向下的边当做出边,针对每个节点只存该节点的出边,于是有如下存储方式:

      nodes = 
              5 5
              4 1
              4 2
              5 1
              0 5
              ...
              0 0
      

求解最短路径

  • 因为每条边权值都为正,所以用Dijkstra算法求解。基本思路是,从起始点出发,不断寻找当前能找到的离起始点最短路径的节点,然后以该节点作为中间节点,考察是否可以更新以该节点为中间节点的其他节点的距离。是一种层层递进的思路,直到到达所求的节点。基于这种思路,有以下解法:

Solution1

public int getMinPath1(int[][] rows, int[][] cols, int x1, int y1, int x2, int y2){//求(x1, y1)和(x2, y2)之间的最短路径
    int m = rows.length;
    if(m==0) return -1;
    int n = rows[0].length;
    if(n==0||x1>m||x2>m||x1<0||x2<0||y1>n||y2>n||y1<0||y2<0) return -1;//这些都是非法情况
    boolean[][] done = new boolean[m][n];//记录该节点是否已经找到最短路径了
    int[][] distance = new int[m][n];//记录从起始点到该节点的当前最短路径
    for(int i=0;i0;//起始点的最短路径为0
    int x=0, y=0,min = Integer.MAX_VALUE;
    for(int k=0;k//总共有m*n个节点,最坏情况下要循环m*n次
        for(int i=0;i//找到还未考察过的节点中的最短路径
            for(int j=0;jif(!done[i][j]&&distance[i][j]if(x == x2 && y == y2) return distance[x2][y2];//已经找到所求节点的最短路径了,直接退出
        done[x][y] = true;
        if(x > 0) distance[x-1][y] = Math.min(distance[x-1][y], distance[x][y] + cols[x-1][y]);//往上走
        if(x < m-1) distance[x+1][y] = Math.min(distance[x+1][y], distance[x][y] + cols[x][y]);//往下走
        if(y > 0) distance[x][y-1] = Math.min(distance[x][y-1], distance[x][y] + rows[x][y-1]);//往左走
        if(y < n-1) distance[x][y+1] = Math.min(distance[x][y+1], distance[x][y] + rows[x][y]);//往右走
    }
    return distance[m-1][n-1];
}
  • 调试结果如下:
    最短路径_第1张图片

    最下方的矩阵为最终生成的到每个节点的最短路径
    
    • 如果矩阵 mn 阶,因为有 N=mn 个节点,因此这种解法的空间复杂度为 O(N) ;求解最短路径过程中,最外层for循环最坏情况下要对所有节点都遍历一次 ,因此是 O(N) ,里层的for循环在寻找当前最短路径的时候遍历了所有节点,时间复杂度也是 O(N) ,因此总的时间复杂度为 O(N2)

Solution2

  • 如果用另一种图的存储方式来求解的话,但是时间和空间复杂度与Solution1一样。 代码如下:

    public int getMinPath2(int[][] rows, int[][] cols, int x1, int y1, int x2, int y2){
        int m = rows.length;
        if(m==0) return -1;
        int n = rows[0].length;
        if(n==0||x1>m||x2>m||x1<0||x2<0||y1>n||y2>n||y1<0||y2<0) return -1;
        int[][] nodes = new int[m*n][2];//有两列,第一列为每个节点往右的边,第二列为往下的边
        for(int i=0;i//这里是将图的存储方式换成节点列表的形式
            for(int j=0;j0] = rows[i][j];
                nodes[n*i+j][1] = cols[i][j];
            }
        }
        boolean[] done = new boolean[m*n];
        int[] distance = new int[m*n];
        Arrays.fill(distance,Integer.MAX_VALUE);
        distance[x1*n+y1] = 0;
        int x=0, min = Integer.MAX_VALUE;
        for(int k=0;k//总共有m*n个节点,最坏情况下要循环m*n次
            for(int i=0;i//找到还未考察过的节点中的最短路径
                if(!done[i]&&distance[i]if(x == x2*n + y2) return distance[x];//已经找到最短路径了,直接退出
            done[x] = true;
            if(x/n > 0) distance[x-n] = Math.min(distance[x-n], distance[x] + nodes[x-n][1]);
            if(x/n < m-1) distance[x+n] = Math.min(distance[x+n], distance[x] + nodes[x][1]);
            if(x%n > 0) distance[x-1] = Math.min(distance[x-1], distance[x] + nodes[x-1][0]);
            if(x%n < n-1) distance[x+1] = Math.min(distance[x+1], distance[x] + nodes[x][0]);
        }
        return distance[m*n-1];
    }

    调试结果如:
    最短路径_第2张图片

Solution3

  • 针对上述解法,最外层循环因为是要对所有节点进行遍历去考察最短路径,所以在这里无法进行优化,主要针对内层每次寻找当前最短路径节点的方法进行优化,由于每一次都是要得到最短路径,因此可以选择用最小堆(优先队列)来保存当前节点,而最小堆的插入和删除都是 O(N) 的复杂度,因此可以将程序的时间复杂度优化到 O(NlogN) 。代码如下:

    
    public int getMinPath3(int[][] rows, int[][] cols, int x1, int y1, int x2, int y2){
        int m = rows.length;
        if(m==0) return -1;
        int n = rows[0].length;
        if(n==0||x1>m||x2>m||x1<0||x2<0||y1>n||y2>n||y1<0||y2<0) return -1;
        boolean[][] done = new boolean[m][n];
        int[][] distance = new int[m][n];
        for(int i=0;i0;
        int x=0, y=0;
        PriorityQueue pq = new PriorityQueue(10,new Comparator(){
            public int compare(Node n1, Node n2){
                return n1.val - n2.val;
            }
        });//这里定义了一个最小堆,使用node的val进行排序。因为加入最小堆的时候,除了带有路径值外还必须有节点的位置信息。所以这里定义了一个node类。详细见下面
        pq.add(new Node(0,x1,y1));
        while(pq.size()>0){//最坏情况下,一个节点会重复添加进去4次,所以这里O(4N)=O(N)
            Node node = pq.poll();//最小堆的添加和删除操作都是O(logN)
            x = node.x;
            y = node.y;
            if(done[x][y]) continue;//在这里需要done数组的原因在于一个节点的不同距离都存入了最小堆中,当前节点已经找到过最小距离时直接跳过
            //实际上,可以如此:if(distance[x][y]!=node.val) continue;这同样可以避免重复考察一个已经找到最短路径的节点,并且节省了done数组的空间
            if(x == x2 && y == y2) return distance[x2][y2];//已经找到最短路径了,直接退出
            done[x][y] = true;
            if(x > 0&&distance[x-1][y]>distance[x][y] + cols[x-1][y]){
                distance[x-1][y] = distance[x][y] + cols[x-1][y];
                pq.add(new Node(distance[x-1][y],x-1,y));
            }
            if(x < m-1&&distance[x+1][y]>distance[x][y] + cols[x][y]){
                distance[x+1][y] = distance[x][y] + cols[x][y];
                pq.add(new Node(distance[x+1][y],x+1,y));
            }
            if(y > 0&&distance[x][y-1]>distance[x][y] + rows[x][y-1]){
                distance[x][y-1] = distance[x][y] + rows[x][y-1];
                pq.add(new Node(distance[x][y-1],x,y-1));
            }
            if(y < n-1&&distance[x][y+1]>distance[x][y] + rows[x][y]){
                distance[x][y+1] = distance[x][y] + rows[x][y];
                pq.add(new Node(distance[x][y+1],x,y+1));
            }
        }
        return distance[m-1][n-1];
    } 
    • 下面是程序中用到的类Node:
    class Node{
    int val = 0;//该节点的当前最短路径
    int x = 0;//节点坐标
    int y = 0;
    Node(int val, int x, int y){this.val = val; this.x = x; this.y = y;}
    Node(){}
    }
    • 调试结果如图:

    最短路径_第3张图片

    图中是从矩阵左上角到右下角的最短路径,下方的矩阵为最终生成的到每个节点的最短路径。

Solution4

  • 如果使用第二种存储方式,依然利用优先队列的方式来求解的话则有:
public int getMinPath4(int[][] rows, int[][] cols, int x1, int y1, int x2, int y2){
    int m = rows.length;
    if(m==0) return -1;
    int n = rows[0].length;
    if(n==0||x1>m||x2>m||x1<0||x2<0||y1>n||y2>n||y1<0||y2<0) return -1; 

    int[][] nodes = new int[m*n][2];
    for(int i=0;ifor(int j=0;j0] = rows[i][j];
            nodes[n*i+j][1] = cols[i][j];
        }
    }
    boolean[] done = new boolean[m*n];
    int[] distance = new int[m*n];
    Arrays.fill(distance,Integer.MAX_VALUE);
    distance[x1*n+y1] = 0;
    int x=0;

    PriorityQueue pq = new PriorityQueue(10,new Comparator(){
        public int compare(Node n1, Node n2){
            return n1.val - n2.val;
        }
    }); 

    pq.add(new Node(0,x1*n+y1,-1));//初始状态加入最小堆
    while(pq.size()>0){ //总共有m*n个节点,最坏情况下要循环m*n次
        Node node = pq.poll();
        x = node.x;
        if(node.val!=distance[x]) continue;
        if(x == x2*n + y2) return distance[x];//已经找到最短路径了,直接退出
        done[x] = true;
        if(x/n > 0&&distance[x-n]>distance[x] + nodes[x-n][1]){
            distance[x-n] = distance[x] + nodes[x-n][1];
            pq.add(new Node(distance[x-n],x-n,-1));
        }
        if(x/n < m-1&&distance[x+n]>distance[x] + nodes[x][1]){
            distance[x+n] = distance[x] + nodes[x][1];
            pq.add(new Node(distance[x+n],x+n,-1));
        }
        if(x%n > 0&&distance[x-1]>distance[x] + nodes[x-1][0]){
            distance[x-1] = distance[x] + nodes[x-1][0];
            pq.add(new Node(distance[x-1],x-1,-1));
        }
        if(x%n < n-1&&distance[x+1]>distance[x] + nodes[x][0]){
            distance[x+1] = distance[x] + nodes[x][0];
            pq.add(new Node(distance[x+1],x+1,-1));
        }
    }
    return distance[m*n-1];
}
  • 调试结果如:
    最短路径_第4张图片
    这里将最短路径的矩阵用一维数组表示。

维特比算法

  • 如果将矩阵中的每个位置当做一个状态,并且将依次能找到的最短路径当做时间t,则可以如下表示:

    1. 初始状态,时刻 t=0 δt(i)={Integer.MAX_VALUE,0,xx
    2. 对于时刻 t1 有:
      δt(i)=min1jN[δt1(j)+aji],i=1,2,...,N;t=1,2,...
    3. 循环第二步,直到时刻t找到的最短路径为所求节点,终止算法运行。
  • 对于算法中的第二步,要求状态j到i的转移,通常来说是要针对所有其他节点到状态i的转移,也就是 min1jN ,但是在这里实际只需要针对每个状态i求最多的5个状态,包括每个节点周边的最多4个的邻节点和一个自身节点。

  • 根据上述思路实现的代码和上面的Dijkstra算法是一样的。

附录:

以下是测试代码,基本思路是针对每一组测试的(m,n)随机产生一个符合各节点边权值和为10的测试用例,有些情况下比如(3,3)是不可能产生符合这个条件的测试用例的,所以在尝试1000次还未能产生一个符合条件的用例情况下,跳过这个(m,n)对:

public static void main(String[] args){
    int[][] testCases = {{2,3},{3,2},{4,5},{3,5},{5,3},{6,4},{7,8},{8,10},{9,12},{11,13}};//每一对都是一组(m,n)
    Solution sl = new Solution();
    for(int[] arr:testCases){
        int m = arr[0];
        int n = arr[1];
        int[][] rows = new int[m][n];
        int[][] cols = new int[m][n];
        boolean flag = false;
        for(int k=0;k<1000;k++){//针对每组(m,n)尝试最多尝试1000次直到有符合条件的用例
            flag = false;
            for(int i=0;ifor(int j=0;jif(i==0&&j==0){
                        rows[i][j] = (int) (Math.random() * 7) + 2;
                        cols[i][j] = 10 - rows[i][j];
                        continue;
                    }
                    int range = 10 - ( j>0 ? rows[i][j-1] : 0 ) - ( i>0 ? cols[i-1][j] : 0 );
                    if( i < m-1 ) {
                        if( j < n-1 ) rows[i][j] = (int) (Math.random() * (range-1)) + 1;//随机产生[1,10)
                    }else rows[i][j] = range;
                    cols[i][j] = range - rows[i][j]; 
                    if((j1?(rows[i][j]<=0):false)||(i1?(cols[i][j]<=0):false)){
                        flag = false;
                        break;
                    } 
                    flag = true;
                }
                if(!flag) break;//跳出多层循环
            }
            if(flag) break;
        }
        for(int i=0;flag&&i//将测试用例打印出来
            for(int j=0;j"*" + (j1?(" -"+rows[i][j]+"- "):"\n"));
            for(int j=0;i1&&j"|" + (j1 ? ("     "):"\n"));
            for(int j=0;i1&&j1 ? ("     "):"\n"));
            for(int j=0;i1&&j"|" + (j1 ? ("     "):"\n"));
        }
        if(flag){
            //int x1= (int)(Math.random()*m),y1= (int)(Math.random()*n),x2= (int)(Math.random()*m),y2= (int)(Math.random()*n);//可以随机产生起始点和终止点
            int x1=0, y1=0, x2=m-1, y2=n-1;
            System.out.println("x1="+x1+", y1="+y1+";  x2= "+x2+",y2= "+y2);
            /*int[][] result = sl.getMinPath1(rows,cols,x1,y1,x2,y2);
            for(int i=0;i
            int[] result = sl.getMinPath4(rows,cols,x1,y1,x2,y2);//在调试的时候程序输出的是整个距离矩阵
            System.out.println(Arrays.toString(result));
            System.out.println();
        }
    }
}

你可能感兴趣的:(算法)