目录
理论基础
dp:拆解为多个子问题,并通过子问题的解来推出原问题的解
每个阶段进行全局状态的计算与更新
通用
背包
背包问题都是序列dp
选或不选,选了容量减少,不选容量不变
01:物品只能选一次
完全:物品可以反复选(递归i-1变i)
物品的重量就是每个物品所消耗的容量
每个物品的价值通常都是1
// 01背包问题模板
public class Knapsack01 {
public int knapsack01(int[] weights, int[] values, int W) {
int n = weights.length;
int[] dp = new int[W + 1];
// 遍历每个物品
for (int i = 0; i < n; i++) {
// 从右到左遍历,确保每个物品只能使用一次
for (int j = W; j >= weights[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[W];
}
}
// 完全背包问题模板
public class KnapsackComplete {
public int knapsackComplete(int[] weights, int[] values, int W) {
int n = weights.length;
int[] dp = new int[W + 1];
// 遍历每个物品
for (int i = 0; i < n; i++) {
// 从左到右遍历,表示可以选择多次
for (int j = weights[i]; j <= W; j++) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[W];
}
}
相关变形:至多/至少/恰好
各类 DP
线性 DP:每一步的决策基于前一个或几个状态, 通过滚动数组优化空间
区间 DP:基于某个区间(子数组或子字符串)计算最优解,然后递推整个区间的解
序列 DP:一般为二维数组,求解两个序列的最优编辑、匹配、比较方式
// 线性 DP 模板
public class LinearDP {
public int solve(int[] nums) {
int n = nums.length;
int[] dp = new int[n]; // 定义 DP 数组,表示以 i 结尾的最优解
// 初始化 dp[0],通常为问题给定的初始状态
dp[0] = nums[0];
// 状态转移
for (int i = 1; i < n; i++) {
dp[i] = Math.max(dp[i - 1], 0) + nums[i]; // 依赖前一个状态来推导
}
// 返回结果,通常需要从 dp 数组中选取最大值
int result = dp[0];
for (int i = 1; i < n; i++) {
result = Math.max(result, dp[i]);
}
return result;
}
}
// 序列 DP 模板
public class SequenceDP {
public int solve(int[] nums) {
int n = nums.length;
int[] dp = new int[n]; // 定义 DP 数组,表示到达第 i 个元素时的最优解
// 初始化 DP 数组
for (int i = 0; i < n; i++) {
dp[i] = 1; // 每个元素自身可以构成一个长度为 1 的子序列
}
// 状态转移
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
// 返回结果,通常为 dp 数组中的最大值
int result = 0;
for (int i = 0; i < n; i++) {
result = Math.max(result, dp[i]);
}
return result;
}
}
198.打劫
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
class Solution {
public int rob(int[] nums) {
int f0 = 0,f1 = 0;//初始
for (int x : nums) {
int newF = Math.max(f1, f0 + x);//最大值
f0 = f1;
f1 = newF;//递推
}
return f1;
}
}
70.爬楼梯
斐波那契数列变形 F(n)=F(n−1)+F(n−2) 0,1,1,2,3,5,8,13,21,34,55,89,…
完全背包问题,但没有用完全的方法做
class Solution {
public int climbStairs(int n) {
int f0 = 1;//一种
int f1 = 1;//一种
for (int i = 2; i <= n; i++) {
int newF = f1 + f0;//走法数
f0 = f1;
f1 = newF;
}
return f1;
}
}
118.杨辉三角
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
提示:
1 <= numRows <= 30
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> res = new ArrayList<>();
if (numRows == 0) return res;
res.add(new ArrayList<>());
res.get(0).add(1);// 第一行永远是 [1]
for (int i = 1; i < numRows; i++) {// 从第二行开始逐行生成
List<Integer> f0 = res.get(i - 1);//f0上一行的数据
List<Integer> f1 = new ArrayList<>();//f1当前行
f1.add(1);// 每一行的第一个元素是1
for (int j = 1; j < i; j++) {
f1.add(f0.get(j - 1) + f0.get(j));//相加得来
}
f1.add(1);// 每一行的最后一个元素也是1
// 将当前行添加到三角形中
res.add(f1);
}
return res;
}
}
322.换钱
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
完全背包问题
class Solution {
public int coinChange(int[] coins, int amount) {
int[] f = new int[amount + 1]; // f[i] 表示凑出金额 i 所需的最少硬币数
Arrays.fill(f, Integer.MAX_VALUE / 2); // 用一个很大的数初始化, 因为要找最小值
f[0] = 0; // 凑出金额 0 所需的硬币数为 0
for (int x : coins) { // 遍历每个硬币面值 x
for (int i = x; i <= amount; i++) { // i 从 x 开始,直到 amount
f[i] = Math.min(f[i], f[i - x] + 1);
}
}
int res = f[amount];
return res < Integer.MAX_VALUE / 2 ? res : -1;
}
}
279.完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 104
完全背包问题
class Solution {
private static final int N = 10000; // 预处理范围
private static final int[] f = new int[N + 1];//存储从0到N的最小完全平方数的个数
static {
Arrays.fill(f, Integer.MAX_VALUE);//初始化无穷大,表示无法凑出该值
f[0] = 0;
for (int i = 1; i * i <= N; i++) {
for (int j = i * i; j <= N; j++) { // j表示当前需要凑成的值
f[j] = Math.min(f[j], f[j - i * i] + 1);
}
}
}
public int numSquares(int n) {
return f[n];
}
}
139.单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
提示:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅由小写英文字母组成
wordDict 中的所有字符串 互不相同
完全背包
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int l= s.length();
boolean[] f = new boolean[l + 1];
f[0] = true;
for (int i = 1; i <= l; i++) {
for (String j : wordDict) {
int curl = j.length();
if (i >= curl && j.equals(s.substring(i - curl, i))) { //字典中的单词 j 是否与截取的子串相等
f[i] |= f[i - curl];//布尔“或”操作 //f[i] = f[i] || f[i - curl];一般这么写
}
}
}
return f[l];
}
}
416.分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
01背包
class Solution {
public boolean canPartition(int[] nums) {
int s = 0;
for (int x : nums) s += x;
if (s % 2 != 0) return false;
s /= 2;
boolean[] f = new boolean[s + 1];//是否可以实现
f[0] = true;
int s2 = 0;
for (int x : nums) {
s2 = Math.min(s2 + x, s); // 限制/剪枝
for (int j = s2; j >= x; j--) {
f[j] = f[j] || f[j - x];//是否为 true
}
}
return f[s];
}
}
竟然只有一道01背包的题
300.最长递增子序列
给你一个整数数组 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 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
线性dp
class Solution {
public int lengthOfLIS(int[] nums) {
int l = nums.length, res = 0;
int[] f = new int[l];
for (int i = 0; i < l; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) f[i] = Math.max(f[i], f[j]);
}
res = Math.max(res, ++f[i]);//再加一
}
return res;
}
}
152.乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续
子数组
(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
提示:
1 <= nums.length <= 2 * 104
-10 <= nums[i] <= 10
nums 的任何子数组的乘积都 保证 是一个 32-位 整数
线性 DP
class Solution {
public int maxProduct(int[] nums) {
if(nums.length == 0) return 0;
int ans = nums[0];
//两个mDP分别定义为以i结尾的子数组的最大积与最小积;
int[] maxDP = new int[nums.length];
int[] minDP = new int[nums.length];
maxDP[0] = nums[0]; minDP[0] = nums[0];
for(int i = 1; i < nums.length; i++){
//最大积的可能情况有:元素i自己本身,上一个最大积与i元素累乘,上一个最小积与i元素累乘;
//与i元素自己进行比较是为了处理i元素之前全都是0的情况;
maxDP[i] = Math.max(nums[i], Math.max(maxDP[i-1]*nums[i], minDP[i-1]*nums[i]));
minDP[i] = Math.min(nums[i], Math.min(maxDP[i-1]*nums[i], minDP[i-1]*nums[i]));;
ans = Math.max(ans, maxDP[i]);
}
return ans;
}
}
1143.最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。
线性dp
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
char[] s1 = text1.toCharArray();
char[] s2 = text2.toCharArray();
int l1 = s1.length;
int l2 = s2.length;
int[][] f = new int[2][l2 + 1];
for (int i = 0; i < l1; i++) {
for (int j = 0; j < l2; j++) {
// 如果s1[i]和s2[j]相等,则长度加1;否则,取前一状态的最大值
f[(i + 1) % 2][j + 1] = s1[i] == s2[j] ? f[i % 2][j] + 1 :
Math.max(f[i % 2][j + 1], f[(i + 1) % 2][j]);
}
}
return f[l1 % 2][l2];
}
}
72.距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成
和上一题基本类似,线性dp
62.不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109
class Solution {
public static int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int i = 0; i < n; i++) dp[0][i] = 1;//初始化
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {//画图可理解
dp[i][j] = dp[i-1][j]+dp[i][j-1];}
}
return dp[m-1][n-1];//从(0 , 0) 位置出发,到(m - 1, n - 1)终点
}
}
64.最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 200
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
//初始化DP数组的第一行和第一列
//左边或上边的网格的最小路径和,再加上这个网格的值
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];
}
}
5.最长回文子串
给你一个字符串 s,找到 s 中最长的
回文
子串
。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母组成
public class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) return s;
int maxLen = 1;
int begin = 0;
boolean[][] dp = new boolean[len][len];//dp[i][j]表示s[i...j]是否是回文串
for (int i = 0; i < len; i++) dp[i][i] = true; // 初始化
char[] charArray = s.toCharArray();
for (int L = 2; L <= len; L++) {// 枚举左边界
for (int i = 0; i < len; i++) {
int j = L + i - 1; // 由 L 和 i 可以确定右边界,即 j - i + 1 = L
if (j >= len) break; // 如果右边界越界,就可以退出当前循环
if (charArray[i] != charArray[j]) dp[i][j] = false;// 如果两端的字符不相等,那么s[i...j]不是回文串
else {
if (j - i < 3) dp[i][j] = true;// 如果子串长度小于3,那么s[i...j]是回文串
else dp[i][j] = dp[i + 1][j - 1];// 如果s[i+1...j-1]是回文串,那么s[i...j]也是回文串
}
if (dp[i][j] && j - i + 1 > maxLen) { // 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文
maxLen = j - i + 1;
begin = i;// 此时记录回文长度和起始位置
}
}
}
return s.substring(begin, begin + maxLen);
}
}
32.最长有效括号h
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号
子串
的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
提示:
0 <= s.length <= 3 * 104
s[i] 为 '(' 或 ')'
说到括号匹配就会想起栈,感觉栈更好理解
class Solution {
public int longestValidParentheses(String s) {
Stack<Integer> st = new Stack<Integer>(); // 栈用于存储左括号的索引
int res = 0;
for(int i = 0, start = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') st.add(i);// 如果是左括号,入栈
else {// 如果是右括号
if (!st.isEmpty()) {// 如果栈不为空,说明有匹配的左括号
st.pop(); // 匹配的左括号出栈
if (st.isEmpty()) res = Math.max(res, i - start + 1); // 如果栈为空,说明当前有效括号从 start 到 i
else res = Math.max(res, i - st.peek());// 栈不为空,说明还有未匹配的左括号
} else start = i + 1; // 如果栈为空,说明没有匹配的左括号,更新起始位置
}
}
return res;
}
}
dp法
class Solution {
public int longestValidParentheses(String s) {
int res = 0;
int[] f= new int[s.length()];
for (int i = 1; i < s.length(); i++) {// 从第二个字符开始遍历
// 如果当前字符是右括号
if (s.charAt(i) == ')') {
// 如果前一个字符是左括号,那么以当前字符结尾的最长有效括号的长度就是以第i-2个字符结尾的最长有效括号的长度加2
if (s.charAt(i - 1) == '(') {
f[i] = (i >= 2 ? f[i - 2] : 0) + 2;
}
// 如果前一个字符也是右括号,那么我们需要检查前一个最长有效括号的左边是否有左括号
else if (i - f[i - 1] > 0 && s.charAt(i - f[i - 1] - 1) == '(') {
// 如果有,那么以当前字符结尾的最长有效括号的长度就是前一个最长有效括号的长度加上以第i-dp[i-1]-2个字符结尾的最长有效括号的长度加2
f[i] = f[i - 1] + ((i - f[i - 1]) >= 2 ? f[i - f[i - 1] - 2] : 0) + 2;
}
res = Math.max(res, f[i]);
}
}
return res;
}
}