动态规划专题

动态规划专题


不要纠结,干就完事了,熟练度很重要!!!多练习,多总结!!!

最长递增子序列

LeetCode 300. 最长递增子序列

在这里插入图片描述

解题思路

dp[i]表示以nums[i]这个数结尾的最长递增子序列的长度。
我们已经知道了dp[0…4]的所有结果,我们如何通过这些已知结果推出dp[5]呢?
nums[5]前面有哪些元素小于nums[5]?这个好算,用 for 循环比较一波就能把这些元素找出来。

for (int i = 0; i < nums.length; i++) {
    for (int j = 0; j < i; j++) {
        // 寻找 nums[0..j-1] 中比 nums[i] 小的元素
        if (nums[i] > nums[j]) {
            // 把 nums[i] 接在后面,即可形成长度为 dp[j] + 1,
            // 且以 nums[i] 为结尾的递增子序列
            dp[i] = Math.max(dp[i], dp[j] + 1);
        }
    }
}

代码实现

class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(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);
                }
            }
        }
        int res = 0;
        for(int i = 0; i < dp.length;i++){
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

LeetCode 354. 俄罗斯套娃信封问题

在这里插入图片描述

解题思路

这道题目其实是最长递增子序列的一个变种,因为每次合法的嵌套是大的套小的,相当于在二维平面中找一个最长递增的子序列,其长度就是最多能嵌套的信封个数。
先对宽度w进行升序排序,如果遇到w相同的情况,则按照高度h降序排序;之后把所有的h作为一个数组,在这个数组上计算 LIS 的长度就是答案。

在这里插入图片描述
对宽度w从小到大排序,确保了w这个维度可以互相嵌套,所以我们只需要专注高度h这个维度能够互相嵌套即可。

其次,两个w相同的信封不能相互包含,所以对于宽度w相同的信封,对高度h进行降序排序,保证 LIS 中不存在多个w相同的信封。

代码实现

class Solution {
    public int maxEnvelopes(int[][] envelopes) {

        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[] nums = new int[envelopes.length];
        for(int i = 0;i < envelopes.length;i++){
            nums[i] = envelopes[i][1];
        }
        return lengthOfLIS(nums);
    }

    public int lengthOfLIS(int[] nums){
        int[] dp = new int[nums.length];
        Arrays.fill(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);
                }
            }
        }
        int res = 0;
        for(int i = 0; i < nums.length;i++){
            res = Math.max(dp[i], res);
        }
        return res;
    }
}

最大子数组和

LeetCode 53. 最大子数组和

在这里插入图片描述

解题思路

以nums[i]为结尾的「最大子数组和」为dp[i]。
dp[i]有两种「选择」,要么与前面的相邻子数组连接,形成一个和更大的子数组;要么不与前面的子数组连接,自成一派,自己作为一个子数组。

// 要么自成一派,要么和前面的子数组合并
dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);

代码实现

class Solution {
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        for(int i = 1;i < nums.length;i++){
            dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);
        }
        int res = Integer.MIN_VALUE;
        for(int c:dp){
            res = Math.max(res, c);
        }
        return res;
    }
}

最大编辑距离

LeetCode 72. 编辑距离

在这里插入图片描述

解题思路

if s1[i] == s2[j]:
    return dp(i - 1, j - 1)  # 啥都不做
# 解释:
# 本来就相等,不需要任何操作
# s1[0..i] 和 s2[0..j] 的最小编辑距离等于
# s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离
# 也就是说 dp(i, j) 等于 dp(i-1, j-1)
dp(i, j - 1) + 1,    # 插入
# 解释:
# 我直接在 s1[i] 插入一个和 s2[j] 一样的字符
# 那么 s2[j] 就被匹配了,前移 j,继续跟 i 对比
# 别忘了操作数加一
dp(i - 1, j) + 1,    # 删除
# 解释:
# 我直接把 s[i] 这个字符删掉
# 前移 i,继续跟 j 对比
# 操作数加一
dp(i - 1, j - 1) + 1 # 替换
# 解释:
# 我直接把 s1[i] 替换成 s2[j],这样它俩就匹配了
# 同时前移 i,j 继续对比
# 操作数加一

代码实现

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m+1][n+1];
        for(int i = 1; i < m+1;i++){
            dp[i][0] = i;
        }

        for(int j = 1;j < n+1;j++){
            dp[0][j] = j;
        }

        for(int i = 1;i < m+1;i++){
            for(int j = 1;j < n+1;j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    dp[i][j] = Math.min(dp[i][j-1]+1, Math.min(dp[i-1][j]+1, dp[i-1][j-1]+1));
                }
            }
        }
        return dp[m][n];
    }
}

最长公共子序列

LeetCode 1143. 最长公共子序列

在这里插入图片描述

解题思路

定义: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
在这里插入图片描述

