【动归】B007_三角形最小路径和(递归 | 记忆化搜索 | 二维 dp | 一维 dp)

一、题目描述

Given a triangle, find the minimum path sum from top to bottom. 
Each step you may move to adjacent numbers on the row below.

For example, given the following triangle
[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]
The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).

Note:
Bonus point if you are able to do this using only O(n) extra space,
 where n is the total number of rows in the triangle.

二、题解

方法一:递归

递归三部曲:

  • 结束时机:到达最后一层的某个点,自然返回该点权值。
  • 本层递归的责任:寻找向下走和向右下走的最小权值。
  • 返回值:本层格子的权值 + 上述最小权值。
int R;
public int minimumTotal(List<List<Integer>> triangle) {
  R = triangle.size();
  return cost(0, 0, triangle);
}
private int cost(int level, int col, List<List<Integer>> tri) {
  if (level == R - 1)
    return tri.get(level).get(col);

  int goDown =  cost(level + 1, col, tri);
  int goRight = cost(level + 1, col + 1, tri);
  return tri.get(level).get(col) + Math.min(goDown, goRight);
}

复杂度分析

  • 时间复杂度: O ( 2 N ) O(2^N) ,N 为三角形的格子数量。
  • 空间复杂度: O ( N ) O(N) ,每一个格子,就是每一个递归树的结点,每个结点都会有两个分叉。

方法二:记忆化搜索

还是递归,只不过用了一个二维的 memo 数组来记录每一个格子的最小权值。进而减少无用的重复计算。

int R;
int[][] memo = null;
public int minimumTotal(List<List<Integer>> triangle) {
  R = triangle.size();
  memo = new int[R][R];
  return search(0, 0, triangle);
}
private int search(int level, int col, List<List<Integer>> tri) {

  if (level == R - 1)
    return tri.get(level).get(col);

  if (memo[level][col] == 0) {
    memo[level][col] = tri.get(level).get(col) +
            Math.min(search(level + 1, col, tri), search(level + 1, col + 1, tri));
  }
  return memo[level][col];
}

复杂度分析

  • 时间复杂度: O ( N 2 ) O(N^2) ,对于每一个格子只需遍历一遍。
  • 空间复杂度: O ( N × N ) O(N × N)

方法三:dp(自顶向下)

为了方便,使用缩写 tri 代替 triangle 了。还是先写出动态规划的三部曲:

  • 初始状态dp[0][0] = tri[0][0]
  • 状态定义dp[i][j] 的值代表直到走到 (i, j) 的最小路径和。
  • 转移方程
    • 起点(第二行)dp[i][j] = dp[i-1][j] + tri[i][j]
    • 每行的最后一列:即 i = j 时,dp[i][j] = dp[i-1][j-1] + tri[i][j]
    • 非三角形边界dp[i][j] = min(dp[i-1][j], dp[i-1][j-1]) + tri[i][j]
public int minimumTotal(List<List<Integer>> triangle) {
  int[][] dp = new int[R][R];
  int R = triangle.size();
  int C = triangle.get(R - 1).size();
  dp[0][0] = triangle.get(0).get(0);

  for (int i = 1; i < R; i++)
  for (int j = 0; j <= i; j++) {
    if (j == 0)     //第一行
      dp[i][j] = dp[i-1][j] + triangle.get(i).get(j);
    else if (i == j)//每行的最后一列
      dp[i][j] = dp[i-1][j-1] + triangle.get(i).get(j);
    else            //非边界
      dp[i][j] = Math.min(dp[i-1][j-1], dp[i-1][j]) + triangle.get(i).get(j);
  }

  final int k = R - 1;
  int min = Integer.MAX_VALUE;
  for (int i = 0; i < R; i++) {
    if (dp[k][i] < min)
      min = dp[k][i];
  }
  return min;
}

复杂度分析

  • 时间复杂度: O ( N 2 ) O(N^2) ,N 为三角形的格子数。
  • 空间复杂度: O ( 2 N ) O(2N)

