❗⭕⭕算法老大哥——动态规划!

1 简介

  • 本文主要是基于labuladong大佬资料的学习笔记

  • 包含递归思想,贪心只是动态规划的一个特例

  • 一般分为自底向上和自上到下的写法,注意

  • 穷举有重叠时候考虑建立备忘录。

  • 明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。

  • 一维的dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。

那么,如果仔细观察的话可以发现其中的原因的。你只要把住两点就行了:

  • 1、遍历的过程中,所需的状态必须是已经计算出来的。

  • 2、遍历的终点必须是存储结果的那个位置

2 简单的动态规划

labuladong大佬详情

2.1斐波那契数列

在这里插入图片描述

解法一:递归

int fib(int N) {
    if (N == 1 || N == 2) return 1;
    return fib(N - 1) + fib(N - 2);
}

解法二:带备忘录的递归,自上到下

int fib(int N) {
    if (N < 1) return 0;
    // 备忘录全初始化为 0
    vector<int> memo(N + 1, 0);
    // 初始化最简情况
    return helper(memo, N);
}

int helper(vector<int>& memo, int n) {
    // base case 
    if (n == 1 || n == 2) return 1;
    // 已经计算过
    if (memo[n] != 0) return memo[n];
    memo[n] = helper(memo, n - 1) + 
                helper(memo, n - 2);
    return memo[n];
}

解法三:带备忘录的dp数组,自下到上

int fib(int N) {
    vector<int> dp(N + 1, 0);
    // base case
    dp[1] = dp[2] = 1;
    for (int i = 3; i <= N; i++)
        dp[i] = dp[i - 1] + dp[i - 2];
    return dp[N];
}

解法四:dp的状态压缩(之和相邻状态有关),自下到上

int fib(int n) {
    if (n == 2 || n == 1) 
        return 1;
    int prev = 1, curr = 1;
    for (int i = 3; i <= n; i++) {
        int sum = prev + curr;
        prev = curr;
        curr = sum;
    }
    return curr;
}

2.2 凑零钱

问题:给你k种面值的硬币,面值分别为c1, c2 … ck,每种硬币的数量无限,再给一个总金额amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。

解法一:自上到下备忘录

ef coinChange(coins: List[int], amount: int):
    # 备忘录
    memo = dict()
    def dp(n):
        # 查备忘录,避免重复计算
        if n in memo: return memo[n]

        if n == 0: return 0
        if n < 0: return -1
        res = float('INF')
        for coin in coins:
            subproblem = dp(n - coin)
            if subproblem == -1: continue
            res = min(res, 1 + subproblem)

        # 记入备忘录
        memo[n] = res if res != float('INF') else -1
        return memo[n]

    return dp(amount)

解法二:自下到上备忘录

int coinChange(vector<int>& coins, int amount) {
    // 数组大小为 amount + 1,初始值也为 amount + 1
    vector<int> dp(amount + 1, amount + 1);
    // base case
    dp[0] = 0;
    for (int i = 0; i < dp.size(); i++) {
        // 内层 for 在求所有子问题 + 1 的最小值
        for (int coin : coins) {
            // 子问题无解,跳过
            if (i - coin < 0) continue;
            dp[i] = min(dp[i], 1 + dp[i - coin]);
        }
    }
    return (dp[amount] == amount + 1) ? -1 : dp[amount];
}

2.3 打家劫舍

详解

不打劫相邻的,样例:
输入:[1,2,3,1]
输出:
4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

解法一:自上而下+备忘录

private int[] memo;
// 主函数
public int rob(int[] nums) {
    // 初始化备忘录
    memo = new int[nums.length];
    Arrays.fill(memo, -1);
    // 强盗从第 0 间房子开始抢劫
    return dp(nums, 0);
}

// 返回 dp[start..] 能抢到的最大值
private int dp(int[] nums, int start) {
    if (start >= nums.length) {
        return 0;
    }
    // 避免重复计算
    if (memo[start] != -1) return memo[start];

    int res = Math.max(dp(nums, start + 1), 
                    nums[start] + dp(nums, start + 2));
    // 记入备忘录
    memo[start] = res;
    return res;
}