代码实现

  • 自底向上的迭代的动态规划思路:
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length();
        int n = text2.length();
        int[][] dp = new int[m+1][n+1];
        for(int i = 1; i <= m;i++){
            for(int j = 1;j <=n;j++){
                if(text1.charAt(i-1) == text2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}
  • 自顶向下带备忘录的动态规划思路:
// 备忘录,消除重叠子问题
int[][] memo;

/* 主函数 */
int longestCommonSubsequence(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    // 备忘录值为 -1 代表未曾计算
    memo = new int[m][n];
    for (int[] row : memo) 
        Arrays.fill(row, -1);
    // 计算 s1[0..] 和 s2[0..] 的 lcs 长度
    return dp(s1, 0, s2, 0);
}

// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j) {
    // base case
    if (i == s1.length() || j == s2.length()) {
        return 0;
    }
    // 如果之前计算过,则直接返回备忘录中的答案
    if (memo[i][j] != -1) {
        return memo[i][j];
    }
    // 根据 s1[i] 和 s2[j] 的情况做选择
    if (s1.charAt(i) == s2.charAt(j)) {
        // s1[i] 和 s2[j] 必然在 lcs 中
        memo[i][j] = 1 + dp(s1, i + 1, s2, j + 1);
    } else {
        // s1[i] 和 s2[j] 至少有一个不在 lcs 中
        memo[i][j] = Math.max(
            dp(s1, i + 1, s2, j),
            dp(s1, i, s2, j + 1)
        );
    }
    return memo[i][j];
}

LeetCode 583. 两个字符串的删除操作

在这里插入图片描述

解题思路

思路参考《LeetCode 1143. 最长公共子序列》,本题要求两个字符串相同操作最小步数,上一题可以求出两个字符串的最大公共长度,那么两个字符串的删除各自多余长度,操作数之和即为本题答案。

代码实现

class Solution {
    public int minDistance(String word1, String word2) {
        int length = lengthCommonStr(word1, word2);
        int m = word1.length(), n = word2.length();
        return m-length+n-length;
    }


    public int lengthCommonStr(String word1, String word2){
        int m = word1.length(), n = word2.length();
        int[][] dp = new int[m+1][n+1];
        for(int i = 1; i <= m;i++){
            for(int j = 1;j <= n;j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}

LeetCode 712. 两个字符串的最小ASCII删除和

在这里插入图片描述

解题思路

思路参考《LeetCode 1143. 最长公共子序列》自顶向下递归方法,只不过在每一步匹配时,要将不匹配的字符算在总和res中。

代码实现

class Solution {
    int[][] memo;
    public int minimumDeleteSum(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        memo = new int[m][n];
        for(int[] arr:memo){
            Arrays.fill(arr, -1);
        }
        return dp(s1, 0, s2, 0);
    }

    public int dp(String s1, int i, String s2, int j){
        int res = 0;
        if(i == s1.length()){
            for(; j < s2.length();j++){
                res += s2.charAt(j);
            }
            return res;
        }
        if(j == s2.length()){
            for(;i < s1.length();i++){
                res+=s1.charAt(i);
            }
            return res;
        }

        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] = Math.min(s1.charAt(i)+dp(s1, i+1,s2,j), s2.charAt(j)+dp(s1,i,s2,j+1));
        }
        return memo[i][j];
    }
}

正则匹配

LeetCode 10. 正则表达式匹配

在这里插入图片描述

解题思路

dp函数的定义如下:
若dp(s,i,p,j) = true,则表示s[i…]可以匹配p[j…];若dp(s,i,p,j) = false,则表示s[i…]无法匹配p[j…]。

bool dp(string& s, int i, string& p, int j) {
    if (s[i] == p[j] || p[j] == '.') {
        // 匹配
        if (j < p.size() - 1 && p[j + 1] == '*') {
            // 1.1 通配符匹配 0 次或多次
            return dp(s, i, p, j + 2)
                || dp(s, i + 1, p, j);
        } else {
            // 1.2 常规匹配 1 次
            return dp(s, i + 1, p, j + 1);
        }
    } else {
        // 不匹配
        if (j < p.size() - 1 && p[j + 1] == '*') {
            // 2.1 通配符匹配 0 次
            return dp(s, i, p, j + 2);
        } else {
            // 2.2 无法继续匹配
            return false;
        }
    }
}
  • 一个 base case 是j == p.size()时,按照dp函数的定义,这意味着模式串p已经被匹配完了,那么应该看看文本串s匹配到哪里了,如果s也恰好被匹配完:
if (j == p.size()) {
    return i == s.size();
}
  • 另一个 base case 是i == s.size()时,此时并不能根据j是否等于p.size()来判断是否完成匹配,只要p[j…]能够匹配空串,就可以算完成匹配。比如说s = “a”, p = "ab* c* ",当i走到s末尾的时候,j并没有走到p的末尾,但是p依然可以匹配s。
int m = s.size(), n = p.size();

if (i == s.size()) {
    // 如果能匹配空串,一定是字符和 * 成对儿出现
    if ((n - j) % 2 == 1) {
        return false;
    }
    // 检查是否为 x*y*z* 这种形式
    for (; j + 1 < p.size(); j += 2) {
        if (p[j + 1] != '*') {
            return false;
        }
    }
    return true;
}

代码实现

class Solution {
    Map<String, Boolean> memo;
    public boolean isMatch(String s, String p) {
        memo = new HashMap<>();
        return dp(s, 0, p, 0);
    }

    public boolean dp(String s1, int i, String s2, int j){
        int m = s1.length(), n = s2.length();
        if(j == n){
            return i == m;
        }
        if(i == m){
            if((n-j)%2==1){
                return false;
            }
            for(;j+1<n;j+=2){
                if(s2.charAt(j+1) != '*'){
                    return false;
                }
            }
            return true;
        }

        String key = i+"_"+j;
        if(memo.containsKey(key)){
            return memo.get(key);
        }
        boolean res = false;
        if(s1.charAt(i) == s2.charAt(j) || s2.charAt(j) == '.'){
            if(j < s2.length()-1 && s2.charAt(j+1) == '*'){
                res = dp(s1, i, s2, j+2)||dp(s1, i+1, s2,j);
            }else{
                res = dp(s1, i+1, s2, j+1);
            }
        }else{
            if(j < s2.length()-1 && s2.charAt(j+1) == '*'){
                res = dp(s1, i, s2,j+2);
            }else{
                res = false;
            }
        }
        memo.put(key, res);
        return res;
    }
}

通用考察

LeetCode 279. 完全平方数

在这里插入图片描述

解题思路

在这里插入图片描述

代码实现

class Solution {
    public int numSquares(int n) {
        int[] f = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            int minn = Integer.MAX_VALUE;
            for (int j = 1; j * j <= i; j++) {
                minn = Math.min(minn, f[i - j * j]);
            }
            f[i] = minn + 1;
        }
        return f[n];
    }
}

LeetCode 238. 除自身以外数组的乘积

在这里插入图片描述

代码实现

class Solution {
    public int[] productExceptSelf(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n]; // 用来存储左侧所有元素的乘积
    int[] pd = new int[n]; // 用来存储右侧所有元素的乘积

    // 初始化
    dp[0] = 1;
    pd[n - 1] = 1;

    // 计算dp数组(左侧所有元素的乘积)
    for (int i = 1; i < n; i++) {
        dp[i] = dp[i - 1] * nums[i - 1];
    }

    // 计算pd数组(右侧所有元素的乘积)
    for (int i = n - 2; i >= 0; i--) {
        pd[i] = pd[i + 1] * nums[i + 1];
    }

    // 构造最终结果
    int[] result = new int[n];
    for (int i = 0; i < n; i++) {
        result[i] = dp[i] * pd[i];
    }

    return result;
 }

}

