最短路径
问题
- 对于如下的图来说,每一个“∙”代表一个节点,节点与节点之间是他们之间相应的边权,由于这个图类似于矩阵的形式,所以当给定坐标(x1,y1)和(x2,y2)时,求这两个节点之间的最短路径。
- 在下面这个图中,最原始的图应该是每条边代表转移概率,这里将概率乘以10取整后得到,也就是每个节点相连边的权值和为10.
如何有效存储图?
- 最通常存储图的方式主要有两种,一种是邻接矩阵,邻接矩阵比较直观易懂,但是通常来说非常消耗空间;另一种是邻接表。
如果矩阵是m∗n阶的话,横向的边数为m∗(n−1),纵向的边数为(m−1)∗n,因此总共的边数为m∗(n−1)+(m−1)∗n=2mn−m−n,所以在这里至少需要O(2mn−m−n)=O(mn)的存储空间,如果使用邻接矩阵的形式存储的话则需要O(mn∗mn)=O(m2∗n2)的存储空间,但是这里的每个节点最多与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;i<m;i++) Arrays.fill(distance[i],Integer.MAX_VALUE);
distance[x1][y1] = 0;//起始点的最短路径为0
int x=0, y=0,min = Integer.MAX_VALUE;
for(int k=0;k<m*n;k++,min = Integer.MAX_VALUE){ //总共有m*n个节点,最坏情况下要循环m*n次
for(int i=0;i<m;i++){ //找到还未考察过的节点中的最短路径
for(int j=0;j<n;j++){
if(!done[i][j]&&distance[i][j]<min){
x = i;
y = j;
min = 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];
}
调试结果如下:
最下方的矩阵为最终生成的到每个节点的最短路径- 如果矩阵m∗n阶,因为有N=m∗n个节点,因此这种解法的空间复杂度为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<m;i++){//这里是将图的存储方式换成节点列表的形式 for(int j=0;j<n;j++){ nodes[n*i+j][0] = 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;k++,min = Integer.MAX_VALUE){ //总共有m*n个节点,最坏情况下要循环m*n次 for(int i=0;i<m*n;i++){ //找到还未考察过的节点中的最短路径 if(!done[i]&&distance[i]<min){ x = i; min = 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]; }调试结果如:
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;i<m;i++) Arrays.fill(distance[i],Integer.MAX_VALUE); distance[x1][y1] = 0; int x=0, y=0; PriorityQueue<Node> pq = new PriorityQueue<Node>(10,new Comparator<Node>(){ 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(){} }- 调试结果如图:
图中是从矩阵左上角到右下角的最短路径,下方的矩阵为最终生成的到每个节点的最短路径。
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;i<m;i++){
for(int j=0;j<n;j++){
nodes[n*i+j][0] = 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<Node> pq = new PriorityQueue<Node>(10,new Comparator<Node>(){
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];
}
- 调试结果如:
这里将最短路径的矩阵用一维数组表示。
维特比算法
如果将矩阵中的每个位置当做一个状态,并且将依次能找到的最短路径当做时间t,则可以如下表示:
- 初始状态,时刻t=0:δt(i)={Integer.MAX_VALUE,0,如果x不是起始点如果x是起始点
- 对于时刻t≥1有:
δt(i)=min1≤j≤N[δt−1(j)+aji],i=1,2,...,N;t=1,2,... - 循环第二步,直到时刻t找到的最短路径为所求节点,终止算法运行。
对于算法中的第二步,要求状态j到i的转移,通常来说是要针对所有其他节点到状态i的转移,也就是min1≤j≤N,但是在这里实际只需要针对每个状态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;i<m;i++){
for(int j=0;j<n;j++){
if(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((j<n-1?(rows[i][j]<=0):false)||(i<m-1?(cols[i][j]<=0):false)){
flag = false;
break;
}
flag = true;
}
if(!flag) break;//跳出多层循环
}
if(flag) break;
}
for(int i=0;flag&&i<m;i++){//将测试用例打印出来
for(int j=0;j<n;j++) System.out.print("*" + (j<n-1?(" -"+rows[i][j]+"- "):"\n"));
for(int j=0;i<m-1&&j<n;j++) System.out.print("|" + (j<n-1 ? (" "):"\n"));
for(int j=0;i<m-1&&j<n;j++) System.out.print(cols[i][j] + (j<n-1 ? (" "):"\n"));
for(int j=0;i<m-1&&j<n;j++) System.out.print("|" + (j<n-1 ? (" "):"\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<m;i++) System.out.println(Arrays.toString(result[i]));
System.out.println();*/
int[] result = sl.getMinPath4(rows,cols,x1,y1,x2,y2);//在调试的时候程序输出的是整个距离矩阵
System.out.println(Arrays.toString(result));
System.out.println();
}
}
}
735

被折叠的 条评论
为什么被折叠?