解法二:自下而上+备忘录

类似于反过来的跳台阶,从后往前扫
 int rob(int[] nums) {
    int n = nums.length;
    // dp[i] = x 表示:
    // 从第 i 间房子开始抢劫,最多能抢到的钱为 x
    // base case: dp[n] = 0
    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];
}

解法三:相邻状态有关+状态压缩+自下而上

int rob(int[] nums) {
    int n = nums.length;
    // 记录 dp[i+1] 和 dp[i+2]
    int dp_i_1 = 0, dp_i_2 = 0;
    // 记录 dp[i]
    int dp_i = 0; 
    for (int i = n - 1; i >= 0; i--) {
        dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
        dp_i_2 = dp_i_1;
        dp_i_1 = dp_i;
    }
    return dp_i;
}

变形1:这些房子不是一排,而是围成了一个圈
分析:最后一个和最先一个不能同时存在,两周情况的最值

public int rob(int[] nums) {
    int n = nums.length;
    if (n == 1) return nums[0];
    return Math.max(robRange(nums, 0, n - 2), 
                    robRange(nums, 1, n - 1));
}

// 仅计算闭区间 [start,end] 的最优结果
int robRange(int[] nums, int start, int end) {
    int n = nums.length;
    int dp_i_1 = 0, dp_i_2 = 0;
    int dp_i = 0;
    for (int i = end; i >= start; i--) {
        dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
        dp_i_2 = dp_i_1;
        dp_i_1 = dp_i;
    }
    return dp_i;
}

变形2:房子在二叉树的节点上,相连的两个房子不能同时被抢劫
分析:相邻结点不相加

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 ? 
            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;
}

优化

int rob(TreeNode root) {
    int[] res = dp(root);
    return Math.max(res[0], res[1]);
}

/* 返回一个大小为 2 的数组 arr
arr[0] 表示不抢 root 的话,得到的最大钱数
arr[1] 表示抢 root 的话,得到的最大钱数 */
int[] dp(TreeNode root) {
    if (root == null)
        return new int[]{0, 0};
    int[] left = dp(root.left);
    int[] right = dp(root.right);
    // 抢,下家就不能抢了
    int rob = root.val + left[0] + right[0];
    // 不抢,下家可抢可不抢,取决于收益大小
    int not_rob = Math.max(left[0], left[1])
                + Math.max(right[0], right[1]);

    return new int[]{not_rob, rob};
}

3 子序列问题

  • 子序列:不连续
  • 字串:连续

满足条件的不连续子序列问题

3.1 编辑距离

leetcode72题目

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符

输入:word1 = “horse”, word2 = “ros”
输出:3
解释: horse -> rorse (将 ‘h’ 替换为 ‘r’) rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)

分析

  • 两个字符同时从后往前扫描
  • 相等则跳过不算操作数。
  • 不等考虑三种操作:str1 插入,操作数+1;str1删除,操作数+1;str1替换成str2对应字符,操作数+1。
    具体分析
if s1[i] == s2[j]:
    啥都别做(skip)
    i, j 同时向前移动
else:
    三选一:
        插入(insert)
        删除(delete)
        替换(replace)

在这里插入图片描述
优化一: 备忘录优化

def minDistance(s1, s2) -> int:

    memo = dict() # 备忘录
    def dp(i, j):
        if (i, j) in memo: 
            return memo[(i, j)]
        ...

        if s1[i] == s2[j]:
            memo[(i, j)] = ...  
        else:
            memo[(i, j)] = ...
        return memo[(i, j)]

    return dp(len(s1) - 1, len(s2) - 1)

优化二: DP table优化⭐⭐
在这里插入图片描述
题目中我给的解法:DP table优化

