Dynamic programming

动态规划一般都是求最值,也是穷举的一个变种

斐波那契数组(从上到下)

这是从上到下的解法,从递归树的头部开始,递归时会发现斐波那契的地递归有很多重复值,使用一个memo就可以不进行重复的递归计算,直接取值降低时间复杂度

public int fib(int n) {
    int[] memo = new int[n + 1];
    return helper(memo, n);
}

int helper (int [] memo, int n){
    if(n == 0){  // base case
        return 0;
    }else if (n == 1 || n == 2){
        return 1;
    }
    
    if(memo[n] != 0){  //查memo
        return memo[n];
    }

    memo[n] = helper(memo, n - 1) + helper(memo, n - 2);
    return memo[n];
}

斐波那契数组(从下到上)

这是从下到上的解法,也就是从0开始用for循环到n,用一个dp数组记录每一个n的斐波那契值,这样dp[n]就是结果。

public int fib(int n) {
    if (n == 0) return 0;
    int[] dp = new int[n + 1];
    // base case
    dp[0] = 0; dp[1] = 1;

    for(int i = 2; i <= n; i++){
        dp[i] = dp[i - 1] + dp[i -2];
    }

    return dp[n];
}

斐波那契数组(最优解)

不管是从下到上还是从上到下,都是用空间换时间(用一个数组来储存之前的值),但是斐波那契数只与前两个值有关系,所以没必要存储整个列表,只要存前两个值动态更新就好了

public int fib(int n) {
    if (n == 0 || n == 1) return n;
    int n_1 = 0, n_2 = 1;
    
    for(int i = 2; i <= n; i++){
        int n_current = n_1 + n_2;
        n_1 = n_2;
        n_2 = n_current; 
    }

    return n_2;
}

凑零钱(从大往小)

使用memo,空间换时间

int[] memo;

int coinChange(int[] coins, int amount) {
    memo = new int[amount + 1];
    Arrays.fill(memo, -666);    // 备忘录初始化为一个不会被取到的特殊值,代表还未被计算
    return dp(coins, amount);
}

int dp(int[] coins, int amount) {
    if (amount == 0) return 0; 
    if (amount < 0) return -1; // 特殊情况
 
    if (memo[amount] != -666)   //如果不是-666说明计算重复了,直接返回memo内容
        return memo[amount];

    int res = Integer.MAX_VALUE;
    for (int coin : coins) {
        int subProblem = dp(coins, amount - coin);  // 子问题:选择了一个面值的硬币后,剩下的面值怎么用最少的硬币组合
        if (subProblem == -1) continue;              // 无解,跳过
        res = Math.min(res, subProblem + 1);        // 选择最优解,+1是因为当前这个foreach的coin也要算一个位置的
    }
    memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; // 计算结果存入memo,无解时返回-1
    return memo[amount];
}

凑零钱(dp数组迭代方式,从前往后)

int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1]; // n块钱的时候最少需要几个硬币
    Arrays.fill(dp, amount + 1);
    dp[0] = 0; // base case
   
    for (int i = 0; i < dp.length; i++) { //循环dp的所有可能取值(有100块就从0到100)
        for (int coin : coins) { // 循环尝试硬币的所有选择
            if (i - coin < 0) continue; //面值比需要的价格还大,无解,跳过
            dp[i] = Math.min(dp[i], 1 + dp[i - coin]); // 取最小值(最少的硬币数量)
            // i是几块钱,i-coin是查看使用了一个面值的硬币后,从前面查看剩下的面值最小需要几个硬币
        }
    }
    return (dp[amount] == amount + 1) ? -1 : dp[amount];
}

最长递增子序列

int lengthOfLIS(int[] nums) {
    int[] dp = new int[nums.length];// dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
    
    Arrays.fill(dp, 1); // base case:dp数组都为 1,因为每一个元素自己都起码算是一个递增序列
    for (int i = 0; i < nums.length; i++) { // 以每一个元素为开始
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) // 找出每一个小于当前的前序数
                dp[i] = Math.max(dp[i], dp[j] + 1); // 去其对应的dp数字里查出对应位置的的最长序列长度
        }
    }
    
    int res = 0;
    for (int i = 0; i < dp.length; i++) {
        res = Math.max(res, dp[i]);
    }
    return res;
}

俄罗斯套娃信封问题

跟最长递增子序列相似,首先,我们对宽边排序,这样保证后面的起码能塞进前一个的宽边(其实替换成高也行),然后就跟上一题一模一样了