LeetCode 221. 最大正方形

在这里插入图片描述

解题思路

在这里插入图片描述

代码实现

class Solution {
    public int maximalSquare(char[][] matrix) {
        int maxSide = 0;
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return maxSide;
        }
        int rows = matrix.length, columns = matrix[0].length;
        int[][] dp = new int[rows][columns];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < columns; j++) {
                if (matrix[i][j] == '1') {
                    if (i == 0 || j == 0) {
                        dp[i][j] = 1;
                    } else {
                        dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                    }
                    maxSide = Math.max(maxSide, dp[i][j]);
                }
            }
        }
        int maxSquare = maxSide * maxSide;
        return maxSquare;
    }
}

LeetCode 152. 乘积最大子数组

在这里插入图片描述

解题思路

在这里插入图片描述

代码实现

class Solution {
    public int maxProduct(int[] nums) {
        int length = nums.length;
        int[] maxF = new int[length];
        int[] minF = new int[length];
        System.arraycopy(nums, 0, maxF, 0, length);
        System.arraycopy(nums, 0, minF, 0, length);
        for (int i = 1; i < length; ++i) {
            maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
            minF[i] = Math.min(minF[i - 1] * nums[i], Math.min(nums[i], maxF[i - 1] * nums[i]));
        }
        int ans = maxF[0];
        for (int i = 1; i < length; ++i) {
            ans = Math.max(ans, maxF[i]);
        }
        return ans;
    }
}

LeetCode 139. 单词拆分

在这里插入图片描述

解题思路

在这里插入图片描述

代码实现

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordDictSet = new HashSet(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] && wordDictSet.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

LeetCode 128. 最长连续序列

在这里插入图片描述

代码实现

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> num_set = new HashSet<Integer>();
        for (int num : nums) {
            num_set.add(num);
        }
        int longestStreak = 0;
        for (int num : num_set) {
            if (!num_set.contains(num - 1)) {
                int currentNum = num;
                int currentStreak = 1;
                while (num_set.contains(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }
                longestStreak = Math.max(longestStreak, currentStreak);
            }
        }
        return longestStreak;
    }
}

背包问题

0-1背包

给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?

解题思路

  • 第一步要明确两点,「状态」和「选择」。状态有两个,就是「背包的容量」和「可选择的物品」。选择就是「装进背包」或者「不装进背包」嘛。
or 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1,选择2...)
  • 第二步要明确dp数组的定义。
    dp[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]。最终答案就是dp[N][W]。base case 就是dp[0][…] = dp[…][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0

for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            把物品 i 装进背包,
            不把物品 i 装进背包
        )