public int minDistance(String word1, String word2) {
        int len = word1.length();
        int len2 = word2.length();
//        dp[i][j]: str 1~i和str 1~j的最大距离,dp
//        从1开始一方面方便理解,一方面防止越界
        int[][] dp = new int[len + 1][len2 + 1];
//        base case:i/j ==0 :dp[j/i] = j/i操作数
        for(int i = 1 ; i<=len; i++){
            dp[i][0] = i;
        }
        for(int i = 1 ; i<=len2; i++){
            dp[0][i] = i;
        }
//            状态传递
        for(int i = 1; i <=len ; i++){
            for(int j = 1 ; j <= len2 ; 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 - 1][j] + 1,           //删除
                            Math.min(
                                    dp[i][j - 1] + 1, //插入
                                    dp[i - 1][j - 1] + 1 //赋值
                            )
                    );
                }
            }
        }
        return dp[len][len2];
    }

优化三: 相邻三状态有关,状态压缩
在这里插入图片描述

3.2 最长递增子序列LIS⭐

详解
问题
在这里插入图片描述
分析1:

  • dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
  • 则dp[i] = max(在i之前存在nums[j] < nums[i] 中最大的dp[j]+11
  • base case:第一次出现dp[i] = 1
    在这里插入图片描述
    在这里插入图片描述
    分析2:
    分发扑克牌+二分查找
    在这里插入图片描述
    代码
    在这里插入图片描述
    变形1:
    最长递增字串。
    dp的定义为0~i位置的最长递增字串的长度,当 nums[i]>nums[i-1]时候dp[i] = dp[i-1]+1。

3.3 信封嵌套问题

问题:
在这里插入图片描述
分析

  • 最长递增子序列LIS的变形
  • 要求每组数组的首位都要满足递增子序列
    在这里插入图片描述
    代码:
    LIS见上一节
    在这里插入图片描述
    在这里插入图片描述

3.4 最大子串和

连续子数组的最大和
问题
在这里插入图片描述

分析

  • nums[0…i]中的「最大的子数组和」为dp[i]。

代码

int maxSubArray(int[] nums) {
    int n = nums.length;
    if (n == 0) return 0;
    int[] dp = new int[n];
    // base case
    // 第一个元素前面没有子数组
    dp[0] = nums[0];
    // 状态转移方程
    for (int i = 1; i < n; i++) {
        dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
    }
    // 得到 nums 的最大子数组
    int res = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        res = Math.max(res, dp[i]);
    }
    return res;
}

状态压缩:

int maxSubArray(int[] nums) {
    int n = nums.length;
    if (n == 0) return 0;
    // base case
    int dp_0 = nums[0];
    int dp_1 = 0, res = dp_0;

    for (int i = 1; i < n; i++) {
        // dp[i] = max(nums[i], nums[i] + dp[i-1])
        dp_1 = Math.max(nums[i], nums[i] + dp_0);
        dp_0 = dp_1;
        // 顺便计算最大的结果
        res = Math.max(res, dp_1);
    }

    return res;
}

3.5 最长公共子序列LCS⭐

详情
leetcode1143
问题:

给你输入两个字符串s1和s2,请你找出他们俩的最长公共子序列,返回这个子序列的长度。
比如说输入s1 = “zabcde”, s2 = “acez”,它俩的最长公共子序列是lcs = “ace”,长度为 3,所以算法返回 3。

分析

  • 自上而下分析:dp(s1, i, s2, j)计算s1[i…]和s2[j…]的最长公共子序列长度。
// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j) {
    if (s1.charAt(i) == s2.charAt(j)) {
        return 1 + dp(s1, i + 1, s2, j + 1)
    } else {
        // s1[i] 和 s2[j] 中至少有一个字符不在 lcs 中,
        // 穷举三种情况的结果,取其中的最大结果
        return max(
            // 情况一、s1[i] 不在 lcs 中
            dp(s1, i + 1, s2, j),
            // 情况二、s2[j] 不在 lcs 中
            dp(s1, i, s2, j + 1),
            // 情况三、都不在 lcs 中
            dp(s1, i + 1, s2, j + 1)
        );
    }
}

代码

  • 自下而上:定义dp[i][j] 是 s1[…i] 和s2[…j]LCS
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];
}

变形1:删除使两字符串相等的操作数

先求lcs,每个字符串减去lcs长度,相加就是总操作数。
在这里插入图片描述

