Leetcode —「动态规划」系列题解

组合数之和 IV

Leetcode - 337 Combination Sum IV (Medium)

题目描述:从数组 nums 中选出和为 target 的数字组合,数字之间的顺序不同表示不同的组合。

nums = [1, 2, 3]
target = 4

The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

Note that different sequences are counted as different combinations.

Therefore the output is 7.
public int combinationSum4(int[] nums, int target) {
    int[] dp = new int[target + 1];
    dp[0] = 1;
    
    for(int i = 1; i < dp.length; i++){
        for(int j = 0; j < nums.length; j++){
            if(i - nums[j] >= 0){
                dp[i] += dp[i - nums[j]];
            }
        }
    }
    return dp[target];
}

唯一的二叉搜索树(BST)

Leetcode - 96 Unique Binary Search Trees (Medium)

题目描述:给定整数 n,计算由 1…n 能组成多少个不同的二叉搜索树。

Input: 3
Output: 5
Explanation:
Given n = 3, there are a total of 5 unique BST's:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

解题思路:这道题非常棒,从上述的输入示例中可以看出,如果 n = 3,最后输出的可能 BST 是分别由 1、2 和 3 作为根节点的 BST 数目之和。设 G(n) 为节点数为 n 时 BST 的数量,这也就是本题需要计算的结果。那么 G(n) 是怎么计算的呢? 设 F(i,n) 为当根节点是 i 时 BST 的数量,那么 G(n) 就可以分解成 G(n) = F(1,n) + F(2,n) +…+ F(n,n)。

问题又来了,F(i,n) 是怎么计算的? 假设给定序列 [1 2 3 4 5 6 7],F(3,7) 表示以 3 为根节点 BST 的数量,这时可以划分左子树与右子树,左子树由 [1 2] 组成,为 G(2);右子树由 [4 5 6 7] 组成,为 G(4),那么组合结果为 F(3,7) = G(2) * G(4),即 F(i,n) = G(i - 1) * G(n - i)。可以定义初始状态,当 n = 0 时,空树,G(0) = 1;当 n = 1 时,树中只有一个节点,G(1) = 1。

这样就可以用动态规划计算最终结果:G(n) = G(0) * G(n-1) + G(1) * G(n-2) +…+ G(n-1) * G(0)。

public int numTrees(int n) {
    int [] G = new int[n + 1];
    G[0] = G[1] = 1;
    for(int i = 2; i <= n; i++) {
        for(int j = 1; j <= i; j++) {
            G[i] += G[j - 1] * G[i - j];
        }
    }
    return G[n];
}

最大正方形

Leetcode - 221 Maximal Square (Medium)

题目描述:找出矩阵中用 1 表示的最大正方形的面积。

Input: 

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

Output: 4

解题思路:d(i, j) 表示在索引为 i 和 j 时,最大正方形的边长,可以从图中看出,如果当前位置是 1, 最大边长就是左上方、上方与左方的最小值加 1,

$dp(i, j) = min(dp(i - 1, j), dp(i - 1, j - 1), dp(i, j - 1)) + 1$


public int maximalSquare(char[][] matrix) {
    int rows = matrix.length, cols = rows > 0 ? matrix[0].length : 0;
    int[][] dp = new int[rows + 1][cols + 1];
    int maxsqlen = 0;
    for (int i = 1; i <= rows; i++) {
        for (int j = 1; j <= cols; j++) {
            if (matrix[i-1][j-1] == '1'){
                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
                maxsqlen = Math.max(maxsqlen, dp[i][j]);
            }
        }
    }
    return maxsqlen * maxsqlen;
}

可以看出每次计算的 dp(i, j) 只与左上方、上方和左方的数值有关,所有可以用一维数组记录数据。

public int maximalSquare(char[][] matrix) {
    int rows = matrix.length, cols = rows > 0 ? matrix[0].length : 0;
    int[] dp = new int[cols + 1];
    int maxsqlen = 0, prev = 0;
    for (int i = 1; i <= rows; i++) {
        for (int j = 1; j <= cols; j++) {
            int temp = dp[j];
            if (matrix[i - 1][j - 1] == '1') {
                dp[j] = Math.min(Math.min(dp[j - 1], prev), dp[j]) + 1;
                maxsqlen = Math.max(maxsqlen, dp[j]);
            } else {
                dp[j] = 0;
            }
            prev = temp;
        }
    }
    return maxsqlen * maxsqlen;
}

完全平方数

Leetcode - 279 Perfect Squares (Medium)

题目描述:给定一个正整数,就算最少能由几个完全平方数组成。

Input: n = 13
Output: 2
Explanation: 13 = 4 + 9.

解题思路:dp[n] 表示给定数为 n 时,完美平方数的最少数量。

dp[0] = 0 
dp[1] = dp[0]+1 = 1
dp[2] = dp[1]+1 = 2
dp[3] = dp[2]+1 = 3
dp[4] = Min{ dp[4-1*1]+1, dp[4-2*2]+1 } 
      = Min{ dp[3]+1, dp[0]+1 } 
      = 1				
dp[5] = Min{ dp[5-1*1]+1, dp[5-2*2]+1 } 
      = Min{ dp[4]+1, dp[1]+1 } 
      = 2
						.
						.
						.
dp[13] = Min{ dp[13-1*1]+1, dp[13-2*2]+1, dp[13-3*3]+1 } 
       = Min{ dp[12]+1, dp[9]+1, dp[4]+1 } 
       = 2
						.
						.
						.