return dp[N][W]
  • 第三步,根据「选择」,思考状态转移的逻辑。
    • 不装入第i个物品:最大价值dp[i][w]应该等于dp[i-1][w]。
    • 装入第i个物品,dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]。
      (由于i是从 1 开始的,所以对val和wt的取值是i-1)
for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            dp[i-1][w],
            dp[i-1][w - wt[i-1]] + val[i-1]
        )
return dp[N][W]

代码实现

int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
    // vector 全填入 0,base case 已初始化
    vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
    for (int i = 1; i <= N; i++) {
        for (int w = 1; w <= W; w++) {
            if (w - wt[i-1] < 0) {
                // 当前背包容量装不下,只能选择不装入背包
                dp[i][w] = dp[i - 1][w];
            } else {
                // 装入或者不装入背包,择优
                dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1], 
                               dp[i - 1][w]);
            }
        }
    }

    return dp[N][W];
}

LeetCode 416. 分割等和子集(0-1背包变体)

在这里插入图片描述

解题思路

可以先对集合求和,得出sum,把问题转化为背包问题:
给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?

  • 第一步要明确两点,「状态」和「选择」。状态就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。

  • 第二步要明确dp数组的定义。dp[i][j] = x表示,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包装满,若x为false,则说明不能恰好将背包装满。
    最终答案dp[N][sum/2],base case 就是dp[…][0] = true和dp[0][…] = false,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。

  • 第三步,根据「选择」,思考状态转移的逻辑。

    • 如果不把nums[i]算入子集,或者说你不把这第i个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态dp[i-1][j],继承之前的结果。

    • 如果把nums[i]算入子集,或者说你把这第i个物品装入了背包,那么是否能够恰好装满背包,取决于状态dp[i - 1][j-nums[i-1]]。

代码实现

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        int sum = 0;
        for(int c:nums){
            sum+=c;
        }
        if(sum%2 != 0){
            return false;
        }
        sum = sum/2;
        boolean[][] dp = new boolean[n+1][sum+1];
        for(int i = 1; i <= n;i++){
            dp[i][0] = true;
        }
        
        for(int i = 1;i <= n;i++){
            for(int j = 1;j <= sum;j++){
                if(j-nums[i-1]<0){
                    dp[i][j] = dp[i-1][j];
                }else{
                    dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]];
                }
            }
        }
        return dp[n][sum];
    }
}

LeetCode 322. 零钱兑换

在这里插入图片描述

代码实现

public class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = amount + 1;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, max);
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

LeetCode 518. 零钱兑换 II(完全背包)

在这里插入图片描述

解题思路

把这个问题转化为背包问题的描述形式:

有一个背包,最大容量为amount,有一系列物品coins,每个物品的重量为coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?每个物品的数量是无限的,这也就是传说中的「完全背包问题」,没啥高大上的,无非就是状态转移方程有一点变化而已。

  • 第一步要明确两点,「状态」和「选择」。就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 计算(选择1,选择2...)
  • 第二步要明确dp数组的定义。base case 为dp[0][…] = 0, dp[…][0] = 1。因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。最终想得到的答案就是dp[N][amount],其中N为coins数组的大小。
int dp[N+1][amount+1]
dp[0][..] = 0
dp[..][0] = 1

for i in [1..N]:
    for j in [1..amount]:
        把物品 i 装进背包,
        不把物品 i 装进背包
return dp[N][amount]
  • 第三步,根据「选择」,思考状态转移的逻辑。
    • 不把这第i个物品装入背包,也就是说你不使用coins[i]这个面值的硬币,那么凑出面额j的方法数dp[i][j]应该等于dp[i-1][j],继承之前的结果。
    • 把这第i个物品装入了背包,也就是说你使用coins[i]这个面值的硬币,那么dp[i][j]应该等于dp[i][j-coins[i-1]]。(0-1背包与完全背包此处i的状态有不同)
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= amount; j++) {
        if (j - coins[i-1] >= 0)
            dp[i][j] = dp[i - 1][j] 
                     + dp[i][j-coins[i-1]];
return dp[N][W]

代码实现

class Solution {
    public int change(int amount, int[] coins) {
        int n = coins.length;
        int[][] dp = new int[n+1][amount+1];
        for(int i = 1;i <= n;i++){
            dp[i][0] = 1;
        }
        for(int i = 1;i <= n;i++){
            for(int j = 1; j<= amount;j++){
                if(j - coins[i-1] < 0){
                    dp[i][j] = dp[i-1][j];
                }else{
                    dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]];
                }
            }
        }
        return dp[n][amount];
    }
}

在这里插入图片描述

class Solution {
    public int change(int amount, int[] coins) {
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int coin : coins) {
            for (int i = coin; i <= amount; i++) {
                dp[i] += dp[i - coin];
            }
        }
        return dp[amount];
    }
}

买卖股票问题

具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。

