题目描述
有一辆汽车需要从 m * n 的地图左上角(起点)开往地图的右下角(终点),去往每一个地区都需要消耗一定的油量,加油站可进行加油。
请你计算汽车确保从从起点到达终点时所需的最少初始油量。
说明:
- 智能汽车可以上下左右四个方向移动
- 地图上的数字取值是 0 或 -1 或 正整数:
- -1 :表示加油站,可以加满油,汽车的油箱容量最大为100;
- 0 :表示这个地区是障碍物,汽车不能通过
- 正整数:表示汽车走过这个地区的耗油量
- 如果汽车无论如何都无法到达终点,则返回 -1
输入描述
第一行为两个数字,M,N,表示地图的大小为 M * N
- 0 < M,N ≤ 200
后面一个 M * N 的矩阵,其中的值是 0 或 -1 或正整数,加油站的总数不超过 200 个
输出描述
如果汽车无论如何都无法到达终点,则返回 -1
如果汽车可以到达终点,则返回最少的初始油量
示例1
输入
2,2
10,20
30,40
输出
70
说明
行走的路线为:右→下
示例2
输入
4,4
10,30,30,20
30,30,-1,10
0,20,20,40
10,-1,30,40
输出
70
说明
行走的路线为:右→右→下→下→下→右
示例3
输入
4,5
10,0,30,-1,10
30,0,20,0,20
10,0,10,0,30
10,-1,30,0,10
输出
60
说明
行走的路线为:下→下→下→右→右→上→上→上→右→右→下→下→下
示例4
输入
4,4
10,30,30,20
30,30,20,10
10,20,10,40
10,20,30,40
输出
-1
说明
无论如何都无法到达终点
题解
package main.java.com.juc;
import java.util.*;
public class 智能驾驶 {
static final int MAX_POWER = 100;
static int n;
static int m;
static int[] walk = new int[]{1 , 0 , -1 , 0 , 1};
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String[] split = sc.nextLine().split(",");
n = Integer.parseInt(split[0]);
m = Integer.parseInt(split[1]);
int[][] map = new int[n][m];
for (int i = 0 ; i < n ; i ++){
String[] str = sc.nextLine().split(",");
for (int j = 0 ; j < m ; j ++){
map[i][j] = Integer.parseInt(str[j]);
}
}
int ans = binarySearch(map);
System.out.println(ans);
}
private static int binarySearch(int[][] map){
int left = 0;
int right = MAX_POWER;
int ans = -1;
while (left <= right){
int mid = (left + right) / 2;
if (bfs(mid , map)){
right = mid - 1;
ans = mid;
}else {
left = mid + 1;
}
}
return ans;
}
private static boolean bfs(int mid , int[][] map){
if (map[0][0] == 0) return false;
int[][] powers = new int[n][m];
for (int[] power : powers) {
Arrays.fill(power , -1);
}
powers[0][0] = map[0][0] == -1 ? MAX_POWER : mid - map[0][0];
PriorityQueue<int[]> queue = new PriorityQueue<>((a,b) -> b[2] - a[2]);
queue.offer(new int[]{0 , 0 , powers[0][0]});
while (!queue.isEmpty()){
int[] poll = queue.poll();
int row = poll[0];
int col = poll[1];
int power = poll[2];
if (row == n - 1 && col == m - 1) return true;
for (int i = 0 ; i < 4 ; i ++){
int newRow = row + walk[i];
int newCol = col + walk[i+1];
if (newRow >= 0 && newRow < n && newCol >= 0 && newCol < m && map[newRow][newCol] != 0){
int newPower = map[newRow][newCol] == -1 ? MAX_POWER : power - map[newRow][newCol];
if (newPower > powers[newRow][newCol]){
powers[newRow][newCol] = newPower;
queue.offer(new int[]{newRow , newCol , newPower});
}
}
}
}
return false;
}
}
算法
1、迪杰斯特拉算法
2、二分查找
3、广度优先搜索
为什么是Dijkstra算法?
- 边权为非负数:Dijkstra算法假设所有边的权重(在这个问题中是油量消耗)为非负数,题目中提供的消耗油量是正整数或加油站(
-1
,表示加油站的特殊情况),不会有负数。 - 单源最短路径:从起点(
(0, 0)
)到终点((m-1, n-1)
)的最短路径,这正是Dijkstra算法的目标。 - 最小油量路径:在这个问题中,“最少初始油量”相当于最短路径问题中的最短路径,Dijkstra算法用于找到从起点到终点的最小油量消耗路径。
-
目标是最少初始油量:
题目要求计算从起点到终点所需的最少初始油量,这可以看作是一个 最短路径问题,但与传统的最短路径问题不同的是,路径的代价(消耗)是“油量”,而且油量会受到不同区域的影响(例如加油站可以加满油)。 -
优先队列:
为了处理“最少油量”这一要求,优先队列(也叫最小堆)用于每次选择当前剩余油量最多的路径。这是 Dijkstra 算法的核心思想之一,即每次都选择当前最优的路径(在这里是油量最少的路径)来扩展搜索。 -
节点状态:
在 Dijkstra 算法中,状态表示的是每个节点的最短路径长度。在这个问题中,状态是每个位置的剩余油量,而通过使用优先队列,我们能保证每次处理的是剩余油量最多的节点,从而尽可能减少油量消耗。
结合二分查找
如果没有加油站,那么最小初始油量只需要计算走到最后一个区域时消耗的最小油量即可(即最小路径)。但有加油站就表明,如果经过加油站就相当于不再消耗初始油量,这样就不能仅仅是计算最小耗油量那么简单。
换一种思路,不去求最小耗油量,而是求最大剩余油量,如果汽车走到下一个区域剩油箱存留的油量越多,就越有可能走到最后的区域。如果起始为满油100来开始行驶,然后逐步递减油量,直至走不通为止,是一定可以找到最小初始油量的。
二分查找正是查找满足条件的数最常用的算法,因此可以再利用二分查找来进一步优化,降低时间复杂度。