最近在一次笔试中遇到这道题,这道题也是leetcode上的原题,但是由于自己刷题太少,最终没有解出这道题。所以记录一下这次惨痛的经历,题目如下:
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。
-2 (K) -3 3
-5 -10 1
10 30 -5 (P)
说明:
骑士的健康点数没有上限。
任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
不知道大家读懂这道题了吗?
大概的意思就是说:有一个矩阵,公主在右下角,骑士在左上角,骑士想要营救公主,每次只能向右或者向下走一格,每当走到一格时会加血也会扣血,但是必须要保证每走一步骑士的血量都是大于0的,否则就失败了,最后要求的就是骑士的最低初始血量。
首先想到的就是用回溯来解决。
回溯(暴力解决)
假设从一个简单的例子开始,比如我们有如下一个2 x 2 的矩阵:
-3 | -5 |
---|---|
-7 | -4 |
骑士想要拯救公主,有两条路可以走,-3 > -5 > -4 或则 -3 > -7 >-4
两条路消耗的血量分别为12和14,所以骑士最小耗费生命值就是12
当骑士在-3位置时,摆在他面前的就两条路可以选择,要么选择-5,要么选择-7,由于无论走那条路终点都是-4,所以只要在-5和-7中选择一个扣血最少的,就能求出骑士的最小初始值。
总结下来就是:
从骑士起始点的右边和起始点的下边,两者之中选择一个耗费生命较小的,然后算上本身这个格子所需要的血量,就是最小的耗费生命值了。
想要得到骑士的最小生命值就要知道起始点的右边格子的最优解,
而右边的格子的的最优值就是:以右边的格子为起始点,继续执行。
而下边的格子的的最优值就是:以下边的格子为起始点,继续执行。
相信到目前位置,已经有一个递归的模糊概念了。
下面推导一下公式:
设f(i,j) 表示骑士需要耗费的最小的生命值,i、j表示横纵坐标
f(0,0)+array[0][0] = min(f(0,1),f(1,0))
想要求f(0,0) 就要求出其右边和下边,再取最小值
f(0,1)+array[0][1]=f(1,1) //只剩最后一格,所以是f(1,1)
f(1,0)+array[1][0]=f(1,1) //只剩最后一格,所以是f(1,1)
已知f(1,1)=4
f(0,1) = f(1,1) - array[0][1] = 4 - (-5) = 9
f(1,0) = f(1,1) - array[1][0] = 4- (-7) = 11
f(0,0) = min(f(0,1),f(1,0)) - array[0][0]=9 - (-3) = 12
当然了,要确保骑士活着,要在结果加1,才是骑士的最低血量。
再把i、j添加到公式里,得到如下公式:
f(i,j) = min(f(i,j+1),f(i+1,j)) - array[i][j]
相信到这里大家已经知道递归该怎么写了,但是还缺少递归的退出条件
下面推导一下退出条件:
当矩阵的右下角也就是公主所在的位置,例子中的值为-4,意思是只要骑士走到这里时,只要他的血量是4,那么就可以救出公主。有人会说那骑士的血量就变成0,那他不就死了嘛。就如刚才所说,我们最后会再加上1,保证骑士是活着的。
那如果最后的格子是大于0的数呢?假设最后一格的数字为3,那么只要保证骑士到最后一格的血量为0,那么就可以救出公主。
递归的退出条件:
if(arr[rowSize-1][colSize-1]>=0){
return 0;
}else{
return -arr[rowSize-1][colSize-1];
}
完整的代码如下:
public class SavePrincess {
public static void main(String[] args) {
int[][] array = {{-3, -5}, {-7, -4}};
System.out.println("地图为:");
for (int row[] : array) {
for (int i : row) {
System.out.printf("%s\t", i);
}
System.out.println("");
}
System.out.println("----------------------");
int res = dnf(array, 0, 0) + 1;
System.out.println("骑士的最小初始值为:" + res);
}
static int rowSize = 0;
static int colSize = 0;
public static int dnf(int[][] array, int rowIndex, int colIndex) {
rowSize = array.length;
colSize = array[0].length;
if (rowIndex >= rowSize || colIndex >= colSize) {
return Integer.MAX_VALUE;
}
//退出条件
if (rowIndex == rowSize - 1 && colIndex == colSize - 1) {
if (array[rowIndex][colIndex] >= 0) {
// 如果最后一个大于等于0,就返还0。
return 0;
} else {
//如果最后一个小于零,就返回负的值。
return -array[rowIndex][colIndex];
}
}
//求右边最优解
int right = dnf(array, rowIndex, colIndex + 1);
//求下边最优解
int down = dnf(array, rowIndex + 1, colIndex);
//求出骑士最小初始值(f(i,j) = min(f(i,j+1),f(i+1,j)) - array[i][j])
int needMin = Math.min(right, down) - array[rowIndex][colIndex];
//不允许骑士的血量小于零
int blood;
if (needMin < 0) {
blood = 0;
} else {
blood = needMin;
}
System.out.printf("row:%d\tcol:%d\tblood:%d", rowIndex, colIndex, blood);
System.out.println();
return blood;
}
}
执行结果如下:
骑士的最小初始值为:13
与我们手动推导的结果一致。
下面再把矩阵改为题干中的矩阵进行校验:
直接结果如下:
迷宫地图为:
-2 -3 3
-5 -10 1
10 30 -5
----------------------
row:1 col:2 res:4
row:0 col:2 res:1
row:1 col:2 res:4
row:2 col:1 res:0
row:1 col:1 res:10
row:0 col:1 res:4
row:1 col:2 res:4
row:2 col:1 res:0
row:1 col:1 res:10
row:2 col:1 res:0
row:2 col:0 res:0
row:1 col:0 res:5
row:0 col:0 res:6
骑士的最小初始值为:7
答案正确。
递归优化
从上面的输出结果可以看出,出现了很多的重复记录,所以为了避免遍历重复的记录,我们可以创建一个数组把这些结果记录下来,这样每次进行递归的时候就先判断所求的值是否存在,如果存在直接获取。
详细代码如下:
public class SavePrincess {
static int rowSize = 0;
static int colSize = 0;
public static void main(String[] args) {
int[][] array = {{-2, -3, 3}, {-5, -10, 1}, {10, 30, -5}};
System.out.println("迷宫地图为:");
for (int row[] : array) {
for (int i : row) {
System.out.printf("%s\t", i);
}
System.out.println("");
}
rowSize = array.length;
colSize = array[0].length;
System.out.println("----------------------");
//递归优化,去除重复值
int[][] memory = new int[rowSize][colSize];
// 初始化为-1。
for (int i = 0; i < rowSize; ++i) {
for (int j = 0; j < colSize; ++j) {
memory[i][j] = -1;
}
}
int res = dnf(array, memory, 0, 0) + 1;
System.out.println("骑士的最小初始值为:" + res);
}
public static int dnf(int[][] array, int[][] memory, int rowIndex, int colIndex) {
if (rowIndex >= rowSize || colIndex >= colSize) {
return Integer.MAX_VALUE;
}
//如果值已经存在,那么直接返回该值
if (memory[rowIndex][colIndex] != -1) {
return memory[rowIndex][colIndex];
}
//退出条件
if (rowIndex == rowSize - 1 && colIndex == colSize - 1) {
if (array[rowIndex][colIndex] >= 0) {
// 如果最后一个大于等于0,就返还0。
return 0;
} else {
//如果最后一个小于零,就返回负的值。
return -array[rowIndex][colIndex];
}
}
//求右边最优解
int right = dnf(array, memory, rowIndex, colIndex + 1);
//求下边最优解
int down = dnf(array, memory, rowIndex + 1, colIndex);
//求出骑士最小初始值(f(i,j) = min(f(i,j+1),f(i+1,j)) - array[i][j])
int needMin = Math.min(right, down) - array[rowIndex][colIndex];
//不允许骑士的血量小于零
int res;
if (needMin < 0) {
res = 0;
} else {
res = needMin;
}
//将当前值存入数组
memory[rowIndex][colIndex] = res;
System.out.printf("row:%d\tcol:%d\tres:%d", rowIndex, colIndex, res);
System.out.println();
return res;
}
}