for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1,选择2...)
dp[i][k][0 or 1]
0 <= i <= n - 1, 1 <= k <= K
n 为天数,大 K 为交易数的上限,01 代表是否持有股票。
此问题共 n × K × 2 种状态,全部穷举就能搞定。

for 0 <= i < n:
    for 1 <= k <= K:
        for s in {0, 1}:
            dp[i][k][s] = max(buy, sell, rest)

想求的最终答案是dp[n - 1][K][0],即最后一天,最多允许K次交易,最多获得多少利润。

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
              max( 今天选择 rest,        今天选择 sell       )

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
			  max( 今天选择 rest,         今天选择 buy         )

这个解释应该很清楚了,如果buy,就要从利润中减去prices[i],如果sell,就要给利润增加prices[i]。今天的最大利润就是这两种可能选择中较大的那个。

注意k的限制,在选择buy的时候相当于开启了一次交易,那么对于昨天来说,交易次数的上限k应该减小 1。
(不能认为在sell的时候给k减小 1 和在buy的时候给k减小 1 是等效的,因为交易是从buy开始,如果buy的选择不改变交易次数k的约束,会出现交易次数超出限制的的错误。)

LeetCode 121. 买卖股票的最佳时机

在这里插入图片描述

解题思路

dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) 
            = max(dp[i-1][1][1], -prices[i])
解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。

现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
可以进行进一步化简去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        for(int i = 0;i < n;i++){
            if(i-1 == -1){
                dp[i][0] = 0;
                dp[i][1] = -prices[i];
                continue;
            }
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
        }
        return dp[n-1][0];
    }
}

LeetCode 122. 买卖股票的最佳时机 II

在这里插入图片描述

解题思路

如果k为正无穷,那么就可以认为k和k - 1是一样的:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
            = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])

我们发现数组中的 k 已经不会改变了,也就是说不需要记录 k 这个状态了:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        for(int i = 0; i < n;i++){
            if(i-1 == -1){
                dp[i][0] = 0;
                dp[i][1] = -prices[i];
                continue;
            }
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }
        return dp[n-1][0];
    }
}

LeetCode 123. 买卖股票的最佳时机 III

在这里插入图片描述

解题思路

k = 2时,由于没有消掉k的影响,所以必须要对k进行穷举:

为什么从大到小遍历k也可以正确提交呢?
因为你注意看,dp[i][k]不会依赖dp[i][k - 1],而是依赖dp[i - 1][k - 1],对于dp[i - 1][…],都是已经计算出来的。所以不管你是k = max_k, k–,还是k = 1, k++,都是可以得出正确答案的。

那为什么我使用k = max_k, k–的方式呢?因为这样符合语义。

你买股票,初始的「状态」是什么?应该是从第 0 天开始,而且还没有进行过买卖,所以最大交易次数限制k应该是max_k;而随着「状态」的推移,你会进行交易,那么交易次数上限k应该不断减少,这样一想,k = max_k, k–的方式是比较合乎实际场景的。

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int max_k = 2;
        int[][][] dp = new int[n][max_k+1][2];
        for(int i = 0;i < n;i++){
            for(int k = max_k;k >= 1;k--){
                if(i-1 == -1){
                    dp[i][k][0] = 0;
                    dp[i][k][1] = -prices[i];
                    continue;
                }
                dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]);
                dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]);
            }
        }
        return dp[n-1][max_k][0];
    }
}

LeetCode 188. 买卖股票的最佳时机 IV

在这里插入图片描述

解题思路

一次交易由买入和卖出构成,至少需要两天。所以说有效的限制k应该不超过n/2,如果超过,就没有约束作用了,相当于k = +infinity。这种情况是之前解决过的。

代码实现

class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        if(k > n/2){
            return maxProfitInfK(prices);
        }

        int[][][] dp = new int[n][k+1][2];
        for(int i = 0;i < n;i++){
            for(int j = k;j >= 1;j--){
                if(i-1 == -1){
                    dp[i][j][0] = 0;
                    dp[i][j][1] = -prices[i];
                    continue;
                }
                dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]);
                dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]);
            }
        }
        return dp[n-1][k][0];

    }


    public int maxProfitInfK(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        for(int i = 0;i < n;i++){
            if(i-1 == -1){
                dp[i][0] = 0;
                dp[i][1] = -prices[i];
                continue;
            }
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }
        return dp[n-1][0];
    }

}

LeetCode 309. 最佳买卖股票时机含冷冻期

在这里插入图片描述

解题思路

每次sell之后要等一天才能继续交易:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        for(int i = 0; i < n;i++){
            if(i-1 == -1){
                dp[i][0] = 0;
                dp[i][1] = -prices[i];
                continue;
            }
            if(i-2 == -1){
                dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
                dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
                continue;
            }
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0]-prices[i]);
        }
        return dp[n-1][0];
    }
}

LeetCode 714. 买卖股票的最佳时机含手续费

在这里插入图片描述

解题思路

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。

代码实现

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        for(int i = 0; i < n;i++){
            if(i-1 == -1){
                dp[i][0] = 0;
                dp[i][1] = -prices[i]-fee;
                continue;
            }
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]-fee);
        }
        return dp[n-1][0];
    }
}