变形2:删除使其相等的ASCII的操作数之和

  • 删除的字符的 ASCII 码加起来是多少
  • 计算lcs长度时,如果一个字符串为空,那么lcs长度必然是 0;但是这道题如果一个字符串为空,另一个字符串必然要被全部删除,所以需要计算另一个字符串所有字符的 ASCII 码之和。
    在这里插入图片描述

变形3:包含另一个子序列的方案数

115不同的子序列

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示,3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

分析:

  • dp【i】【j】表示 s 1~i 包含 t 1~j的多少方案

  • 当 S[j] == T[i] , dp[i][j] = dp[i-1][j-1] + dp[i-1][j]; 考虑s的i是否用不用,用的话抵消掉,不用的话去找之前匹配的方案

  • 当 S[j] != T[i] , dp[i][j] = dp[i-1][j] 不相等s的i肯定不能用了,找之前的方案,按顺序的找

在这里插入图片描述

class Solution {
 public int numDistinct(String s, String t) {
        
        int len1 = s.length();
        int len2 = t.length();
        //dp【i】【j】表示 s 1~i 包含 t 1~j的多少方案
        int[][] dp = new int[len1 + 1][len2 + 1];
        //base case
        for(int i = 0; i <= len2 ;i++){
            dp[0][i] = 0;
        }
        for(int j = 0; j <= len1; j++){
            dp[j][0] = 1;
        }
//        自底向上遍历dp
        for(int i = 1 ;i <=len1 ;i++){
            for(int j = 1; j <= len2;j++){
                if(s.charAt(i-1) == t.charAt(j-1)){
                    //相等时考虑用不用i,用的话会匹配掉,不用的话去匹配j-1
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                }else{
                    //不相等的话只能舍弃i,也考虑了匹配的顺序性质。
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[len1][len2];
    }
}

3.6 最长回文子序列

子序列解题模板:最长回文子序列
问题
在这里插入图片描述

分析

  • dp[i][j]最长回文子序列的长度
  • 状态转移思路
if (s[i] == s[j])
    // 它俩一定在最长回文子序列中
    dp[i][j] = dp[i + 1][j - 1] + 2;
else
    // s[i+1..j] 和 s[i..j-1] 谁的回文子序列更长?
    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
  • 两种遍历方式:
    在这里插入图片描述

代码

int longestPalindromeSubseq(string s) {
    int n = s.size();
    // dp 数组全部初始化为 0
    vector<vector<int>> dp(n, vector<int>(n, 0));
    // base case
    for (int i = 0; i < n; i++)
        dp[i][i] = 1;
    // 反着遍历保证正确的状态转移
    for (int i = n - 1; i >= 0; i--) {
        for (int j = i + 1; j < n; j++) {
            // 状态转移方程
            if (s[i] == s[j])
                dp[i][j] = dp[i + 1][j - 1] + 2;
            else
                dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
        }
    }
    // 整个 s 的最长回文子串长度
    return dp[0][n - 1];
}

3.7 最长回文子串

法一

  • 分奇偶中心点
  • 依次从前往后扫描
    在这里插入图片描述

在这里插入图片描述

法二

  • 动态规划
    这个问题可以用动态规划方法解决,时间复杂度一样,但是空间复杂度至少要 O(N^2) 来存储 DP table。这道题是少有的动态规划非最优解法的问题。

4 0-1背包

问题
详情

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

  • 举个简单的例子,输入如下:
    背包:N = 3, W = 4
    物品的重量 wt = [2, 1, 3]
    物品的价值val = [4, 2, 3]

分析

  • 要求最大价值(因变量),限制因素为重量,数量(自变量)。
  • dp【i】【m】定位为前i个物品的不超过m重量的最大价值
  • 递推可行性分析:
    当使用到第i个物品时候,由于m的限制可能用不了,此时只能不用他,最大价值和用不用他没啥关系,则有dp[i][m]=dp[i-1][m]
    第二种情况用上第i个物品重量也不超m,可以用他,那此时的最大价值就要在之前的一个状态的而基础上加上i物品的价值,之前的那个状态就是不用他的那个最大价值,即dp[i][m] = dp[i-1][m-wt[i]]+val[i]
  • base case很好理解,i == 0 没有物品和m == 0没有重量,价值肯定是零。

代码

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];
}

4.1 背包变体1——分割等和子集

详情
问题

在这里插入图片描述

分析