public int maxEnvelopes(int[][] envelopes) { // envelopes = [[w, h], [w, h]...]
    int n = envelopes.length; 

    Arrays.sort(envelopes, new Comparator<int[]>() // 按宽度升序排列,如果宽度一样,则按高度降序排列
    {
        public int compare(int[] a, int[] b) {
            return a[0] == b[0] ? 
                b[1] - a[1] : a[0] - b[0];
        }
    });
    
    int[] height = new int[n]; // 提取高度
    for (int i = 0; i < n; i++)
        height[i] = envelopes[i][1];

    return lengthOfLIS(height); 对高度数组寻找 LIS
}

int lengthOfLIS(int[] nums) {
    // 见前文
}

最小编辑距离

// 备忘录
int[][] memo;
    
public int minDistance(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    // 备忘录初始化为特殊值,代表还未计算
    memo = new int[m][n];
    for (int[] row : memo) {
        Arrays.fill(row, -1);
    }
    return dp(s1, m - 1, s2, n - 1);
}

int dp(String s1, int i, String s2, int j) {
    if (i == -1) return j + 1;
    if (j == -1) return i + 1;
    // 查备忘录,避免重叠子问题
    if (memo[i][j] != -1) {
        return memo[i][j];
    }
    // 状态转移,结果存入备忘录
    if (s1.charAt(i) == s2.charAt(j)) {
        memo[i][j] = dp(s1, i - 1, s2, j - 1);
    } else {
        memo[i][j] =  min(
            dp(s1, i, s2, j - 1) + 1,  //插入
            dp(s1, i - 1, s2, j) + 1,  //删除
            dp(s1, i - 1, s2, j - 1) + 1  //交换
        );
    }
    return memo[i][j];
}

int min(int a, int b, int c) {
    return Math.min(a, Math.min(b, c));
}

最小下降路径(正下,左下,右下)

int minFallingPathSum(int[][] matrix) {
    int n = matrix.length;
    int res = Integer.MAX_VALUE;

    memo = new int[n][n];
    for (int i = 0; i < n; i++) {
        Arrays.fill(memo[i], 66666);// 备忘录里的值初始化为 66666
    }
    
    for (int j = 0; j < n; j++) {// 终点可能在 matrix最底部[n-1] 的任意一列,所以要尝试最后一行每一个的最小距离
        res = Math.min(res, dp(matrix, n - 1, j));
    }
    return res;
}

int[][] memo;

int dp(int[][] matrix, int i, int j) {
    
    if (i < 0 || j < 0 ||
        i >= matrix.length ||
        j >= matrix[0].length) {// 索引合法性检查
        return 99999;
    }
    
    if (i == 0) { // base case,i=0 代表已经回到了二维数组的最上面一行,其最小距离就是他自己
        return matrix[0][j];
    }

    if (memo[i][j] != 66666) { // memo节省运算时间
        return memo[i][j];
    }
    
    //memo[i][j] 是当前位置的最小下落距离,从memo默认的66666变成matrix当前位置的自身值加上前面的最小下落距离
    memo[i][j] = matrix[i][j] + min(
                                    dp(matrix, i - 1, j),       //正上方
                                    dp(matrix, i - 1, j - 1),   //左上
                                    dp(matrix, i - 1, j + 1)    //右上
                                );
    return memo[i][j];
}

int min(int a, int b, int c) {
    return Math.min(a, Math.min(b, c));
}

最小下降路径(只有正下和右下)

上一题一样,改一下dp的可能性就完了

int[][] memo;

int minPathSum(int[][] grid) {
    int m = grid.length;
    int n = grid[0].length;
    memo = new int[m][n]; // 构造备忘录,初始值全部设为 -1

    for (int[] row : memo)
        Arrays.fill(row, -1);
    
    return dp(grid, m - 1, n - 1);
}

int dp(int[][] grid, int i, int j) {
    if (i == 0 && j == 0) {//base case
        return grid[0][0];
    }
    if (i < 0 || j < 0) {//error case
        return Integer.MAX_VALUE;
    }
    if (memo[i][j] != -1) {//use memo if memo has the answer
        return memo[i][j];
    }
    memo[i][j] = Math.min(
        dp(grid, i - 1, j),
        dp(grid, i, j - 1)
    ) + grid[i][j];

    return memo[i][j];
}

最大子数组和