附:空间压缩

如果理解了上面的代码,你会发现每次计算子问题时,都会用到 dp[i-1][j]dp[i-1][j-1] 而已,这完全可以用两个变量来将二维 dp 数组降到一维。

  • int dp_i1j 代表 dp[i-1][j]
  • int dp_i1j1 代表 dp[i-1][j-1]

你会发现对于每一行的每一次遍历只是列坐标在变化,所以这个做法是可行的。

public int minimumTotal2(List<List<Integer>> triangle) {

  int R = triangle.size();
  int C = triangle.get(R - 1).size();
  int[] dp = new int[R];
  dp[0] = triangle.get(0).get(0);

  int dp_i1j;     //dp[i-1][j]
  int dp_i1j1 = 0;//dp[i-1][j-1]
  for (int i = 1; i < R; i++)
  for (int j = 0; j <= i; j++) {
    dp_i1j = dp[j];
    if (j == 0)
      dp[j] = triangle.get(i).get(j) + dp_i1j;
    else if (i == j)
      dp[j] = triangle.get(i).get(j) + dp_i1j1;
    else
      dp[j] = Math.min(dp_i1j, dp_i1j1) + triangle.get(i).get(j);
    dp_i1j1 = dp_i1j;
  }

  int min = Integer.MAX_VALUE;
  for (int i = 0; i < R; i++) {
    if (dp[i] < min)
      min = dp[i];
  }
  return min;
}

复杂度分析

  • 时间复杂度: O ( N 2 ) O(N^2)
  • 空间复杂度: O ( N ) O(N)

方法四:dp(自底向上)

还是先写出动态规划的三部曲,dp 的方向代表行走的方向:

  • 初始状态dp[R-1][i] = tri[R-1][i],即 dp 的最后一行初始化为 tri 的最后一行。
  • 状态定义dp[i][j] 的值代表直到走到 (i, j) 的最小路径和。
  • 转移方程dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + tri[i][j]
public int minimumTotal3(List<List<Integer>> triangle) {
  int R = triangle.size();
  int C = triangle.get(R - 1).size();
  int[][] dp = new int[R][R];
  dp[0][0] = triangle.get(0).get(0);
  //初始化最后一行
  for (int i = 0; i < triangle.get(R - 1).size(); i++) {
    dp[R-1][i] = triangle.get(R-1).get(i);
  }
  for (int i = R - 2; i >= 0; i--) {
    List<Integer> oneRow = triangle.get(i);
    for (int j = 0; j < oneRow.size(); j++) {
      dp[i][j] = oneRow.get(j) + Math.min(dp[i+1][j+1], dp[i+1][j]);
    }
  }
  return dp[0][0];
}

复杂度分析

  • 时间复杂度: O ( N 2 ) O(N^2)
  • 空间复杂度: O ( 2 N ) O(2N)

附:空间压缩

dp[j] 中记录了求第 i (0<= i <=R-2)行时的最小路径总和。也是一样,遍历的每一行都是列下标在变化。

public int minimumTotal(List<List<Integer>> triangle) {
  int R = triangle.size();
  int C = triangle.get(R - 1).size();
  int[] dp = new int[R];
  for (int i = 0; i < triangle.get(R - 1).size(); i++) {
    dp[i] = triangle.get(R-1).get(i);
  }

  for (int i = R - 2; i >= 0; i--) {
    List<Integer> oneRow = triangle.get(i);
    for (int j = 0; j < oneRow.size(); j++) {
      dp[j] = Math.min(dp[j], dp[j+1]) + oneRow.get(j);
    }
  }
  return dp[0];
}

复杂度分析

  • 时间复杂度: O ( N 2 ) O(N^2)
  • 空间复杂度: O ( N ) O(N)
发布了461 篇原创文章 · 获赞 102 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_43539599/article/details/104582199