  • 两个等和的包。从一个数组中拿物品。
  • 给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。
  • 限制条件:数量和重量,声明dp[i][j] = x,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包装满,若x为false,则说明不能恰好将背包装满。
  • 前i个数正好和为j的真假:i可以用可以不用,用的时候,dp[i][j] = dp[i-1][j-nums[i]],不能用的话,只能取决于之前的状态,dp[i][j] = dp[i-1][j],不管能不能用,只要一个满足他就为真

代码

bool canPartition(vector<int>& nums) {
    int sum = 0;
    for (int num : nums) sum += num;
    // 和为奇数时,不可能划分成两个和相等的集合
    if (sum % 2 != 0) return false;
    int n = nums.size();
    sum = sum / 2;
    vector<vector<bool>> 
        dp(n + 1, vector<bool>(sum + 1, false));
    // base case
    for (int i = 0; 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) {
               // 背包容量不足,不能装入第 i 个物品
                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];
}

状态压缩优化
注意j的倒序

bool canPartition(vector<int>& nums) {
    int sum = 0, n = nums.size();
    for (int num : nums) sum += num;
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    vector<bool> dp(sum + 1, false);
    // base case
    dp[0] = true;

    for (int i = 0; i < n; i++) 
        for (int j = sum; j >= 0; j--) 
            if (j - nums[i] >= 0) 
                dp[j] = dp[j] || dp[j - nums[i]];

    return dp[sum];
}

4.2 背包变体2——完全背包(重复使用组合)

详情
问题
物品重复使用(不考虑组合顺序,即组合问题)
在这里插入图片描述

分析

  • 限制条件:个数和总数 待求方法数。
  • 递归可行性,使用i个硬币时候方法数,和之前的不用他的方法数有关
  • dp[i][j]:前i个硬币组成j金额的方法数,那么如果用i的硬币的话,那么此时的方法数应该是dp[i][j-nums[i]],抵消掉该硬币的使用价值,也可用i,所以不是i-1;如果不用i的话,那就由dp[i-1][j],之前的i-1个硬币来凑j吧。

在这里插入图片描述

代码

int change(int amount, int[] coins) {
    int n = coins.length;
    int[][] dp = amount int[n + 1][amount + 1];
    // base case
    for (int i = 0; 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] 
                         + dp[i][j - coins[i-1]];
            else 
                dp[i][j] = dp[i - 1][j];
    }
    return dp[n][amount];
}

状态压缩

int change(int amount, int[] coins) {
    int n = coins.length;
    int[] dp = new int[amount + 1];
    dp[0] = 1; // base case
    //用到coin的方案计算
    for (int coin:conis)
        for (int j = 1; j <= amount; j++)
            if (j - coin >= 0)
                dp[j] += dp[j-coin];
    return dp[amount];
}

4.3 背包变体2——完全背包(重复使用排列)

问题

  • leetcode377
  • 给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4
