- 0-1背包问题(二维dp)
- 0-1背包升级版(二维dp)
- 完全背包(费解)如凑领钱(一维、二维dp)
- 子序列问题(重要)
- 最长递增子序列(一维dp)
- 最长公共子序列(二维dp)
- 最长回文子序列(二维dp)
- 最短编辑距离(二维dp)
- 最短路径(机器人走路)(二维dp)
第一步要明确两点,「状态」和「选择」。明确dp
数组的定义
状态有两个:「背包的容量」、「可选择的物品」
选择有两个:「装进背包」、「不装进背包」
几种状态就是几层for循环,也就是几维dp
第二步,根据「选择」,思考状态转移的逻辑
第三步,确定初始条件
labuladong的动归
例题一:0-1背包升级版
给你一个可装载重量为
W
的背包和N
个物品,每个物品有重量和价值两个属性。其中第i
个物品的重量为wt[i]
,价值为val[i]
,现在让你用这个背包装物品,最多能装的价值是多少?
- 定义二维
dp[i][j]
:对于前i种物品,当前背包重量为j时,能够获得的最大价值为dp[i][j]
。我们要求的就是dp[n][w]
- 数组元素之间的关系:
- 当
j - wt[i - 1] < 0
时:dp[i][j] = dp[i - 1][j]
。表示当前剩余的容量装不下当前的物品,只能继承上一个装填的 - 当
j - wt[i - 1] >= 0
时:装或者不装。dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - wt[i - 1]] + val[i - 1])
- 当
- 数组元素之间的关系:
dp[i] = dp[i - 1] + dp[i - 2]
- 初始条件:
dp[...][0] = 0;dp[0][...] = 0
。表示物品或者容量为0时,当前价值为0
// 经典动态规划:0-1背包问题
int baseDP(int w, int n, int weight[], int value[]){
// 定义二维状态数组
int dp[n + 1][w + 1];
// 初始化边界
for(int i = 0; i <= n; i++)
dp[i][0] = 0;
for(int i = 0; i <= w; i++)
dp[0][i] = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= w; j++){
if(j - weight[i - 1] < 0)
// 装不下,直接继承前一个状态的
dp[i][j] = dp[i - 1][j];
// dp[i][j] = 择优(选择1, 选择2)
// 背包装或者不装,两者择优选择
else
// 这个地方如果是一维数组的话就是倒着来的
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
}
}
return dp[n][w];
}
例题二:0-1背包变体
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:每个数组中的元素不会超过 100;数组的大小不会超过 200
示例 1:输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
- **定义
dp[i][j]
:对于前i个物品,当前背包的容量为j时,若dp[i][j]
为true,则说明可以装满。我们要求的就是dp[N][sum/2]
** - 数组元素之间的关系:
- 当
j - wt[i - 1] < 0
时:dp[i][j] = dp[i - 1][j]
。表示当前剩余的容量装不下当前的物品,只能继承上一个装填的 - 当
j - wt[i - 1] >= 0
时:dp[i][j] = dp[i - 1][j] || dp[i - 1][j - wt[i - 1]]
- 当
- 初始条件:初始条件:
dp[...][0] = true;dp[0][...] = false
。表示物品为0,价值不为0时,肯定装不满
class Solution {
public:
bool ans = false;
// 这种方法超时
void dfs(vector<int> &num, vector<int> a, vector<int> b, int index){
if(index == num.size()){
int sumA = 0, sumB = 0;
for(int i = 0; i < a.size(); i++)
sumA += a[i];
for(int i = 0; i < b.size(); i++)
sumB += b[i];
if(sumA == sumB)
ans = true;
return ;
}
// 放入A背包
a.push_back(num[index]);
dfs(num, a, b, index + 1);
a.pop_back();
b.push_back(num[index]);
dfs(num, a, b, index + 1);
b.pop_back();
}
bool canPartition(vector<int>& nums) {
// 定义状态数组
int sum = 0;
for(int i : nums)
sum += i;
if(sum % 2 != 0)
return false;
int n = nums.size();
sum = sum / 2;
bool dp[n + 1][sum + 1];
// 初始化
for(int i = 0; i <= n; i++)
dp[i][0] = true;
for(int i = 0; i <= sum; i++)
dp[0][i] = false;
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];
}
};
例题三:完全背包问题
凑领钱1:给你
k
种面值的硬币,面值分别为c1, c2 ... ck
,每种硬币的数量无限,再给一个总金额amount
,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。
- 定义一维数组
dp[i]
:当前总金额为i时,需要最少dp[i]
个硬币凑出这个金额。我们要求的就是dp[amount]
- 数组元素之间的关系:
dp[i] = min(dp[i - coin] + 1)
- 初始条件:
dp[0] = 0
。即金额为0就需要0枚硬币
int coinChangeDP(vector<int> &coins, int amount){
// 初始化备忘录
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
// 填表
for(int i = 1; i < dp.size(); i++){
// 内层循环,找最小
for(int coin : coins){
if(i - coin < 0)
continue;
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
凑领钱2:给定不同面额的硬币和一个总金额,写出函数来计算可以凑成总金额的硬币组合数,假设每种面额的硬币有无限个。
- 定义二维数组
dp[i][j]
:当前总金额为j时,前i个物品可能有dp[i][j]
种可能凑齐。我们要求的就是dp[n][amount]
- 数组元素之间的关系:
- 当
j-coins[i - 1] < 0
时:dp[i][j] = dp[i - 1][j]
。表示当前容量装不下当前的硬币,只能继承上一个状态的 - 当
j-coins[i - 1] >= 0
时:dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]]
- 当
- 初始条件:
dp[i][0] = 1;dp[0][i] = 0;
int change(int amount, vector<int>& coins) {
if(amount == 0 && coins.size() == 0)
return 1;
int n = coins.size();
int dp[n + 1][amount + 1];
// 初始化
for(int i = 0; i <= n; i++)
dp[i][0] = 1;
for(int i = 0; i <= amount; i++)
dp[0][i] = 0;
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
// 注意这里是i不是i-1了,这样就保证了物品可以选无数次,如果是i-1的话,就是普通背包,只能选一次
// 这个地方如果用一维数组,那么就是顺着来的
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
}
}
return dp[n][amount];
}
例题四:最长公共子序列
解决两个字符串的动态规划问题,一般都是用两个指针i,j
分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。都是建立一个二维的dp数组。
求两个字符串的 LCS 长度:
输入: str1 = "abcde", str2 = "ace" 输出: 3 解释: 最长公共子序列是 "ace",它的长度是 3
- 定义二维
dp[i][j]
:表示str1
的(0, i)子序列与str2
的(0, j)子序列的最长公共序列。我们要求的就是dp[m][n]
- 数组元素之间的关系:
- 当
str1[i] = str2[j]
时:dp[i][j] = dp[i - 1][j - 1] + 1
- 当
str1[i] != str2[j]
时:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
- 当
- 初始条件:
dp[...][0] = dp[0][...] = 0
int myMax(int a, int b, int c){
return max(max(a, b), c);
}
int longestComStr(string s1, string s2){
int m = s1.size(), n = s2.size();
int dp[m + 1][n + 1];
// 初始化
for(int i = 0; i <= m; i++)
dp[i][0] = 0;
for(int i = 0; i <= n; i++)
dp[0][i] = 0;
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(s1[i - 1] == s2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
// 这个地方写成dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])就行了
dp[i][j] = myMax(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
return dp[m][n];
}
例题五:求两个字符串的最小编辑距离
和上一个题一样,一般来说,处理两个字符串的动态规划问题,都是按本文的思路处理,建立 DP table。为什么呢,因为易于找出状态转移的关系。这里的dp(i)(j)数组表示的是 s1[0..i] 和 s2[0..j] 的最小编辑距离。
- 定义二维数组
dp[i][j]
:当字符串s1长度为i,字符串s2长度为j时,它们的最短编辑距离是dp[i][j]
- 数组元素之间的关系:
- 当
s1[i - 1] == s2[j - 1]
时:dp[i][j] = dp[i - 1][j - 1]
- 当
s1[i - 1] != s2[j - 1]
时:dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
- 当
- 初始条件:
dp[i][0] = i;dp[0][i] = i
int min(int a, int b, int c){
return min(min(a, b), c);
}
int minDistance(string s1, string s2){
int m = s1.size(), n = s2.size();
int dp[m + 1][n + 1];
// 初始化
for(int i = 0; i <= m; i++)
dp[i][0] = i;
for(int i = 0; i <= n; i++)
dp[0][i] = i;
for(int i = 1; i <= m; i++){
for(int j = 1;j <= n; j++){
if(s1[i - 1] == s2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else{
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
// 储存着整个s1和s2的最小编辑距离
return dp[m][n];
}
总结子序列问题模板
首先注意区分一个问题:
- 子序列:可以不连续的子字符串/子数组
- 子串:必须是连续的子字符串/子数组
遇到子序列问题,首先想到两种动态规划思路,然后根据实际问题看看哪种思路容易找到状态转移关系。
这类问题都是让你求一个最长子序列,因为最短子序列就是一个字符嘛,没啥可问的。一旦涉及到子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。
1 第一种思路模板是一维的 dp 数组
最长递增子序列(注意是序列,可以不连续)
- 定义一维
dp[i]
:数组中以num[i]
结尾的最长递增序列为dp[i]
。我们要求的就是所有的dp[i]
中最大的那一个 - 数组元素之间的关系:
dp[i] = max(dp[i], dp[j] + 1)
,其中num[j] < num[i]
- 初始条件:
dp[...] = 1
,保证最短为1
int lengthOfLIS(int nums[], int n){
vector<int> dp(n, 1);
for(int i = 0; i < n; i++){
for(int j = 0; j < i; j++){
if(nums[j] < nums[i])
dp[i] = max(dp[i], dp[j] + 1);
}
}
int ans = INT_MIN;
for(int i = 0; i < n; i++)
ans = max(ans, dp[i]);
return ans;
}
2 第二种思路模板是二维的 dp 数组
这种思路数组含义又分为「只涉及一个字符串」和「涉及两个字符串」两种情况
2.1 涉及两个字符串/数组时
- 最长公共子序列
- 最短编辑距离
2.2 涉及一个字符串/数组时
最长回文子序列(注意,和最长回文子串不一样,子序列可以不连续)
- 定义二维
dp[i][j]
数组:在子串s[i..j]
中,最长回文子序列的长度为dp[i][j]
。我们要求的就是dp[0][n - 1]
- 数组元素之间的关系
- 当
s[i] = s[j]
时:dp[i][j] = dp[i + 1][j - 1] + 2
- 当
s[i] != s[j]
时:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
- 当
- 初始条件:
dp[i][i] = 1
为了保证每次计算dp[i][j]
,左、下、左下三个方向的位置已经被计算出来,只能斜着遍历或者反着遍历,本例选择反着遍历:
// 反着遍历
int longestPalindromeSubseq(string s){
int n = s.size();
int dp[n][n];
// 初始化
memset(dp, 0, sizeof(dp));
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];
}
帅地的动归
1 一维dp
例题一:青蛙跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级台阶总共有多少种跳法?
- 定义一维
dp[i]
:跳上一个i
级的台阶共有dp[i]
种跳法,我们要求的就是dp[n]
- 数组元素之间的关系:
dp[i] = dp[i - 1] + dp[i - 2]
- 初始条件:
dp[0] = 0;dp[1] = 1;dp[2] = 2
完整代码
// 跳台阶问题
// dp[n]表示跳上一个n阶台阶共有dp[n]种跳法
int f(int n){
if(n <= 2)
return n;
int dp[n + 1];
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
2 二维dp
例题二:机器人走路(不含权值)
⼀个机器⼈位于⼀个 m x n ⽹格的左上⻆ (起始点在下图中标记为“Start” )。
机器⼈每次只能向下或者向右移动⼀步。机器⼈试图达到⽹格的右下⻆(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
- 定义二维
dp[i][j]
:当机器人从左上角走到(i, j)这个位置,共有dp[i][j]
种路径,我们要求的就是dp[m - 1][n - 1]
- 数组元素之间的关系:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
- 初始条件:
dp[...][0] = 1;dp[0][...] = 1
,因为第一行只能往左走,第一列只能往下走
完整代码
// 机器人走路(无路径权值)
// dp[i][j]表示当机器人从左上角走到(i,j)这个位置时,一共有dp[i][j]种路径
int f(int m, int n){
if(m < 0 || n < 0)
return 0;
int dp[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];
}
例题三:机器人走路的最短路径(含权值)
给定⼀个包含⾮负整数的 m x n ⽹格,请找出⼀条从左上⻆到右下⻆的路径,使得路径上的数字总和为最⼩。
- 定义二维
dp[i][j]
:当机器人从左上角走到(i, j)这个位置的最短路径,我们要求的就是dp[m - 1][n - 1]
- 数组元素之间的关系:
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + val[i][j]
- 初始条件:
dp[i][0] = dp[i - 1][0] + val[i][0];dp[0][i] = dp[0][i - 1] + val[0][i]
完整代码
// 机器人走路,有路径权值
// dp[i][j]表示机器人从左上角走到(i,j)这个位置的最短路径值
int f(int val[][], int m, int n){
int dp[m][n];
dp[0][0] = val[0][0];
for(int i = 1; i < m; i++)
dp[i][0] = dp[i - 1][0] + val[i][0];
for(int i = 1; i < n; i++)
dp[0][i] = dp[0][i - 1] + val[0][i - 1];
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + val[i][j];
}
}
return dp[m - 1][n - 1];
}
例题四:最短编辑距离
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使⽤的最少操作数 。
你可以对⼀个单词进⾏如下三种操作:插⼊⼀个字符 删除⼀个字符 替换⼀个字符
- 定义二维数组
dp[i][j]
:当字符串s1长度为i,字符串s2长度为j时,它们的最短编辑距离是dp[i][j]
- 数组元素之间的关系:
- 当
s1[i - 1] == s2[j - 1]
时:dp[i][j] = dp[i - 1][j - 1]
- 当
s1[i - 1] != s2[j - 1]
时:dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
- 当
- 初始条件:
dp[i][0] = i;dp[0][i] = i
完整代码
int min(int a, int b, int c){
return min(min(a, b), c);
}
int minDistance(string s1, string s2){
int m = s1.size(), n = s2.size();
int dp[m + 1][n + 1];
// 初始化
for(int i = 0; i <= m; i++)
dp[i][0] = i;
for(int i = 0; i <= n; i++)
dp[0][i] = i;
for(int i = 1; i <= m; i++){
for(int j = 1;j <= n; j++){
if(s1[i - 1] == s2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else{
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
// 储存着整个s1和s2的最小编辑距离
return dp[m][n];
}
王争老师的动归
例题一:0-1背包问题
//weight:物品重量,n:物品个数,w:背包可承载重量
// 二维dp
public int knapsack(int[] weight, int n, int w) {
boolean[][] states = new boolean[n][w+1]; // 默认值false
states[0][0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
if (weight[0] <= w) {
states[0][weight[0]] = true;
}
for (int i = 1; i < n; ++i) { // 动态规划状态转移
for (int j = 0; j <= w; ++j) {// 不把第i个物品放入背包
if (states[i-1][j] == true) states[i][j] = states[i-1][j];
}
for (int j = 0; j <= w-weight[i]; ++j) {//把第i个物品放入背包
if (states[i-1][j]==true) states[i][j+weight[i]] = true;
}
}
for (int i = w; i >= 0; --i) { // 输出结果
if (states[n-1][i] == true) return i;
}
return 0;
}
// 一维dp
public static int knapsack2(int[] items, int n, int w) {
boolean[] states = new boolean[w+1]; // 默认值false
states[0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
if (items[0] <= w) {
states[items[0]] = true;
}
for (int i = 1; i < n; ++i) { // 动态规划
for (int j = w-items[i]; j >= 0; --j) {//把第i个物品放入背包
if (states[j]==true) states[j+items[i]] = true;
}
}
for (int i = w; i >= 0; --i) { // 输出结果
if (states[i] == true) return i;
}
return 0;
}