打家劫舍系列

LeetCode 198. 打家劫舍

在这里插入图片描述

解题思路

解决动态规划问题就是找「状态」和「选择」,仅此而已。
假想你就是这个专业强盗,从左到右走过这一排房子,在每间房子前都有两种选择:抢或者不抢。

  • 如果你抢了这间房子,那么你肯定不能抢相邻的下一间房子了,只能从下下间房子开始做选择。
  • 如果你不抢这间房子,那么你可以走到下一间房子前,继续做选择。
  • 当你走过了最后一间房子后,你就没得抢了,能抢到的钱显然是 0(base case)。
    在这里插入图片描述

代码实现

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        if(n == 1){
            return nums[0];
        }
        dp[0]=nums[0];
        dp[1]=Math.max(nums[0], nums[1]);
        for(int i = 2;i < n;i++){
            dp[i]=Math.max(dp[i-1], dp[i-2]+nums[i]);
        }
        return dp[n-1];
    }
}

LeetCode 213. 打家劫舍 II

在这里插入图片描述

解题思路

这些房子不是一排,而是围成了一个圈。也就是说,现在第一间房子和最后一间房子也相当于是相邻的,不能同时抢。比如说输入数组nums=[2,3,2],算法返回的结果应该是 3 而不是 4,因为开头和结尾不能同时被抢。
在这里插入图片描述
只要比较情况二和情况三就行了,因为这两种情况对于房子的选择余地比情况一大呀,房子里的钱数都是非负数,所以选择余地大,最优决策结果肯定不会小。

代码实现

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n == 1){
            return nums[0];
        }
        if(n == 2){
            return Math.max(nums[0], nums[1]);
        }
        int[] dp1 = new int[n];
        int[] dp2 = new int[n];

        dp1[0] = nums[0];
        dp1[1] = Math.max(nums[0], nums[1]);
        for(int i = 2;i < n-1;i++){
            dp1[i] = Math.max(dp1[i-1], dp1[i-2]+nums[i]);
        }

        dp2[1] = nums[1];
        dp2[2] = Math.max(nums[1], nums[2]);
        for(int j = 3;j < n;j++){
            dp2[j] = Math.max(dp2[j-1], dp2[j-2]+nums[j]);
        }
        return Math.max(dp1[n-2], dp2[n-1]);
    }
}

LeetCode 337. 打家劫舍 III

在这里插入图片描述

解题思路

动态规划本质思路都是在根据找“状态”,再“选择”,本题也是如此,和数组类似,二叉树有多个节点,每个节点都可以做出选择是否选择“盗窃”,再根据不能盗窃相邻节点的要求,去比对当前节点 偷 与 不偷 的最大值。

代码实现


class Solution {
    Map<TreeNode, Integer> memo = new HashMap<>();
    public int rob(TreeNode root) {
        if(root == null){
            return 0;
        }
        if(memo.containsKey(root)){
            return memo.get(root);
        }
        int do_it = root.val+(root.left != null? rob(root.left.left)+rob(root.left.right):0)+(root.right != null? rob(root.right.left)+rob(root.right.right):0);
        int not_do = rob(root.left)+rob(root.right);
        int res = Math.max(do_it, not_do);
        memo.put(root, res);
        return res;
    }
}

博弈问题

LeetCode 877. 石子游戏

在这里插入图片描述

解题思路

博弈类问题的套路都差不多,其核心思路是在二维 dp 的基础上使用元组分别存储两个人的博弈结果。
把「石头游戏」改的更具有一般性:
你和你的朋友面前有一排石头堆,用一个数组piles表示,piles[i]表示第i堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。

在这里插入图片描述
对 dp 数组含义的解释:

  • dp[i][j].fir = x表示,对于piles[i…j]这部分石头堆,先手能获得的最高分数为x。
  • dp[i][j].sec = y表示,对于piles[i…j]这部分石头堆,后手能获得的最高分数为y。
dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)
dp[i][j].fir = max(     选择最左边的石头堆     ,     选择最右边的石头堆      )
# 解释:我作为先手,面对 piles[i...j] 时,有两种选择:
# 要么我选择最左边的那一堆石头,然后面对 piles[i+1...j]
# 但是此时轮到对方,相当于我变成了后手;
# 要么我选择最右边的那一堆石头,然后面对 piles[i...j-1]
# 但是此时轮到对方,相当于我变成了后手。

if 先手选择左边:
    dp[i][j].sec = dp[i+1][j].fir
if 先手选择右边:
    dp[i][j].sec = dp[i][j-1].fir
# 解释:我作为后手,要等先手先选择,有两种情况:
# 如果先手选择了最左边那堆,给我剩下了 piles[i+1...j]
# 此时轮到我,我变成了先手;
# 如果先手选择了最右边那堆,给我剩下了 piles[i...j-1]
# 此时轮到我,我变成了先手。
根据 dp 数组的定义,我们也可以找出 base case,也就是最简单的情况:

