如果矩阵是 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
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];
}
最下方的矩阵为最终生成的到每个节点的最短路径
如果用另一种图的存储方式来求解的话,但是时间和空间复杂度与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];
}
针对上述解法,最外层循环因为是要对所有节点进行遍历去考察最短路径,所以在这里无法进行优化,主要针对内层每次寻找当前最短路径节点的方法进行优化,由于每一次都是要得到最短路径,因此可以选择用最小堆(优先队列)来保存当前节点,而最小堆的插入和删除都是 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];
}
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(){}
}
图中是从矩阵左上角到右下角的最短路径,下方的矩阵为最终生成的到每个节点的最短路径。
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];
}
如果将矩阵中的每个位置当做一个状态,并且将依次能找到的最短路径当做时间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;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();
}
}
}