public int maxSubArray(int[] nums) {
    if (nums.length == 0) return 0;

    int[] dp = new int[nums.length];  // 定义:dp[i] 记录以 nums[i] 为结尾的「最大子数组和」
    dp[0] = nums[0];  // base case,以nums[0] 为结尾的连续数组的和自然是nums[0]本身,因为其前面没有东西了

    for(int i = 1; i < nums.length; i++){
        dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
    }
    
    int res = Integer.MIN_VALUE;
    for(int cur : dp){
        res = Math.max(res, cur);
    }

    return res;
}

最长公共子序列

int[][] memo;

int longestCommonSubsequence(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    memo = new int[m][n];
    for (int[] row : memo) 
        Arrays.fill(row, -1); // -1代表未计算

    return dp(s1, 0, s2, 0); //s1[0..]和s2[0..]的lcs长度
}

int dp(String s1, int i, String s2, int j) {// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
    if (i == s1.length() || j == s2.length()) {   // base case
        return 0;
    }

    if (memo[i][j] != -1) {
        return memo[i][j];
    }
    
    if (s1.charAt(i) == s2.charAt(j)) {//如果字符一样,那么当前位置肯定是互为子串,同时进一,memo里这个位置的值是 1+后面字串的 lcs
        memo[i][j] = 1 + dp(s1, i + 1, s2, j + 1);
    } else {
        memo[i][j] = Math.max( // s1[i] 和 s2[j] 至少有一个不在 lcs 中
            dp(s1, i + 1, s2, j), // s1不在的情况
            dp(s1, i, s2, j + 1)  // s2不在的情况
        );
    }
    return memo[i][j];
}
int longestCommonSubsequence(String s1, String s2) { // 自底向上循环法
    int m = s1.length(), n = s2.length();
    int[][] dp = new int[m + 1][n + 1];
    // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
    // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
    // base case: dp[0][..] = dp[..][0] = 0

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 现在 i 和 j 从 1 开始,所以要减一
            if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                // s1[i-1] 和 s2[j-1] 必然在 lcs 中
                dp[i][j] = 1 + dp[i - 1][j - 1];
            } else {
                // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
                dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
            }
        }
    }

    return dp[m][n];
}

抢劫规划

int[] memo;
public int rob(int[] nums) { //迭代式
    memo = new int[nums.length];
    Arrays.fill(memo, -1);

    return dp(nums, 0);
}

int dp(int[] nums, int start){
    if (start >= nums.length) { // error case
        return 0;
    }
    
    if (memo[start] != -1) return memo[start];

    int result = Math.max(dp(nums, start+2) + nums[start], dp(nums, start+1));
    memo[start] = result;

    return result;
}
int rob(int[] nums) {  //自底向上循环的方法
    int n = nums.length;
    int[] dp = new int[n + 2];
    for (int i = n - 1; i >= 0; i--) {
        dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]);
    }
    return dp[0];
}

环形抢劫规划

public int rob(int[] nums) {
    int n = nums.length;
    if (n == 1) return nums[0];

    int[] memo1 = new int[n];
    int[] memo2 = new int[n];
    Arrays.fill(memo1, -1);
    Arrays.fill(memo2, -1);
    return Math.max(
            dp(nums, 0, n - 2, memo1),  //0和-2,也就是0,-1,-2,其中-1隔开不抢
            dp(nums, 1, n - 1, memo2)   //1和-1,也就是1,0,-1,其中0隔开不抢
    );
}

// 定义:计算闭区间 [start,end] 的最优结果
int dp(int[] nums, int start, int end, int[] memo) {
    if (start > end) { // error case
        return 0;
    }

    if (memo[start] != -1) {
        return memo[start];
    }

    int res = Math.max(
            dp(nums, start + 2, end, memo) + nums[start],
            dp(nums, start + 1, end, memo)
    );

    memo[start] = res;
    return res;
}

二叉树抢劫

Map<TreeNode, Integer> memo = new HashMap<>();

public int rob(TreeNode root) {
    if (root == null) return 0; //base case, 当前位置没有房子

    if (memo.containsKey(root)) //memo case
        return memo.get(root);

    // 抢,去下下家
    int do_it = root.val + 
    (root.left == null ? 0 : rob(root.left.left) + rob(root.left.right)) + //
    (root.right == null ? 0 : rob(root.right.left) + rob(root.right.right));
    
    int not_do = rob(root.left) + rob(root.right);// 不抢,去下家

    int res = Math.max(do_it, not_do);
    memo.put(root, res);
    return res;
}
Previous
Next