dp[i][j].fir = piles[i]
dp[i][j].sec = 0
其中 0 <= i == j < n
# 解释:i 和 j 相等就是说面前只有一堆石头 piles[i]
# 那么显然先手的得分为 piles[i]
# 后手没有石头拿了,得分为 0

代码实现

class Solution {
    public boolean stoneGame(int[] piles) {
        int n = piles.length;
        Pair[][] dp = new Pair[n][n];
        for(int i = 0;i < n;i++){
            for(int j = 0;j < n;j++){
                dp[i][j] = new Pair(0,0);
                if(i == j){
                    dp[i][j].fir = piles[i];
                    dp[i][j].sec = 0;
                }
            }
        }

        for(int i = n-2;i >=0;i--){
            for(int j = i+1;j < n;j++){
                int left = piles[i]+dp[i+1][j].sec;
                int right = piles[j]+dp[i][j-1].sec;
                if(left > right){
                    dp[i][j].fir = left;
                    dp[i][j].sec = dp[i+1][j].fir;
                }else{
                    dp[i][j].fir = right;
                    dp[i][j].sec = dp[i][j-1].fir;
                }
            }
        }

        Pair res = dp[0][n-1];
        return res.fir>res.sec;
        
    }
}

class Pair{
    int fir,sec;
    Pair(int fir, int sec){
        this.fir = fir;
        this.sec = sec;
    }
}

路径和

LeetCode 64. 最小路径和

在这里插入图片描述

解题思路

dp函数的定义如下:
从左上角位置(0, 0)走到位置(i, j)的最小路径和为dp(grid, i, j)。
dp[i][j]依然取决于dp[i-1][j]和dp[i][j-1]

代码实现

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[][] dp = new int[m][n];
        dp[0][0] = grid[0][0];
        for(int i = 1;i < m;i++){
            dp[i][0] = dp[i-1][0]+grid[i][0];
        }
        for(int j = 1;j < n;j++){
            dp[0][j] = dp[0][j-1]+grid[0][j];
        }

        for(int i = 1;i < m;i++){
            for(int j = 1;j < n;j++){
                dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1])+grid[i][j];
            }
        }
        return dp[m-1][n-1];
    }
}

LeetCode 63. 不同路径 II

在这里插入图片描述

解题思路

在这里插入图片描述
注意障碍物的情况,dp值为0

代码实现

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m=obstacleGrid.length;
        int n=obstacleGrid[0].length;
        int[][] dp=new int[m][n];
        if (obstacleGrid[0][0]!=1){
            dp[0][0]=1;
        }
        for (int i=1;i<m;i++){
            dp[i][0]=obstacleGrid[i][0]==1?0:dp[i-1][0];
        }
        for (int j=1;j<n;j++){
            dp[0][j]=obstacleGrid[0][j]==1?0:dp[0][j-1];
        }
        for (int i=1;i<m;i++){
            for (int j=1;j<n;j++){
                dp[i][j]=obstacleGrid[i][j]==1?0:(dp[i-1][j]+dp[i][j-1]);
            }
        }
        return dp[m-1][n-1];
    }
}

DP玩游戏

LeetCode 174. 地下城游戏

在这里插入图片描述
在这里插入图片描述

解题思路

dp函数的定义:
从grid[i][j]到达终点(右下角)所需的最少生命值是dp(grid, i, j)。
base case,想求dp(0, 0),那就应该试图通过dp(i, j+1)和dp(i+1, j)推导出dp(i, j),这样才能不断逼近 base case,正确进行状态转移。

代码实现

class Solution {
    int[][] memo;
    public int calculateMinimumHP(int[][] dungeon) {
        int m = dungeon.length, n = dungeon[0].length;
        memo = new int[m][n];
        for(int[] arr:memo){
            Arrays.fill(arr, -1);
        }
        return dp(dungeon, 0, 0);
    }

    int dp(int[][] dungeon, int i, int j){
        int m = dungeon.length, n = dungeon[0].length;
        if(i == m-1 && j == n-1){
            return dungeon[i][j] >= 0? 1:-dungeon[i][j]+1;
        }
        if(i == m || j == n){
            return Integer.MAX_VALUE;
        }
        if(memo[i][j] != -1){
            return memo[i][j];
        }

        int res = Math.min(dp(dungeon, i, j+1), dp(dungeon, i+1, j))-dungeon[i][j];
        memo[i][j] = (res <= 0 ?1:res);
        return memo[i][j];
    }
}

LeetCode 514. 自由之路

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

解题思路

dp函数的定义如下:
当圆盘指针指向ring[i]时,输入字符串key[j…]至少需要dp(ring, i, key, j)次操作。
根据这个定义,题目其实就是想计算dp(ring, 0, key, 0)的值

int dp(string& ring, int i, string& key, int j) {
    // base case 完成输入
    if (j == key.size()) return 0;

    // 做选择
    int res = INT_MAX;
    for (int k : [字符 key[j] 在 ring 中的所有索引]) {
        res = min(
            把 i 顺时针转到 k 的代价,
            把 i 逆时针转到 k 的代价
        );
    }

    return res;
}
// 字符 -> 索引列表
unordered_map<char, vector<int>> charToIndex;
// 备忘录
vector<vector<int>> memo;

