面试题98:
问题:
一个机器人从m x n的格子的左上角出发,它每一步只能向下走或者向右走,计算机器人从左上角到达右下角的路径数量。
解决方案:
- 机器人每走一步都有两个选择,要么向下走要么向右走。一个任务需要多个步骤才能完成,每步面临若干选择,这类问题看起来可以用回溯法解决,但由于这个题目只要求计算从左上角到达右下角的路径的数目,并没有要求列出所有的路径,因此这个问题更适合用动态规划解决。
- 当i等于0或者j等于0时,机器人位于格子最左边的一列,机器人走到左下角或者右上角只能走直线,故f(0,0)走到f(i,0)或者f(0,j)都是只有一条路径。
- 当行号i、列号j都大于0时,机器人有两种方法可以到达坐标为(i,j)的位置。它既可以从坐标为(i-1,j)的位置向下走一步,也可以从坐标为(i,j-1)的位置向右走一步,因此,f(i,j)等于 f(i-1,j)与f(i,j-1)之和。
源代码(递归):
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
return dfs(m-1,n-1,dp);
}
private int dfs(int i,int j,int[][] dp){
if(dp[i][j] == 0){
if(i == 0 || j == 0){
dp[i][j] = 1;
}else{
dp[i][j] = dfs(i-1,j,dp) + dfs(i,j-1,dp);
}
}
return dp[i][j];
}
}
源代码(迭代):
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
dp[0][0] = 0;
for(int j = 0;j < n;j++){
dp[0][j] = 1;
}
for(int i = 1;i < m;i++){
dp[i][0] = 1;
for(int j = 1;j < n;j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
优化空间效率思路:
在计算f(i,j)时需要用到f(i-1,j)和f(i,j-1)的值。接下来在计算f(i,j+1)时需要用到f(i-1,j+1)和f(i,j)的值。由于在用f(i-1,j)计算出f(i,j)之后就不再需要f(i-1,j),因此可以只用一个位置来保存f(i-1,j)和f(i,j)的值。
源代码:
class Solution {
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp,1);
for(int i = 1;i < m;i++){
for(int j = 1;j < n;j++){
dp[j] += dp[j-1];
}
}
return dp[n-1];
}
}
面试题99:
问题:
在一个m x n的格子中,每个位置都有一个数字。机器人每步只能向下或向右,计算它从格子左上角到右下角的路径数字之和的最小值。
解决方案:
- 机器人每一步都有两种选择,要么向下走,要么向右走,但是题目并没有让我们计算出所有的路径,而是让我们计算路径的最小数字之和,故使用动态规划。
- 用函数f(i,j)表示从格子的左上角坐标为(0,0)的位置(用grid[0][0]表示)出发到达坐标为(i,j)的位置(用grid[i][j]表示)的路径的数字之和的最小值。如果格子的大小为m×n,那么f(m-1,n-1)就是问题的解。
- 当i等于0或者j等于0时,机器人位于格子最上面的一行,机器人只能向下或者向右,如果它想去左下角或者右上角,那么它只能走直线,f(i,0)就等于0到i的数字之和,f(0,j)就等于0到j的数字之和。
- 当行号i、列号j都大于0时,机器人有两种方法可以到达坐标为(i,j)的位置。它既可以从坐标为(i-1,j)的位置向下走一步,也可以从坐标为(i,j-1)的位置向右走一步,因此,f(i,j)等于 f(i-1,j)与f(i,j-1)的最小值加上grid[i][j]。
源代码:
class Solution {
public int minPathSum(int[][] grid) {
int len1 = grid.length;
int len2 = grid[0].length;
int[][] dp = new int[len1][len2];
dp[0][0] = grid[0][0];
for(int j = 1;j < len2;j++){
dp[0][j] = dp[0][j-1] + grid[0][j];
}
for(int i = 1;i < len1;i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
for(int j = 1;j < len2;j++){
dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1]) + grid[i][j];
}
}
return dp[len1-1][len2-1];
}
}
优化空间效率思路:
在计算f(i,j)时需要f(i-1,j)、f(i,j-1)的值。值得注意的是,f(i-1,j)在完成f(i,j)的计算之后再也用不到了,因此将f(i-1,j)和f(i,j)保存到同一个数组dp的同一个位置“dp[j]”中。而f(i,j-1)在计算f(i,j)之前就已经计算好了。
源代码:
class Solution {
public int minPathSum(int[][] grid) {
int len1 = grid.length;
int len2 = grid[0].length;
int[] dp = new int[len2];
dp[0] = grid[0][0];
for(int j = 1;j < len2;j++){
dp[j] = dp[j-1] + grid[0][j];
}
for(int i = 1;i < len1;i++){
//注意:这里与上题不一样,因为上题是求路径数目,dp【i】与dp【j】相同都是1,使用上题不需要这个步骤。而这题不同,求的是路径和,故需要进行grid[i][0]的累加操作。
dp[0] += grid[i][0];
for(int j = 1;j < len2;j++){
dp[j] = Math.min(dp[j],dp[j-1]) + grid[i][j];
}
}
return dp[len2-1];
}
}
面试题100:
问题:
在一个由数字组成的三角形中,第1个行有1个数字,第2行有2个数字,以此类推,第n行有n个数字。每步只能前往下一行中相邻的数字,计算从三角形顶部到底部的路径经过的数字之和的最小值。
解决方案:
- 如果一个三角形有多行,那么从它的顶部到底部需要多步,而且每步都面临两个选择。但是题目没要求我们写出从三角形顶部到底部的全部路径,而是让我们求三角形顶部到底部的最小路径之和,故使用动态规划解决该问题。
- 可以用f(i,j)表示从三角形的顶部出发到达行号和列号分别为i和j(i≥j)的位置时路径数字之和的最小值,同时用T[i][j]表示三角形行号和列号分别为i和j的数字。如果三角形中包含n行数字,那么 f(n-1,j)的最小值就是整个问题的最优解。
- 如果j等于0,也就是当前到达某行的第1个数字。由于路径的每步都是前往正下方或右下方的数字,而此时当前位置的左上方没有数字,那么前一步是一定来自它的正上方的数字,因此f(i,0)等于 f(i-1,0)与T[i][0]之和。如果i等于j,也就是当前到达某行的最后一个数字,此时它的正上方没有数字,前一步只能是来自它左上方的数字,因此f(i,i)等于f(i-1,i-1)与T[i][i]之和。
- 如果当前行号和列号分别为i和j的位置位于某行的中间,那么前一步既可能是来自它正上方的数字(行号和列号分别为i-1和j),也可能是来自它左上方的数字(行号和列号分别为i-1和j-1),所以 f(i,j)等于f(i-1,j)与f(i-1,j-1)的最小值再加上T[i][j]。
源代码:
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int len = triangle.size();
int[][] dp = new int[len][len];
dp[0][0] = triangle.get(0).get(0);
for(int i = 1;i < len;i++){
dp[i][0] = dp[i-1][0] + triangle.get(i).get(0);
}
for(int i = 1;i < len;i++){
for(int j = 1;j <= i;j++){
if(j == i){
dp[i][j] = dp[i-1][j-1] + triangle.get(i).get(j);
}else{
dp[i][j] = Math.min(dp[i-1][j],dp[i-1][j-1]) + triangle.get(i).get(j);
}
}
}
int min = Integer.MAX_VALUE;
for(int j = 0;j < len;j++){
min = Math.min(min,dp[len-1][j]);
}
return min;
}
}
优化空间效率思路:
- 在计算f(i,j)之前“dp[j]”中保存的是f(i-1,j)的值。在计算f(i,j)时需要f(i-1,j-1)和f(i-1,j)。在计算完 f(i,j)之后能否用f(i,j)的值覆盖保存在“dp[j]”中的f(i-1,j)取决于是否还需要f(i-1,j)的值。
- 如果从左到右计算的话,计算f(i,j)后会覆盖f(i-1,j),但是后面计算f(i,j+1)的时候还需要用到f(i-1,j),由于计算f(i,j)时并不依赖同一行左侧的f(i,j-1),所以我们可以按照从右到左的顺序计算。
源代码:
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int len = triangle.size();
int[] dp = new int[len];
dp[0] = triangle.get(0).get(0);
for(int i = 1;i < len;i++){
for(int j = i;j >= 0;j--){
if(j == 0){
dp[j] += triangle.get(i).get(j);
}else if(j == i){
dp[j] = dp[j-1] + triangle.get(i).get(j);
}else{
dp[j] = Math.min(dp[j],dp[j-1]) + triangle.get(i).get(j);
}
}
}
int min = Integer.MAX_VALUE;
for(int j = 0;j < len;j++){
min = Math.min(min,dp[j]);
}
return min;
}
}