dp[n] = Min{ dp[n - i*i] + 1 },  n - i*i >=0 && i >= 1
public int numSquares(int n) {
    int[] dp = new int[n + 1];
    Arrays.fill(dp, Integer.MAX_VALUE);
    dp[0] = 0;
    for(int i = 1; i <= n; ++i) {
        int min = Integer.MAX_VALUE;
        int j = 1;
        while(i - j*j >= 0) {
            min = Math.min(min, dp[i - j*j] + 1);
            ++j;
        }
        dp[i] = min;
    }		
    return dp[n];
}

最长上升子序列

Leetcode - 300 Longest Increasing Subsequence (Medium)

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

解题思路:动态规划经典问题,dp[i] 表示前 i 个数以 A[i] 结尾的最长上升子序列长度。

$dp[i] = max(dp[j]) + 1, 0 <= j < i$

$LIS_{length} = max(dp[i]), 0 <= i < n$

public int lengthOfLIS(int[] nums) {
    if(nums.length == 0) return 0;
    int[] dp = new int[nums.length];
    dp[0] = 1;
    int maxAns = 1;
    for(int i = 1; i < dp.length; i++){
        int maxVal = 0;
        for(int j = 0; j < i; j++){
            if(nums[i] > nums[j]){
                maxVal = Math.max(maxVal, dp[j]);
            }
        }
        dp[i] = maxVal + 1;
        maxAns = Math.max(maxAns, dp[i]);
    }
    return maxAns;
}

拆分词句

Leetcode - 139 Word Break (Medium)

题目描述:判断给定的字符串是否是有词库里的单词拼接而成的,词库中的单词都能够多次使用。

Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: Return true because "leetcode" can be segmented as "leet code".

解题思路:dp[i] 表示前 i 个字符能否被拆分成词库中的单词,dp[n] 就是本题需要求解的结果。

public boolean wordBreak(String s, List<String> wordDict) {
    boolean[] dp = new boolean[s.length() + 1];
    dp[0] = true;
    for(int i = 1; i <= s.length(); i++){
        for(int j = 0; j < i; j++){
            if(dp[j] && wordDict.contains(s.substring(j, i))){
                dp[i] = true;
                break;
            }
        }
    }
    return dp[s.length()];
}

三角形

Leetcode - 120 Triangle (Medium)

题目描述:给定一个三角形,寻找一条自上而下的路径,使得路径和最短,只能向下走相邻的节点。

解题思路: 每个节点记录从下到上的最短路径和。

dp[i] = min(triangle[j], triangle[j + 1]) + triangle[i][j]

public int minimumTotal(List<List<Integer>> triangle) {
    int[] res = new int[triangle.size() + 1];
    for (int i = triangle.size() - 1; i >= 0; i--) {
        for (int j = 0; j < triangle.get(i).size(); j++) {
            res[j] = Math.min(res[j], res[j + 1]) + triangle.get(i).get(j);
        }
    }
    return res[0];
}

爬台阶

Leetcode - 70 Climbing Stairs (Easy)

题目描述:每次能爬一个或两个台阶,如果有 n 个台阶的话,计算爬到顶端的方法数量。

解题思路:dp(1) = 1 dp(2) = 2 dp(n) = dp(n - 1) + dp(n - 2)

public int climbStairs(int n) {
    if(n <= 0) return 0;
    if(n <= 2) return n;
    int f1 = 1, f2 = 2;
    for(int i = 3; i <= n; i++){
        int t = f1 + f2;
        f1 = f2;
        f2 = t;
    }
    return f2;
}

最大乘积子序列

Leetcode - 152 Maximum Product Subarray (Medium)

Input: [2,3,-2,4]
Output: 6
Explanation: [2,3] has the largest product 6.

解题思路:记录从 0 到 i,每个 i 的最大乘积值与最小乘积值。

public int maxProduct(int[] nums) {
    if(nums.length == 0) return 0;
    int maxPre = nums[0];
    int minPre = nums[0];
    int max, min;
    int maxSoFar = nums[0];
    for(int i = 1; i < nums.length; i++){
        max = Math.max(Math.max(maxPre * nums[i], minPre * nums[i]), nums[i]);
        min = Math.min(Math.min(maxPre * nums[i], minPre * nums[i]), nums[i]);
        System.out.println(max + " " + min);
        maxSoFar = Math.max(max, maxSoFar);
        maxPre = max;
        minPre = min;
    }
    return maxSoFar;
}

最佳买卖股票的时间 I

Leetcode - 121 Best Time to Buy and Sell Stock (Easy)

题目描述:只能持有一只股票,一天只能买卖一次。

解法一:DP 解法

public int maxProfit(int[] prices) {
    if(prices == null || prices.length == 0) return 0;
    int[][] mp = new int[prices.length][3];
    mp[0][1] = -prices[0];
    int res = 0;
    for (int i = 1; i < prices.length; i++){
        mp[i][0] = mp[i - 1][0]; // 没有买卖
        mp[i][1] = Math.max(mp[i - 1][1], mp[i - 1][0] - prices[i]); // 买了股票
        mp[i][2] = mp[i - 1][1] + prices[i]; // 卖了股票
        res = Math.max(res, Math.max(mp[i][0], Math.max(mp[i][1], mp[i][2])));
    }
    return res;
}

解法二:优解

public int maxProfit(int prices[]) {
    int minprice = Integer.MAX_VALUE;
    int maxprofit = 0;
    for (int i = 0; i < prices.length; i++) {
        if (prices[i] < minprice)
            minprice = prices[i];
        else if (prices[i] - minprice > maxprofit)
            maxprofit = prices[i] - minprice;
    }
    return maxprofit;
}