/* 主函数 */
int findRotateSteps(string ring, string key) {
    int m = ring.size();
    int n = key.size();
    // 备忘录全部初始化为 0
    memo.resize(m, vector<int>(n, 0));
    // 记录圆环上字符到索引的映射
    for (int i = 0; i < ring.size(); i++) {
        charToIndex[ring[i]].push_back(i);
    }
    // 圆盘指针最初指向 12 点钟方向,
    // 从第一个字符开始输入 key
    return dp(ring, 0, key, 0);
}

// 计算圆盘指针在 ring[i],输入 key[j..] 的最少操作数
int dp(string& ring, int i, string& key, int j) {
    // base case 完成输入
    if (j == key.size()) return 0;
    // 查找备忘录,避免重叠子问题
    if (memo[i][j] != 0) return memo[i][j];

    int n = ring.size();
    // 做选择
    int res = INT_MAX;
    // ring 上可能有多个字符 key[j]
    for (int k : charToIndex[key[j]]) {
        // 拨动指针的次数
        int delta = abs(k - i);
        // 选择顺时针还是逆时针
        delta = min(delta, n - delta);
        // 将指针拨到 ring[k],继续输入 key[j+1..]
        int subProblem = dp(ring, k, key, j + 1);
        // 选择「整体」操作次数最少的
        res = min(res, 1 + delta + subProblem);
        // PS:加一是因为按动按钮也是一次操作
    }
    // 将结果存入备忘录
    memo[i][j] = res;
    return res;
}

代码实现

class Solution {
    Map<Character, List<Integer>> charIndex;
    int[][] memo;
    public int findRotateSteps(String ring, String key) {
        int m = ring.length();
        int n = key.length();
        memo = new int[m][n];
        charIndex = new HashMap<>();

        for(int i = 0;i < ring.length();i++){
            char ch = ring.charAt(i);
            List<Integer> list;
            if(charIndex.containsKey(ch)){
                list = charIndex.get(ch);
            }else {
                list = new ArrayList<>();
            }
            list.add(i);
            charIndex.put(ch, list);
        }
        return dp(ring, 0, key, 0);
    }

    public int dp(String ring, int i, String key, int j){
        if(j == key.length()){
            return 0;
        }
        if(memo[i][j] != 0){
            return memo[i][j];
        }
        int res = Integer.MAX_VALUE;
        for(int k : charIndex.get(key.charAt(j))){
            int delta = Math.abs(k - i);
            delta = Math.min(delta, ring.length() - delta);
            int subNum = dp(ring, k, key, j+1);
            res = Math.min(res, delta+subNum+1);
        }

        memo[i][j] = res;
        return res;
    }
}

旅行

LeetCode 787. K 站中转内最便宜的航班

在这里插入图片描述

解题思路

让你求src到dst权重最小的一条路径,同时要满足,这条路径最多不能超过K + 1条边(经过K个节点相当于经过K + 1条边。

从起点src出发,k步之内(一步就是一条边)到达节点s的最小路径权重为dp(s, k)。
在这里插入图片描述
s1, s2是指向dst的相邻节点,我只要能够在K - 1步之内从src到达s1, s2,那我就可以在K步之内从src到达dst。

也就是如下关系式:

dp(dst, k) = min(
    dp(s1, k - 1) + w1, 
    dp(s2, k - 1) + w2
)

代码实现

class Solution {
    Map<Integer, List<int[]>> indegree;
    int src,dst;
    int[][] memo;
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        k++;
        this.src = src;
        this.dst = dst;
        indegree = new HashMap<>();
        memo = new int[n][k+1];
        for(int[] f:flights){
            int from = f[0];
            int to = f[1];
            int price = f[2];
            indegree.putIfAbsent(to, new ArrayList<>());
            indegree.get(to).add(new int[]{from, price});
        }
        return dp(dst, k);
    }

    public int dp(int s, int k){
        if(src == s){
            return 0;
        }
        if(k == 0){
            return -1;
        }
        if(memo[s][k] != 0){
            return memo[s][k];
        }
        int res = Integer.MAX_VALUE;
        if(indegree.containsKey(s)){
            for(int[] f:indegree.get(s)){
                int from = f[0];
                int price = f[1];
                int sub = dp(from, k-1);
                if(sub != -1){
                    res = Math.min(res, sub+price);
                }
            }
        }
        res = (res == Integer.MAX_VALUE ? -1:res);
        memo[s][k] = res;
        return res;
    }
}

总结

本题来源于Leetcode中 归属于动态规划类型题目。
同许多在算法道路上不断前行的人一样,不断练习,修炼自己!
如有博客中存在的疑问或者建议,可以在下方留言一起交流,感谢各位!

觉得本博客有用的客官,可以给个点赞+收藏哦! 嘿嘿

喜欢本系列博客的可以关注下,以后除了会继续更新面试手撕代码文章外,还会出其他系列的文章!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值