模版:
穷举 「状态转移方程」「备忘录」
明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
背包 状态有两个,就是「背包的容量」和「可选择的物品
# 自底向上迭代的动态规划
# 初始化 base case
dp[0][0][...] = base case
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
最长递增子序列
题目要求:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
难度:🌟🌟
解答:
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;
}
2.最长公共子序列
题目要求:
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
难度:🌟🌟
解答:子序列可以是不连续的;子数组(子字符串)需要是连续的
dp[i] 定义为 nums[0:i] 中想要求的结果;当两个数组或者字符串要用动态规划时,可以把动态规划定义成两维的 dp[i][j] ,其含义是在 A[0:i] 与 B[0:j] 之间匹配得到的想要的结果。
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
//dp[i][j] 表示 text1[0:i-1] 和 text2[0:j-1] 的最长公共子序列
int n1=text1.length();
int n2=text2.length();
int[][] dp = new int[n1+1][n2+1];
for (int i=1;i<=n1;i++){
for (int j=1;j<=n2;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][j-1],dp[i-1][j]);
}
}
}
return dp[n1][n2];
}
}
3.分割等和子集 子集背包问题
题目要求:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
难度:🌟🌟
解答:
转化为0-1背包问题
被包容量 sum/2 boolean[][] dp = new boolean[n + 1][sum + 1];
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num:nums) {
sum+=num;
}
if (sum % 2 != 0) return false;
int n = nums.length;
sum = sum / 2;
boolean[][] dp = new boolean[n + 1][sum + 1];
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){ //容量不足
dp[i][j]=dp[i-1][j];
}else {
dp[i][j]=dp[i-1][j] || dp[i][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
4.零钱兑换 完全背包问题
题目要求:
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
难度:🌟🌟
解答:
若只使用 coins 中的前 i 个(i 从 1 开始计数)硬币的面值,若想凑出金额 j,有 dp[i][j] 种凑法。
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
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];
}
}
5.目标和 回溯
回溯模版
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
题目要求:
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
难度:🌟🌟
解答:回溯
int result = 0;
public int findTargetSumWays(int[] nums, int target) {
if (nums.length == 0) return 0;
backtrack(nums, 0, target);
return result;
}
public void backtrack(int[] nums, int i, int remain){
if (i==nums.length){
if(remain == 0){
result++;
}
return;
}
//选择-号
remain=remain-nums[i];
backtrack(nums,i+1,remain);
remain=remain+nums[i];
//选择+号
remain=remain+nums[i];
backtrack(nums,i+1,remain);
remain=remain-nums[i];
}