输出:7
解释: 所有可能的组合为:
(1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。

分析
重复使用,并考虑组合顺序,排列

  • dp[i][j]定义为长度为i,和为j的组合种类。
  • 需要考虑最后一位取得是哪个num,dp[i][j] = dp[i-1][j-num1]+dp[i-1][j-num2]+…+dp[i-1][j-numn]
  • 排列与组合不同的是,循环在内部还是外部
  • 如果是排列,决策需要考虑每个数字 i 用分别可以用哪些num(硬币),所以 i 的遍历在外部
  • 如果是组合,决策就只需要考虑哪个 num(硬币)可用,所以 num 的遍历在外部,用到哪个计算哪个,减去当前的就是之前硬币组成的方案数。

代码

    public int combinationSum4(int[] nums, int target) {
        int len = nums.length;
        //f[i][j] 为组合长度为 ii,凑成总和为 jj 的方案数是多少
        int[][] dp = new int[len + 1][target+1];

        dp[0][0] = 1;

        int res = 0;
        for(int i = 1 ;i <= target;i++){
            for(int j = 0 ;j <= target;j++ ){
                for(int num:nums){
                    //排列问题,考虑最后一位用不用前i个的
                    if(j >= num){
                        dp[i][j] += dp[i-1][j-num];
                    }
                }
            }
            res+=dp[i][target];
        }
        return res;
    }

状态压缩优化

public int combinationSum5(int[] nums, int target) {
        int len = nums.length;
        //f[j] 不限制长度,凑成总和为 j 的方案数是多少
        int[] dp = new int[target+1];
        dp[0] = 1;
     
        for(int j = 1 ;j <= target;j++ ){
             for(int num:nums){
                 //排列问题,考虑最后一位用不用前i个的
                  if(j >= num){
                      dp[j] += dp[j-num];
                  }
               }
         }       
        return dp[target];
    }

5 股票买卖

详解

6 贪心算法

  • 每一步都做出一个局部最优的选择,最终的结果就是全局最优
  • 这是一种特殊性质,其实只有一小部分问题拥有这个性质。
  • 打牌不符合贪心,符合动规

6.1 区间调度问题⭐

详情
问题

  • 给你很多形如[start,end]的闭区间,请你设计一个算法,算出这些区间中最多有几个互不相交的区间。
  • 个例子,intvs=[[1,3],[2,4],[3,6]],这些区间最多有两个区间互不相交,即[[1,3],[3,6]],你的算法应该返回 2。注意边界相同并不算相交

分析

  • 使用贪心思想,若使区间的个数最多,应该使每个不相交的子区间长度更短,
  • 长度由start点和end点决定,start的越晚,end越早的长度就会越短。
  • 如果选择start晚的不能能保证一些start早点,end早点的更短区间不被忽略
  • 所以正确的贪心思想是:每次选择最早的不相交end,则保证了每次选择的区间是最小的,区间的总数就是最多的。
    在这里插入图片描述

代码
在这里插入图片描述

6.1.1 应用一:无重叠区间

问题
在这里插入图片描述
分析:

  • 求出最多有几个区间不会重叠,那么剩下的就是至少需要去除的区间。
int eraseOverlapIntervals(int[][] intervals) {
    int n = intervals.length;
    return n - intervalSchedule(intervals);
}

6.1.2 最少的箭射气球

问题
在这里插入图片描述
分析

  • 也是算不重叠的区间,几个不重叠的区间就需要几只箭。
  • 以前是不同区间的边界可以重叠,现在重叠边接算到一个区间内。
  • 变化就是找下一个区间时候的判断条件

代码

int findMinArrowShots(int[][] intvs) {
    // ...
    if(intvs.length == 0) return 0;
    Arrays.sort(intvs,new Comparator<int[]>(){
    	@Override
    	public int Compare(int[] a,int[] b){
    		return a[1]-b[1]; }
    });
    int count = 1;
    int x_end = intvs[0][1];
    for (int[] interval : intvs) {
        int start = interval[0];
        // 把 >= 改成 > 就行了
        if (start > x_end) {
            count++;
            x_end = interval[1];
        }
    }
    return count;
}

6.1.3 跳跃游戏

7博弈问题

详情

8 DP-路径问题

详情

  • 第一讲:62.不同路径(中等)
  • 第二讲:63.不同路径 II(中等)
  • 第三讲:64.最小路径和(中等)
  • 第四讲:120.三角形最小路径和(中等)
  • 第五讲:931.下降路径最小和(中等)
  • 第六讲:1289.下降路径最小和 II(困难)
  • 第七讲:1575.统计所有可行路径(困难)【记忆化搜索】
  • 第八讲:1575.统计所有可行路径(困难)【动态规划】
  • 第九讲:576.出界的路径数(中等)
  • 第十讲:1301.最大得分的路径数目(困难)
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zkFun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值