动态规划的提示
- 多阶段决策求最优解
- 一维数组:一维动态规划 -> 写出状态转移方程,然后进行空间压缩
- 二维数组:二维动态规划 -> 二维状态转移表法 or 状态转移方程法
基本动态规划:一维
好像都不太容易画递归树
class Solution {
public:
int climbStairs(int n) {
// 我的写法
/* 动态规划
1. 问题划分:是一个多阶段决策问题,每次决定爬1或2
2. 定义状态dp[i]:表示爬到第i阶,一共有dp[i]种方法
3. 状态转移方程:dp[i] = dp[i-1] + dp[i-2]
*/
if (n <= 2) return n;
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; ++i)
{
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
// 空间压缩写法:dp[i]只跟i-1和i-2状态有关系,所以只要定义prevprev、prev和cur三个变量即可
if(n <= 2) return n;
int prevprev = 1, prev = 2, cur;
for(int i = 3; i <= n; ++i)
{
cur = prevprev + prev;
prevprev = prev;
prev = cur;
}
return cur;
}
};
class Solution {
public:
int rob(vector<int>& nums) {
/* 动态规划
我最开始的思路(有问题):
1. 问题划分:每次觉得要不要偷这个房屋,求能偷到的最高金额,是一个多阶段决策求最优解问题
2. 定义状态dp[i]表示以会偷第i间房屋结尾时,能偷到的最高金额
3. 状态转移方程:dp[i] = max(dp[i - 2] + nums[i], dp[i])
正确题解思路:
1. 定义状态dp[i]表示,到了第i间房屋时,能偷到的最高金额
2. 状态转移方程:dp[i] = max(dp[i-1], dp[i-2]+nums[i]),第一项表示不偷第i间,第二项表示偷第i间
*/
// (建议使用~不易错)我自己按照正确解题思路的写法:多写了一些边界条件
int n = nums.size();
if(n == 1) return nums[0];
if(n == 2) return max(nums[0], nums[1]);
vector<int> dp(n, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < n; ++i)
{
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
// leetcode 101的写法:dp设置成长度为n+1的数组,在最开头补一个0,这样可以少写边界条件;但这样要注意dp[i]对应的是nums[i-1]
int n = nums.size();
if(n == 0) return 0;
vector<int> dp(n + 1, 0);
dp[1] = nums[0];
for(int i = 2; i <= n; ++i)
{
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - 1]);
}
return dp[n];
// 转成空间压缩写法
int n = nums.size();
if(n == 1) return nums[0];
if(n == 2) return max(nums[0], nums[1]);
int prevprev = nums[0], prev = max(nums[0], nums[1]), cur;
for(int i = 2; i < n; ++i)
{
cur = max(prev, prevprev + nums[i]);
prevprev = prev;
prev = cur;
}
return cur;
}
};
class Solution {
public:
int rob(vector<int>& nums) {
/* 动态规划 + 环形处理
1. 分别求nums[0:-1)和nums[1:-1]的最大金额,再求最大
2. 实现细节:
(1) c++没有直接对数组切片的功能,所以helper里指定起始下标和终止下标;并且因为dp的下标和nums的下标难对应,所以采用空间压缩的写法忽略dp的下标
(2) 空间压缩时,看dp[i]依赖到了什么,比如这里dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]),dp[i]只依赖到了i-1和i-2,所以只需要prevprev, prev, cur三个变量就好
*/
int n = nums.size();
if (n == 0) return 0;
if (n == 1) return nums[0];
if (n == 2) return max(nums[0], nums[1]);
// 前面的边界,保证进到helper里时,左下标小于右下标
return max(helper(nums, 0, n - 1), helper(nums, 1, n)); // 左闭右开
}
int helper(vector<int> nums, int idx_begin, int idx_end)
{
int prevprev = 0;
int prev = nums[idx_begin];
int cur = 0;
for(int i = idx_begin + 1; i < idx_end; ++i)
{
cur = max(prev, prevprev + nums[i]);
prevprev = prev;
prev = cur;
}
return cur;
}
};
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
/* 动态规划
1. 依次查看每个数,决定是否可以作为等差数列的结尾元素
2. 定义状态dp[i]:以第i个元素结尾的等差数列的个数
3. 状态转移方程:如果第i个元素可以作为等差数列的结尾(nums[i]-nums[i-1]=nums[i-1]-nums[i-2]),则dp[i] = dp[i-1] + 1;否则dp[i]=0
4. 结果是sum(dp[i]), i从0到n
*/
int n = nums.size();
if(n <= 2) return 0;
vector<int> dp(n, 0);
for(int i = 2; i < n; ++i)
{
if(nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) dp[i] = dp[i - 1] + 1;
}
int res = 0;
for(int i = 0; i < n; ++i) res += dp[i];
return res;
}
};
基本动态规划:二维
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
/* 动态规划
1. 每个位置上决策往下还是往右,所以是多阶段决策求最优解问题,用动态规划
2. 定义状态dp[i, j],是到达位置ij上的最短路径
3. 状态转移方程:dp[i, j] = min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
4. 结果是dp[n-1, n-1]
5. 可以画出递归树,状态f(i,j,l)为到第ij位置的路径为l,会发现有重复的状态ij,只要记录它的最短路径即可,所以可以用动态规划
*/
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
dp[0][0] = grid[0][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) dp[i][0] = dp[i - 1][0] + grid[i][0];
for(int i = 1; i < m; ++i) // 这里i从1开始,而不是2
{
for(int j = 1; j < n; ++j) // 这里j从1开始,而不是2
{
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
// 进行空间压缩:从行上进行压缩,dp[j] = min(老dp[j], 新dp[j - 1])+grid[i][j],所以要先更新j-1再更新j,所以j从左往右遍历
int m = grid.size(), n = grid[0].size();
vector<int> dp(n, 0);
// 初始化第一个值
dp[0] = grid[0][0];
// 初始化第0行
for(int j = 1; j < n; ++j)
{
dp[j] = dp[j - 1] + grid[0][j];
}
// 对于第1到m-1行,进行状态转移
for(int i = 1; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(j == 0) dp[j] = dp[j] + grid[i][j];
else dp[j] = min(dp[j], dp[j - 1]) + grid[i][j];
}
}
return dp[n - 1];
}
};
class Solution {
public:
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
/* 动态规划
1. 为什么不用广度优先搜索,而是用动态规划的状态转移?因为广度优先搜索要对每个位置上进行四向搜索,复杂度太高了
2. dp[i][j]为第ij个元素到最近的0的距离
3. 如果mat[i][j]==0,dp[i][j]=0;如果mat[i][j]!=0, dp[i][j] = min(dp[i-1][j], dp[i+1][j], dp[i][j-1], dp[i][j+1]) + 1。但这里的四向搜索需要满足无后效性,也就是后一个状态完全由之前的状态决定,在一个位置上通过一次遍历无法让四周先变到前序状态,只能先左上来一次、再右下来一次,取最小。
*/
// // 我尝试的实现(未成功)
// // 实现时遇到的问题及处理方法:
// // 1.dp里的初始值设置为INT_MAX,+1时会上溢;-> 设置为INT_MAX-1,每次取min时,如果周围都是未接触到0的,那么这里会是min(INT_MAX-1, INT_MAX-1 + 1)就还是INT_MAX-1
// // 2.边界条件太多了;-> 对于00位置、无需特殊处理,因为要么是0、要么是INT_MAX-1;对于第0行、第0列,与其他行列合并处理了,合并的方法是,把min(dp[i-1][j], dp[i][j-1])拆成了先看列:if(j>0) min(dp[i][j], dp[i][j-1]+1), 再看行:if(i>0) min(dp[i][j], dp[i-1][j]+1)。这样规避了对于第0行、第0列的特殊边界处理。
// int m = mat.size(), n = mat[0].size();
// vector<vector<int>> dp(m, vector<int>(n, INT_MAX));
// // 左上->右下遍历
// // 先初始化[0][0]
// // 再初始化第1行
// // 再初始化第1列
// // 后续填表
// dp[0][0] = mat[0][0] == 0? 0 : INT_MAX;
// for(int j = 1; j < n; ++j) mat[0][j] == 0? 0 : dp[0][j] = dp[0][j-1] + 1;
// // 右下->左上遍历
// // 先初始化[m-1][n-1]
// // 再初始化第m-1行
// // 再初始化第n-1列
// // 后续填表
// 看答案后的实现
int m = mat.size(), n = mat[0].size();
vector<vector<int>> dp(m, vector<int> (n, INT_MAX - 1));
// 左上->右下
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(mat[i][j] == 0) dp[i][j] = 0;
else
{
if(j > 0) dp[i][j] = min(dp[i][j], dp[i][j - 1] + 1);
if(i > 0) dp[i][j] = min(dp[i][j], dp[i - 1][j] + 1);
}
}
}
// 右下->左上
for(int i = m - 1; i >= 0; --i)
{
for(int j = n - 1; j >= 0; --j)
{
if(mat[i][j] != 0)
{
if(j < n - 1) dp[i][j] = min(dp[i][j], dp[i][j + 1] + 1);
if(i < m - 1) dp[i][j] = min(dp[i][j], dp[i + 1][j] + 1);
}
}
}
return dp;
}
};
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
/* 动态规划
1. 为什么不是深度优先搜索,而是动态规划?如果是找不规则的最大岛屿面积、那是在每个位置上去进行深度优先搜索;但这里有要求形状是正方形,就需要对ij位置的元素进行规则判断、和决策是否加入,所以是动态规划;
2. 定义状态dp[i][j]:以ij位置的元素为右下角的最大边长
3. 状态转移方程:
if matrix[i][j] == 0, dp[i][j] = 0;
else:
(最开始的想法,dp[i][j]由dp[i-1][j-1]决定,想错了
if(matrix[i][j-1] == 1 && matrix[i-1][j] == 1 && matrix[i-1][j-1]==1) dp[i][j] = dp[???])
正确的想法,dp[i][j]由dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]里的最小值决定,因为是个正方形
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1 // 别忘了加一
*/
int m = matrix.size(), n = matrix[0].size(), max_size = 0;
vector<vector<int>> dp(m, vector<int> (n, INT_MAX));
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(matrix[i][j] == '0') dp[i][j] = 0;
else
{
dp[i][j] = 1;
if(i >= 1 && j >= 1)
{
dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1;
}
}
max_size = max(max_size, dp[i][j]);
}
}
return max_size * max_size;
}
};
分割类型题
class Solution {
public:
int numSquares(int n) {
/* 动态规划(自己完全没想出来怎么做)
1. 为什么用动态规划,而不是回溯、贪心?这道题一定有解(比如n个1),并且可能有多种解(比如剩下的全用1来补充),所以会涉及到达到同状态下的重复子问题,所以适合用动态规划而非回溯,来减少复杂度;不用贪心是因为贪心不一定是最优,而本题要最优解。
2. 我自己的思考是:可以看作是从1,2,3...,int(sqrt(n))里可放回地采样,求和为指定值时,最少采样次数,有点像找零钱的问题。但有点定义不出来状态,不知道这样看成完全背包问题要怎么解???
3. 答案的做法:分割问题当前状态由前序满足条件的状态决定、而非由前序连续状态决定。定义dp[i]为数字i最少可由dp[i]个完全平方数相加得到,那么dp[i] = 1 + min{dp[i-1], dp[i-4], ..., dp[i-j*j]},实现的时候,i从0推到n;对于每个i,j从1遍历到j*j<=i。
*/
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for(int i = 1; i <= n; ++i)
{
for(int j = 1; j * j <= i; ++j) // j * j <= i,不是j * j < i,不然下面i-j*j会出错
{
dp[i] = min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
};
class Solution {
public:
int numDecodings(string s) {
/* 动态规划
1. 定义dp[i]:到第i个字符为止,最多有dp[i]种解码方法
2. 状态转移方程:以下两种情况解码方法数量之和
(1)第一种情况,只使用第i个字符解码。条件为nums[i]!=0,解码方法数为dp[i-1];
(2)第二种情况,使用第i-1和第i个字符解码。条件为i>1 && nums[i-1] != 0 && nums[i-1]*10+nums[i]<=26,解码方法数为dp[i-2];
3. 写代码时注意,dp多加了一个标志位,所以dp里的i对应到nums里是i-1。
*/
int n = s.size();
vector<int> dp(n + 1); // 初始化为全0
dp[0] = 1; // 方便dp[i] = dp[i - 1]
for(int i = 1; i <= n; ++i)
{
if(s[i - 1] - '0' != 0) dp[i] = dp[i - 1];
if(i > 1 && (s[i - 2] - '0') != 0 && ((s[i - 2] - '0') * 10 + (s[i - 1] - '0')) <= 26) dp[i] += dp[i - 2];
}
return dp[n];
}
};
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
/* DP
1. dp[i] = true/false: 到s的第i个字符为止,是否可分割
2. 状态转移方程:dp[i] = dp[i] || dp[i - len] if i > len && s.substr(i-len, len)==word for word in wordDict
3. 注意s.substr()第一个参数是起始位置,第二个参数是长度(不是结束位置)
*/
int n = s.size();
vector<bool> dp(n + 1, false);
dp[0] = true;
for(int i = 1; i <= n; ++i)
{
for(auto & word : wordDict)
{
int len = word.length();
if(i >= len && s.substr(i - len, len) == word) dp[i] = dp[i] || dp[i - len]; // 本来应该是i-len+1,但s比dp要少1,所以是i-len
}
}
return dp[n];
}
};
class Solution {
public:
int integerBreak(int n) {
/* 动态规划 + 分割问题
1. 定义dp[i]为将正整数i拆分为多个正整数的和时的最大乘积
// 我最初的想法,错误的,这样的问题是少算了拆成两个数字(i-j)*j的情况,也即部分dp没有正确初始化
2. dp[i] = max(dp[i-1]*1, dp[i-2]*2, ..., dp[i-j]*j, dp[1]*(i-1)),j从1到i-1
// 官方的做法,对于j从1到i-1的每个j,要从以下两种情况里求max
(1)i拆成2个数字,所以乘积是(i-j)*j
(2)i拆成多个数字,所以最大乘积是dp[i-j]*j
*/
// // 我最初的想法,错误的
// vector<int> dp(n + 1, 1);
// for(int i = 1; i <= n; ++i)
// {
// for(int j = 1; j < i; ++j)
// {
// dp[i] = max(dp[i], dp[i - j] * j);
// }
// }
// return dp[n];
// 官方做法
vector<int> dp(n + 1, 1);
for(int i = 2; i <= n; ++i)
{
for(int j = 1; j < i; ++j)
{
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); // 别忘了dp[i]自己记录住最大值
}
}
return dp[n];
}
};
子序列问题
- 动态规划解法,时间复杂度 ( N 2 ) (N^2) (N2)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
/* 动态规划
定义状态:dp[i]表示以位置i结尾的最长严格递增子序列的长度
状态转移方程:dp[i] = max(dp[j] + 1, dp[i]), j from [0, i) && nums[j] < nums[i]
*/
int n = nums.size();
if(n < 2) return n;
vector<int> dp(n, 1);
for(int i = 1; i < n; ++i)
{
for(int j = 0; j < i; ++ j)
{
if(nums[j] < nums[i]) dp[i] = max(dp[j] + 1, dp[i]);
}
}
int res = 1;
for(int i = 0; i < n; ++i)
{
res = max(res, dp[i]);
}
return res;
}
};
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
/* 动态规划 + 最长递增子序列的变形
1. “以任何顺序”指的是从左到右但可跳跃?
2. dp[i]为以第i个数对结尾的最长数对链的长度
3. dp[i]为以下情况求最大:
(1) 不包含以前的数对,dp[i] = 1
(2) 包含以前的数对,dp[i] = max(dp[i-1]+1, dp[i-2]+1, ..., dp[i-j]+1, ..., dp[1] + 1), 遍历所有满足nums[i-j][1] < nums[i][0]的j
然而“以任何顺序”并非如此,它是指任意顺序,所以以上想法不对。得先对pairs排序,再按以上方法。
*/
sort(pairs.begin(), pairs.end()); //默认递增排序,默认先按第一个元素、再按第二个元素排序
int n = pairs.size();
vector<int> dp(n + 1, 1);
dp[0] = 0;
for(int i = 1; i <= n; ++i)
{
for(int j = 1; j < i; ++j)
{
if(pairs[i - j - 1][1] < pairs[i - 1][0]) // pairs的索引比dp少1
{
dp[i] = max(dp[i], dp[i - j] + 1);
}
}
}
return dp[n];
}
};
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
/* 动态规划 + 最长递增子序列的变形
// 我的思路(忽略掉):不太fancy,需要额外记录sign
1. dp[i]为以第i个元素结尾的最长摆动子序列的长度, sign[i]为以第i个元素结尾的最长摆动子序列最后一个差值
2. dp[i] = max(dp[i], dp[i - j] + 1) 对于所有满足(nums[i] - nums[i - j]) * sign[i - j] < 0的j,j从1到i-2
// 按照这个链接里的解题思路,定义up和down两个状态:https://leetcode.cn/problems/wiggle-subsequence/solution/tan-xin-si-lu-qing-xi-er-zheng-que-de-ti-jie-by-lg/
1. up[i]表示nums[0..i]中最后两个数字递增的最长摆动序列长度(不一定以i结尾);
down[i]表示nums[0...i]中最后两个数字递减的最长摆动序列长度(不一定以i结尾)
2. 状态转移:up一定由前序down来,down一定又前序up来
(1)如果nums[i] == nums[i-1], 那么up[i]=up[i-1], down[i]=down[i-1]
(2)如果nums[i] < nums[i-1],
(2.1)如果up[i-1]是以i-1结尾的, 那么down[i] = up[i-1] + 1;
(2.2)如果up[i-1]不是以i-1结尾的,是以j结尾的,j<i-1,那么从j到i-1一定是递减的,不然up[i-1]就大于up[j]了,所以up[j]=up[j+1]=...=up[i-1],所以down[i] = up[i-1]+1.
(3)如果nums[i] > nums[i-1],
(3.1)如果down[i-1]是以i-1结尾的,那么up[i] = down[i-1] + 1;
(3.2)如果down[i-1]不是以i-1结尾的,是以j结尾的,j<i-1,那么从j到i-1一定是递增的,要不然down[i-1]就大于down[j]了,所以down[j]=down[j+1]=...=down[i-1],所以up[i] = down[i-1] + 1。
*/
int n = nums.size();
vector<int> up(n, 1);
vector<int> down(n, 1);
for(int i = 1; i < n; ++i)
{
if(i == 1)
{
if(nums[i] > nums[i - 1]) up[i] = 2;
else if (nums[i] < nums[i - 1]) down[i] = 2;
}
else
{
if(nums[i] == nums[i - 1])
{
up[i] = up[i - 1];
down[i] = down[i - 1];
}
else if(nums[i] < nums[i - 1])
{
down[i] = up[i - 1] + 1;
up[i] = up[i - 1];
}
else
{
up[i] = down[i - 1] + 1;
down[i] = down[i - 1];
}
}
}
return max(up[n - 1], down[n - 1]);
}
};
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
/* 动态规划
1. 公共子序列:可以不连续;公共子串:必须连续
2. 定义状态dp[i][j]:text1[0,...,i]和text2[0,...,j]的最长公共子序列长度,最长公共子序列不必非得以i、j结尾
3. 状态转移方程(自己没太想清楚,看了理论讲解,又看了答案):
if text1[i] == text2[j]: dp[i][j] = max(dp[i-1][j-1]+1, dp[i-1][j], dp[i][j-1]),第一项是指什么都不删,后面两项是说要删掉text2[j]和text1[i]。实际上dp[i-1][j]和dp[i][j-1]一定是>=dp[i-1][j-1]的,并且不会大超过1,所以只需要dp[i][j] = dp[i-1][j-1]+1
else: dp[i][j] = max(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]),第一项是指删掉text1[i]和text2[j],后面两项是说要删掉text2[j]和text1[i]。实际上dp[i-1][j]和dp[i][j-1]一定是>=dp[i-1][j-1]的,所以只需要dp[i][j] = max(dp[i-1][j], dp[i][j-1])
*/
int m = text1.length(), n = text2.length();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // 这样定义,dp[0][j]和dp[i][j]可以不用特殊初始化了
for(int i = 1; i <= m; ++i)
{
for(int j = 1; j <= n; ++j)
{
if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m][n];
}
};
子串问题
class Solution {
public:
int maxSubArray(vector<int>& nums) {
/* 动态规划
1. 子数组=子串=subarray,是连续的;子序列=subsequence,是可以不连续的
2. 定义dp[i]为以第i个元素为结尾的子数组的最大和,最后的结果是遍历dp各元素里的最大值
3. 转移方程dp[i] = max(nums[i], dp[i-1] + nums[i])
*/
// // 我自己最开始的写法,会有单个负值元素过不了,因为如果dp[0]初始为0之后,如果nums[0]是负值,那么dp[1]会是0、而不是负值。
// int n = nums.size();
// // vector<int> dp(n + 1, 0); // 这里要初始化为可能的最小值
// vector<int> dp(n + 1, -10001);
// for(int i = 1; i <= n; ++i)
// {
// dp[i] = max(nums[i - 1], dp[i - 1] + nums[i - 1]);
// }
// int res = dp[0];
// for(int i = 1; i <= n; ++i)
// {
// res = res < dp[i]? dp[i] : res;
// }
// return res;
// 空间压缩的写法
int n = nums.size();
int prev = nums[0], cur;
int res = prev;
for(int i = 1; i < n; ++i)
{
cur = max(nums[i], prev + nums[i]);
res = max(res, cur);
prev = cur; // 别丢了
}
return res;
}
};
背包问题
有N个物品和容量为W的背包,每个物品有自己的体积w和价值v,求拿哪些物品可以使得背包所装下的物品等总价值最大。定义dp[N][W]为决策到第i个物品(不管选几次、都在一行里)、容量为W时的最大价值。
1. 0-1背包问题:每个物品只能选0次或1次
二维转移矩阵
状态转移方程、代码
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w]+v) if j>=w else dp[i-1][j]
- dp[i-1][j]是对于当前物品的决策是不选
- dp[i-1][j-W]+v表示对于当前物品的决策是选
空间优化
从右向左遍历,因为j依赖上次的j-w,所以要避免j-w先更新:
2. 完全背包问题:每个物品可以选任意次
二维转移矩阵
可以理解成:第i个物品可以进行多次决策,每次决策要么不选(向下)、要么在上次选了的基础上再选一次(向右)
状态转移方程、代码
dp[i][j] = max(dp[i-1][j], dp[i][j-w]+v) if j>=w else dp[i-1][j]
- dp[i-1][j]是对于当前物品的决策是不选
- dp[i][j-W]+v表示对于当前物品的决策是选(在对当前物品多次决策的基础上选,所以是i、不是i-1)
空间优化
从左向右遍历,因为j依赖这次的j-w,所以要让j-w先更新:
class Solution {
public:
bool canPartition(vector<int>& nums) {
/* 动态规划
1. 0-1背包问题:对于nums里每个数,做拿/不拿的决策,使得拿的总值达到sum(nums)/2。每个数只有一次选择机会,所以是0-1背包,不是完全背包。
2. 定义状态dp[i][j]=true/false,表示选择到第i个样本时,总和能否达到j这个值。i从0到nums.size(),j从0到sum(nums)/2。
3. 状态转移:对于第i个元素做决策,
(1)情况一:不选,dp[i][j] = dp[i - 1][j]
(2)情况二:选,dp[i][j] = dp[i - 1][j - nums[i]]
*/
int m = accumulate(nums.begin(), nums.end(), 0);
if(m % 2) return false;
int n = nums.size();
vector<vector<bool>> dp(n + 1, vector<bool> (m / 2 + 1, false));
dp[0][0] = true;
for(int i = 1; i <= n; ++i)
{
for(int j = 0; j <= m / 2; ++j)
{
if(j == 0) dp[i][j] = true;
else if(nums[i - 1] <= j) dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; // 注意nums的i比dp的i要小1
else dp[i][j] = dp[i - 1][j];
}
}
return dp[n][m / 2];
}
};
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
/* 动态规划
1. 0-1背包问题,对于strs里的每个string逐个决策要不要选,决策时需受限于0的个数(最多m个)和1的个数(最多n个)
2. 定义状态dp[i][j][k],表示决策到第i个string为止,最多j个0、k个1的情况下,最多可以构成多少个string
3. 状态转移方程:对于第i个字符串,假设有n0个0、n1个1
(1)如果n0>j or n1>k,dp[i][j][k] = dp[i-1][j][k]
(2)如果n0<=j && n1<=k,dp[i][j][k] = max(dp[i-1][j][k], dp[i][j-n0][k-n1]+1)
4. 代码写法:注意同时返回两个数的函数,返回值类型是pair<int, int>,return是make_pair(a, b),返回值是auto [a, b]
*/
int num_str = strs.size();
vector<vector<vector<int>>> dp(num_str + 1, vector<vector<int>> (m + 1, vector<int> (n + 1, 0)));
for(int i = 1; i <= num_str; ++i)
{
for(int j = 0; j <= m; ++j)
{
for(int k = 0; k <= n; ++k)
{
auto [n0, n1] = count(strs[i - 1]); // 注意返回两个数的写法
if(n0 > j || n1 > k) dp[i][j][k] = dp[i - 1][j][k];
else dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - n0][k - n1] + 1);
}
}
}
return dp[num_str][m][n];
}
pair<int, int> count(string & str) // 同时返回两个数的函数
{
int n0 = 0, n1 = 0;
for(auto & c: str) // 这样是char,下面得是单引号
{
if(c == '0') ++n0;
else if(c == '1') ++n1;
}
return make_pair(n0, n1); // 同时返回两个数的函数
}
};
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
/* 动态规划:0-1背包问题
1. 定义dp[i][j]为选择到第i个元素、target等于j时,不同表达式的数目
2. 状态转移方程:dp[i][j]可以从左上or右上转移过来
(1)是由'+'来的:if nums[j]<=j, dp[i][j] += dp[i-1][j-nums[j]]
(2)是由'-'来的:if nums[j]>=j, dp[i][j] += dp[i-1][j+nums[j]]
3. 可以由右侧转移过来,所以j的范围不只是0到target,而是[0,sum-1]、sum、[sum+1, 2*sum]一共2*sum+1个,也即全负到全正向右平移sum
4. target在题目里可以是负数,不能直接作为下标,要做一下hash映射为非负整数,也即统一加sum;
我的思路跟他的一样:https://leetcode.cn/problems/target-sum/solution/dong-tai-gui-hua-si-kao-quan-guo-cheng-by-keepal/
*/
int n = nums.size();
int sum = accumulate(nums.begin(), nums.end(), 0); // 还有个起始值0
vector<vector<int>> dp(n, vector<int> (2 * sum + 1, 0));
// 边界初始化
if(nums[0] == 0) dp[0][0 + sum] = 2; // 0向右平移sum,如果是0加正负号都可
else
{
dp[0][-nums[0] + sum] = 1; // 加负号后,向右平移sum
dp[0][nums[0] + sum] = 1; // 加正号后,向右平移sum
}
// 填表
for(int i = 1; i < n; ++i)
{
for(int j = 0; j < 2 * sum + 1; ++j)
{
if(j - nums[i] >= 0) dp[i][j] += dp[i - 1][j - nums[i]]; // 记得nums的索引是i不是j;这里是在j的基础上往左往右看,所以跟平移量sum无关
if(j + nums[i] < 2 * sum + 1) dp[i][j] += dp[i - 1][j + nums[i]];
}
}
return (target + sum >= 2 * sum + 1) || (target + sum < 0)? 0 : dp[n - 1][target + sum]; // target向右平移sum。如果target超出范围的话说明没有能够达成的
}
};
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
/* 动态规划:
1. 每个硬币可以多次选择,所以是完全背包问题,二维dp,行表示第i种硬币(一共coins.size()种,同一种不管选几次都只影响到列金额)、列表示金额j;
2. 状态转移方程dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i]] + 1)
3. 可以将二维转到一维,进行空间优化
*/
// // 二维
// vector<vector<int>> dp(coins.size() + 1, vector<int> (amount + 1, amount + 1)); // 选择到第i种硬币时,能够达到金额j的最少硬币个数
// for(int i = 0; i <= coins.size(); ++i) dp[i][0] = 0;
// for(int i = 1; i <= coins.size(); ++i)
// {
// int w = coins[i - 1]; // 注意dp的行列各扩充了一格,对应到原来的数组时就要-1
// for(int j = 1; j <= amount; ++j)
// {
// if(j >= w) dp[i][j] = min(dp[i - 1][j], dp[i][j - w] + 1);
// else dp[i][j] = dp[i - 1][j];
// }
// }
// return dp[coins.size()][amount] > amount ? -1 : dp[coins.size()][amount];
// 空间优化:一维dp
vector<int> dp(amount + 1, amount + 1); // 去掉i
dp[0] = 0;
for(int i = 1; i <= coins.size(); ++i)
{
int w = coins[i - 1];
for(int j = 1; j <= amount; ++j)
{
if(j >= w) dp[j] = min(dp[j], dp[j - w] + 1);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
字符串编辑
class Solution {
public:
int minDistance(string word1, string word2) {
/* 动态规划
1. 定义dp[i][j]为将word1[0,...,i-1]转换成word2[0,...,j-1]的最少操作数
2. (我自己初步想的,想的不对)dp[i][j] = 以下情况求最小
(1) 不改动:dp[i][j]
(2) 插入一个字符:dp[i][j] + 1
(3) (i > 1)删除一个字符:dp[i - 1][j] + 1
(4) 替换一个字符:dp[i][j] + 1
2. 分情况讨论:
(1)如果word1[i] == word2[j],则不用改动:dp[i][j] = dp[i-1][j-1]
(2)如果word1[i] != word2[j]:
a. 替换word1[i]为word2[j](与替换word2[j]为word1[i]等价):dp[i-1][j-1] + 1(这里比较难想到,看等价就理解了)
b. 删除word1[i](与插入word2[j]等价):dp[i][j] = dp[i - 1][j] + 1
c. 插入word1[i](与删除word2[j]等价):dp[i][j] = dp[i][j - 1] + 1 (这里也比较难想到,看等价就理解了)
*/
int m = word1.length(), n = word2.length();
vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0));
// 边界初始化
dp[0][0] = 0;
for(int j = 1; j <= n; ++j) dp[0][j] = dp[0][j - 1] + 1; // 相当于插入word1
for(int i = 1; i <= m; ++i) dp[i][0] = dp[i - 1][0] + 1; // 相当于删除word1
// 填表
for(int i = 1; i <= m; ++i)
{
for(int j = 1; j <= n; ++j)
{
if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = min(dp[i - 1][j - 1] + 1, min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
}
}
return dp[m][n];
}
};
class Solution {
public:
int minDistance(string word1, string word2) {
/* 动态规划
1. 定义dp[i][j]为word1[0...i-1]和word2[0...j-1]相同所需要的最小步数
2. dp[i][j]由以下情况求min
(1)如果word1[i]==word2[j],不需要删除,dp[i-1][j-1]
(2)如果word1[i]!=word2[j]
(2.1)只删除word1[i],dp[i - 1][j] + 1
(2.2)只删除word2[j],dp[i][j - 1] + 1
(2.3)既删除word1[i]又删除word2[j],dp[i - 1][j - 1] + 2 // 这种情况忘记考虑了
*/
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m + 1, vector<int> (n + 1, m + n)); // 初始化值是既删除word1的所有值又删除word2的所有值,所以是m+n
// 边界初始化
dp[0][0] = 0; // 别忘了
for(int i = 1; i <= m; ++i) // word2为空,删word1
{
dp[i][0] = i;
}
for(int j = 1; j <= n; ++j) // word1为空,删word2
{
dp[0][j] = j;
}
// 填表
for(int i = 1; i <= m; ++i)
{
for(int j = 1; j <= n; ++j)
{
if(word1[i - 1] == word2[j - 1]) dp[i][j] = min(dp[i][j], dp[i - 1][j - 1]); // 注意这里word1和word2的下标,要比dp少1
else dp[i][j] = min(min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), dp[i - 1][j - 1] + 2);
}
}
return dp[m][n];
}
};
class Solution {
public:
int minSteps(int n) {
/* 动态规划(我的想法)
1. dp[i]为输出恰好i个字符时,需要使用的最少操作次数
2. dp[i]可由以下状态转移过来,取min
(1)这次是复制,dp[i] + 1
(2)上次是复制,这次是粘贴,dp[i / 2] + 1 if i % 2 == 0
(3)上次是粘贴,这次也是粘贴,但不知道粘贴了几次,所以不知道是粘贴了多少次,所以对所有可取余的遍历一遍。可以和(2)合并
*/
vector<int> dp(n + 1, n); // 默认初始值:到n,需要复制1次、粘贴n-1次,一共n次
dp[0] = 0;
dp[1] = 0;
for(int i = 2; i <= n; ++i)
{
for(int j = 2; j <= n; ++j) // j是份数
{
if(i % j == 0)
{
dp[i] = min(dp[i], dp[i / j] + 1 + j - 1); // 复制1次,粘贴j-1次
}
}
}
return dp[n];
}
};
class Solution {
public:
bool isMatch(string s, string p) {
/* 动态规划
1. 定义状态dp[i][j]为s[0...i-1]和p[0...j-1]是否匹配
2. 状态转移方程的分析过程见官方题解下的评论区
(1)如果p[j-1]是字母或'.':
a. 如果p[j-1]和s[i-1]匹配,那么dp[i][j] = dp[i-1][j-1]
b. 如果p[j-1]和s[i-1]不匹配,那么dp[i][j] = false
(2)如果p[j-1]是'*':
a. 把p[j-2]和p[j-1]当成一个整体,去和s[i-1]匹配:
a1: 如果匹配
1) 一方面可以看当前:dp[i][j] = dp[i][j-2]
2)另一方面也可以继续向前匹配,变成和s[i-2]是否匹配的子问题: dp[i][j] = dp[i-1][j]
a2: 如果没匹配(也即匹配0次,*是支持匹配0次的),只能看当前了:dp[i][j] = dp[i][j-2]
*/
int m = s.size();
int n = p.size();
vector<vector<bool>> dp(m + 1, vector<bool> (n + 1, false));
/* 边界初始化
1. s空p空时,匹配
2. s非空p空时,不匹配,默认false
3. s空p非空时,p是"#*#*#*..."才匹配
*/
dp[0][0] = true;
for(int j = 2; j <= n; j += 2)
{
if(p[j - 1] == '*') dp[0][j] = dp[0][j - 2];
}
/* 开始实现状态转移
*/
for(int i = 1; i <= m; ++i) // 从s非空开始考虑
{
for(int j = 1; j <= n; ++j) // 从p非空开始考虑
{
if(p[j - 1] != '*')
{
if(p[j - 1] == '.' || p[j - 1] == s[i - 1]) dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = false;
}
else // p是*的话(一定不是p下标0的位置(也即j下标1,也即j下标一定>=2),因为题目里已经说了出现*前面一定有有效字符)
{
if(p[j - 2] == '.' || p[j - 2] == s[i - 1]) dp[i][j] = dp[i][j-2] || dp[i - 1][j];
else dp[i][j] = dp[i][j-2];
}
}
}
return dp[m][n];
}
};
股票交易
class Solution {
public:
int maxProfit(vector<int>& prices) {
/* 动态规划
1. 定义dp[i]为到第i天为止能获取到的最大利润
2. 状态转移方程:dp[i] = max(dp[i-1], prices[i] - min_price) if prices[i] > min_price else dp[i] = dp[i-1]
*/
int min_price = prices[0];
vector<int> dp(prices.size(), 0);
for(int i = 1; i < prices.size(); ++i)
{
if(prices[i] > min_price)
{
dp[i] = max(dp[i - 1], prices[i] - min_price);
}
else
{
dp[i] = dp[i - 1];
}
min_price = min(min_price, prices[i]);
}
return dp[prices.size() - 1];
/* 方法二:动态规划:每天都买卖,看状态转移
1. 定义buy和sell分别是买入后手里的最大利润、卖出后手里的最大利润
2. 按天更新buy和sell,最后的动作一定是卖出,所以sell就是返回值
*/
int buy = INT_MIN, sell = 0;
for(int i = 0; i < prices.size(); ++i)
{
buy = max(buy, -prices[i]);
sell = max(sell, buy + prices[i]); // 这里buy是目前手头的钱,卖出相当于+prices;隔天才能卖在这里也不会影响,因为如果是当前买卖buy + prices[i]是0
}
return sell;
}
};
class Solution {
public:
int maxProfit(vector<int>& prices) {
/* 动态规划
1. 状态转移:买-卖-买-卖...
2. 定义状态:buy买入后当前最大利润,sell卖出后当前最大利润
*/
int buy = INT_MIN, sell = 0;
for(int i = 0; i < prices.size(); ++i)
{
buy = max(buy, sell - prices[i]);
sell = max(sell, buy + prices[i]);
}
return sell;
}
};
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
/* 动态规划
1. 状态转移:买1-卖1-买2-卖2-...-买k-卖k
*/
vector<int> buy(k + 1, INT_MIN), sell(k + 1, 0);
for(int i = 0; i < prices.size(); ++i)
{
for(int j = 1; j <= k; ++j)
{
// buy[j] = max(buy[j - 1], sell[j - 1] - prices[i]);
// sell[j] = max(sell[j - 1], buy[j] + prices[i]);
buy[j] = max(buy[j], sell[j - 1] - prices[i]); // max(不买,买)
sell[j] = max(sell[j], buy[j] + prices[i]); // // max(不卖,卖)
}
}
return sell[k];
}
};
class Solution {
public:
int maxProfit(vector<int>& prices) {
/* 动态规划
1. 状态转移:买-卖-冷冻期-买-卖...
*/
int buy = INT_MIN, sell = 0, cold = 0;
for(int i = 0; i < prices.size(); ++i)
{
buy = max(buy, cold - prices[i]); // buy从cold来
cold = max(cold, sell); // cold从sell来,放在sell之前是因为sell要变更了,得先记住它???
sell = max(sell, buy + prices[i]); // sell从buy来
}
return sell;
}
};
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
/* 动态规划
1. 无限次交易
2. 有交易费用,在buy时算上fee即可
*/
int buy = INT_MIN, sell = 0;
for(int i = 0; i < prices.size(); ++i)
{
buy = max(buy, sell-prices[i]-fee);
sell = max(sell, buy + prices[i]);
}
return sell;
}
};