动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
对于动态规划问题,代码随想录的解析方法是拆解为如下五步曲,这五步都搞清楚了,动态规划的题目做起来就会比较顺畅。
至于为什么要先确定递推公式,然后在考虑初始化,是因为有一些情况是递推公式决定了dp数组的初始化情况。
找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样;如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了;如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
注意要点:
下面贴出代码:
class Solution {
public:
int fib(int n) {
if (n <= 1) {return n;}
int dp[2] = {0};
dp[1] = 1;
for (int i = 2; i <= n; i++)
{
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
};
int fib(int n){
if (n <= 1) {return n;}
int* dp = (int* )malloc(sizeof(int) * 2);
dp[0] = 0, dp[1] = 1;
for (int i = 2; i <= n; i++)
{
int now = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = now;
}
return dp[1];
}
注意要点:
下面贴出代码:
class Solution {
public:
int climbStairs(int n) {
if (n <= 2) {return n;}
int dp[2] = {1};
dp[1] = 2;
for (int i = 3; i <= n; i++)
{
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
};
int climbStairs(int n){
if (n <= 1) {return n;}
int* dp = (int* )malloc(sizeof(int) * (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];
}
注意要点:
下面贴出代码:
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int dp[2] = {0};
for (int i = 2; i <= cost.size(); i++)
{
int now = min(dp[0] + cost[i - 2], dp[1] + cost[i - 1]);
dp[0] = dp[1];
dp[1] = now;
}
return dp[1];
}
};
int minCostClimbingStairs(int* cost, int costSize){
int* dp = (int* )malloc(sizeof(int) * (costSize + 1));
dp[0] = 0, dp[1] = 0;
for (int i = 2; i <= costSize; i++)
{
dp[i] = fmin((dp[i - 1] + cost[i - 1]), (dp[i - 2] + cost[i - 2]));
}
return dp[costSize];
}
注意要点:
下面贴出代码:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int> dp(n);
for (int i = 0; i < n; i++) {dp[i] = 1;}
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++) {dp[j] += dp[j - 1];}
}
return dp[n - 1];
}
};
int uniquePaths(int m, int n){
//开辟二维数组空间
int** dp = (int** )malloc(sizeof(int* ) * m);
for (int i = 0; i < m; i++)
{
dp[i] = (int* )malloc(sizeof(int) * 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];
}
注意要点:
下面贴出代码:
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if (obstacleGrid[m - 1][n - 1] || obstacleGrid[0][0]) {return 0;}
vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m && !obstacleGrid[i][0]; i++) {dp[i][0] = 1;}
for (int j = 1; j < n && !obstacleGrid[0][j]; j++) {dp[0][j] = 1;}
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
{
if (obstacleGrid[i][j]) {continue;}
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
int uniquePathsWithObstacles(int** obstacleGrid, int obstacleGridSize, int* obstacleGridColSize){
//开辟二维数组空间
int m = obstacleGridSize;
int n = obstacleGridColSize[0];
int** dp = (int** )malloc(sizeof(int* ) * m);
for (int i = 0; i < m; i++)
{
dp[i] = (int* )malloc(sizeof(int) * n);
}
//初值
//先将第一行第一列设为0
for(int i = 0; i < m; i++)
{
dp[i][0] = 0;
}
for(int j = 0; j < n; j++)
{
dp[0][j] = 0;
}
//若碰到障碍,之后的都走不了。退出循环
for(int i = 0; i < m; i++)
{
if(obstacleGrid[i][0]) {break;}
dp[i][0] = 1;
}
for(int j = 0; j < n; j++)
{
if(obstacleGrid[0][j]) {break;}
dp[0][j] = 1;
}
//动态规划
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
{
if (obstacleGrid[i][j]) {dp[i][j] = 0;}
else dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
注意要点:
下面贴出代码:
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1);
dp[2] = 1;
for (int i = 3; i <= n; i++)
{
for (int j = 1; j <= i / 2; j++)
{
dp[i] = max(max(j * (i - j), j * dp[i - j]), dp[i]);
}
}
return dp[n];
}
};
int integerBreak(int n){
int* dp = (int* )malloc(sizeof(int) * (n + 1));
dp[2] = 1;
for (int i = 3; i <= n; i++)
{
for (int j = 1; j < i - 1; j++)
{
int now = fmax(j * (i - j), j * dp[i - j]);
dp[i] = fmax(dp[i], now);
}
}
return dp[n];
}
注意要点:
下面贴出代码:
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1);
dp[0] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
};
int numTrees(int n){
int* dp = (int* )malloc(sizeof(int) * (n + 1));
dp[0] = 1;
for (int i = 1; i <= n; i++) {dp[i] = 0;}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
这是动态规划的经典题型,一般而言,应对求职掌握01背包和完全背包就够用了,最多再加上多重背包;以下图是总结了所有的背包问题:
leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。
而完全背包问题,就是从01背包上进行扩展,即:完全背包的物品数量是无限的。
所以可以说,背包问题的理论基础重中之重就是01背包!
有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
当然这一类问题可以回溯法保利求解,毕竟只有取/不取两种情况,但是暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
举一个例子:背包最大重量为4;物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
求解背包能装下的物品最大价值。这就是一个典型的01背包问题。
动规的五部曲来进行分析。
对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
可以有两个方向来进行递推dp[i][j]:
所以可以得到递推公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
再看其他情况。状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i是由 i-1 推导出来,那么i为0的时候就一定要初始化。
dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
所以初始化情况应如下图所示:
再看递推公式,可以看出dp[i][j]就是通过左上方的元素进行推导的,所以其他下标可以随意初始化,为了简便可以初始化为0,那么最终的初始化如下:
初始化的代码如下:
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
从题目中就可以看出,有两个遍历方向,一个是背包,一个是物品。
先后的遍历情况是都可以的!!但是先遍历物品更好理解。
先遍历物品,然后遍历背包的代码如下:
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
之所以遍历顺序是都可以的,是因为递归的本质和递推的方向,从递推公式出发进行推导,可以看出**虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!**但是先遍历物品再遍历背包会更容易理解。
其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了。
根据计算推导就可以得到上图,最终的结果就是dp[2][4]。
所以说,这道例题最终可以通过如下代码解决:
void test_2_wei_bag_problem1() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagweight = 4;
// 二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[weight.size() - 1][bagweight] << endl;
}
int main() {
test_2_wei_bag_problem1();
}
讲了这么多才刚刚把二维dp的01背包讲完,这里大家其实可以发现最简单的是推导公式了,推导公式估计看一遍就记下来了,但难就难在如何初始化和遍历顺序上。
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
既然如此,动规五部曲重新来进行一维dp分析:
一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
dp[j - weight[i]] + value[i] 表示容量为 j - 物品i重量 的背包加上物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])
此时dp[j]有两个选择,一个是取自己dp[j] 相当于二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值;那么可以有如下的递推公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
再从递推公式出发:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 从中进行推理可以看出,dp推导过程中一定是取价值最大的,那么非0下标在价值均为正整数时直接都给0就可以了。这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
与二维dp不同的是,一维dp一定要**倒序遍历!倒序遍历是为了保证物品i只被放入一次!**如果一旦正序遍历了,那么物品0就会被重复加入多次!
遍历的代码如下:
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
二维dp不需要倒序遍历,是因为二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。所以不可以先遍历背包再遍历物品!
倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:
最终可写出如下代码:
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_1_wei_bag_problem();
}
注意要点:
下面贴出代码:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
vector<int> dp(200 * 100 / 2 + 1, 0);
for (int i = 0; i < nums.size(); i++) {sum += nums[i];}
if (sum % 2) {return 0;}
int target = sum / 2;
// 相当于背包容量为sum/2,背包的大小就是nums.size,物品的重量和价值都是nums[i]
for (int i = 0; i < nums.size(); i++)
{
for (int j = target; j >= nums[i]; j--)
{
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
if (dp[target] == target) {return 1;}
return 0;
}
};
bool canPartition(int* nums, int numsSize){
int sum = 0;
for (int i = 0; i < numsSize; i++)
{
sum += nums[i];
}
if (sum % 2) {return false;}
int target = sum / 2;
int* dp = (int* )malloc(sizeof(int) * 10001);
memset(dp, 0, sizeof(int) * 10001);
for (int i = 0; i < numsSize; i++)
{
for (int j = target; j >= nums[i]; j--)
{
dp[j] = fmax(dp[j], dp[j - nums[i]] + nums[i]);
}
}
if (dp[target] == target) {return true;}
return false;
}
注意要点:
下面贴出代码:
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for (int stone : stones) {sum += stone;}
int target = sum / 2;
vector<int> dp(target + 1, 0);
for (int i = 0; i < stones.size(); i++)
{
for (int j = target; j >= stones[i]; j--)
{
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - 2 * dp[target];
}
};
int lastStoneWeightII(int* stones, int stonesSize){
int sum = 0;
for (int i = 0; i < stonesSize; i++)
{
sum += stones[i];
}
int target = sum / 2;
int* dp = (int* )malloc(sizeof(int) * 15001);
memset(dp, 0, sizeof(int) * 15001);
for (int i = 0; i < stonesSize; i++)
{
for (int j = target; j >= stones[i]; j--)
{
dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - 2 * dp[target];
}
注意要点:
下面贴出代码:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int num : nums) {sum += num;}
if (abs(target) > sum) {return 0;}
if ((sum + target) % 2) {return 0;}
int bagWeight = (target + sum) / 2;
vector<int> dp(bagWeight + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++)
{
for (int j = bagWeight; j >= nums[i]; j--)
{
dp[j] += dp[j - nums[i]];
}
}
return dp[bagWeight];
}
};
int findTargetSumWays(int* nums, int numsSize, int target){
int sum = 0;
for (int i = 0; i < numsSize; i++)
{
sum += nums[i];
}
if (abs(target) > sum) return 0;
if ((sum + target) % 2) return 0;
int tar = (sum + target) / 2;
int* dp = (int* )malloc(sizeof(int) * (tar + 1));
//这里初始化dp=1
memset(dp, 0, sizeof(int) * (tar + 1));
dp[0] = 1;
for (int i = 0; i < numsSize; i++)
{
for (int j = tar; j >= nums[i]; j--)
{
dp[j] += dp[j-nums[i]];
}
}
return dp[tar];
}
注意要点:
下面贴出代码:
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0));
for (string str : strs)
{
int zeroNum = 0, oneNum = 0;
for (char c : str)
{
if (c == '0') {zeroNum++;}
else {oneNum++;}
}
for (int i = m; i >= zeroNum; i--)
{
for (int j = n; j >= oneNum; j--)
{
dp[i][j] = max(dp[i][j], dp[i-zeroNum][j-oneNum] + 1);
}
}
}
return dp[m][n];
}
};
int findMaxForm(char ** strs, int strsSize, int m, int n){
//相当于1和0两个背包
int** dp = (int** )malloc(sizeof(int* ) * (m + 1));
for (int i = 0; i <= m; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (n + 1));
}
for (int i = 0; i <= m; i++)
{
for (int j = 0; j <= n; j++)
{
dp[i][j] = 0;
}
}
for (int i = 0; i < strsSize; i++)
{
int zero_num = 0, one_num = 0;
char* str = strs[i];
for (int j = 0; j < strlen(str); j++)
{
if (str[j] == '0') zero_num++;
else one_num++;
}
for (int i = m; i >= zero_num; i--)
{
for (int j = n; j >= one_num; j--)
{
dp[i][j] = fmax(dp[i][j], dp[i - zero_num][j - one_num] + 1);
}
}
}
return dp[m][n];
}
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
以下的学习依然举出一个例子:
背包最大重量为4;物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
每件商品有无限个!
01背包和完全背包唯一不同就是体现在遍历顺序上,所以这里就跳过动规五部曲的分析了,与之前的分析大同小异!
我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次;而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
01背包中二维dp数组的两个for遍历的先后循序是可以颠倒,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
因为dp[j]是根据下标j之前所对应的dp[j]计算出来的,只要保证下标j之前的dp[j]都是经过计算的就可以了。
当然了,对于我们这种应试者,我就记住一种遍历的模板就好了嘛,所以我就全部是先遍历物品,在遍历背包!
完整的完全背包实现的代码如下:
// 先遍历物品,在遍历背包
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_CompletePack();
}
注意要点:
下面贴出代码:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for (int i = 0; i < coins.size(); i++)
{
for (int j = coins[i]; j <= amount; j++)
{
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
int change(int amount, int* coins, int coinsSize){
int* dp = (int* )malloc(sizeof(int) * (amount + 1));
memset(dp, 0, sizeof(int) * (amount + 1));
dp[0] = 1;
for (int i = 0; i < coinsSize; i++)
{
for (int j = coins[i]; j <= amount; j++)
{
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
注意要点:
下面贴出代码:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
for (int j = 0; j <= target; j++)
{
for (int i = 0; i < nums.size(); i++)
{
if (j - nums[i] >= 0 && dp[j] <= INT_MAX - dp[j - nums[i]])
dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
};
int combinationSum4(int* nums, int numsSize, int target){
int* dp = (int* )malloc(sizeof(int) * (target + 1));
memset(dp, 0, sizeof(int) * (target + 1));
dp[0] = 1;
for (int j = 0; j <= target; j++)
{
for (int i = 0; i < numsSize; i++)
{
if (j >= nums[i] && dp[j] < INT_MAX - dp[j - nums[i]])
{
dp[j] += dp[j - nums[i]];
}
}
}
return dp[target];
}
注意要点:
这里比较简单,我就只把C++代码贴出来,C就是vector换成int的一维数组指针就可以了:
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n + 1, 0);
int m = 2;
dp[0] = 1;
for (int j = 1; j <= n; j++)
{
for (int i = 1; i <= m; i++)
{
if (j - i >= 0) {dp[j] += dp[j - i];}
}
}
return dp[n];
}
};
注意要点:
下面贴出代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++)
{
for (int j = coins[i]; j <= amount; j++)
{
if (dp[j - coins[i]] != INT_MAX)
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
}
}
if (dp[amount] == INT_MAX) {return -1;}
return dp[amount];
}
};
int coinChange(int* coins, int coinsSize, int amount){
int* dp = (int* )malloc(sizeof(int) * (amount + 1));
for (int i = 0; i <= amount; i++) {dp[i] = INT_MAX;}
dp[0] = 0;
for (int i = 0; i < coinsSize; i++)
{
for (int j = coins[i]; j <= amount; j++)
{
if (dp[j - coins[i]] != INT_MAX)
{
dp[j] = fmin(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == INT_MAX ? -1: dp[amount];
}
注意要点:
下面贴出代码:
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i * i <= n; i++)
{
for (int j = i * i; j <= n; j++)
{
dp[j] = min(dp[j], dp[j - i * i] + 1);
}
}
return dp[n];
}
};
int numSquares(int n){
int* dp = (int* )malloc(sizeof(int) * (n + 1));
for (int i = 0; i <= n; i++) {dp[i] = INT_MAX;}
dp[0] = 0;
for (int i = 1; i * i <= n; i++)
{
for (int j = i * i; j <= n; j++)
{
if (dp[j - i * i] < INT_MAX)
{
dp[j] = fmin(dp[j], dp[j - i * i] + 1);
}
}
}
return dp[n];
}
注意要点:
这题还涉及对字符串的匹配,所以比较难,C++可以用unordered_set完成哈希匹配,C的话就要用strcmp来进行比较。
下面贴出代码:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.size() + 1, 0);
dp[0] = 1;
unordered_set<string> hash(wordDict.begin(), wordDict.end());
for (int j = 1; j <= s.size(); j++)
{
for (int i = 0; i < j; i++)
{
string word = s.substr(i, j - i);
if (hash.find(word) != hash.end() && dp[i]) {dp[j] = 1;}
}
}
return dp[s.size()];
}
};
bool find(char* s, char** wordDict, int wordDictSize)
{
for (int i = 0; i < wordDictSize; i++)
{
if (!strcmp(s, wordDict[i])) {return 1;}
}
return 0;
}
bool wordBreak(char * s, char ** wordDict, int wordDictSize){
bool* dp = (bool* )malloc(sizeof(bool) * (strlen(s) + 1));
memset(dp, 0, sizeof(bool) * (strlen(s) + 1));
dp[0] = 1;
for (int j = 1; j <= strlen(s); j++)
{
for (int i = 0; i < j; i++)
{
char* str = (char* )malloc(sizeof(char) * (j - i + 1));
for (int k = 0; k < j - i; k++) {str[k] = s[i + k];}
str[j - i] = '\0';
if (find(str, wordDict, wordDictSize) && dp[i])
{
dp[j] = 1;
}
}
}
return dp[strlen(s)];
}
有N种物品和一个容量为V的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
多重背包和01背包是非常像的, 为什么和01背包像呢?
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
例如:背包最大重量为10,物品为:
重量 | 价值 | 数量 | |
---|---|---|---|
物品0 | 1 | 15 | 2 |
物品1 | 3 | 20 | 3 |
物品2 | 4 | 30 | 2 |
如果把数量展开,如下所示,就相当于一个01背包:
重量 | 价值 | 数量 | |
---|---|---|---|
物品0 | 1 | 15 | 1 |
物品0 | 1 | 15 | 1 |
物品1 | 3 | 20 | 1 |
物品1 | 3 | 20 | 1 |
物品1 | 3 | 20 | 1 |
物品2 | 4 | 30 | 1 |
物品2 | 4 | 30 | 1 |
那么就可以像01背包一样来实现求解,代码如下:
void test_multi_pack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
vector<int> nums = {2, 3, 2};
int bagWeight = 10;
for (int i = 0; i < nums.size(); i++) {
while (nums[i] > 1) { // nums[i]保留到1,把其他物品都展开
weight.push_back(weight[i]);
value.push_back(value[i]);
nums[i]--;
}
}
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
for (int j = 0; j <= bagWeight; j++) {
cout << dp[j] << " ";
}
cout << endl;
}
cout << dp[bagWeight] << endl;
}
int main() {
test_multi_pack();
}
这里只要知道,多重背包可以展开然后变成01背包,类比的写出代码就可以了,leetcode暂时也没有相关的题目。
注意要点:
下面贴出代码:
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 1) {return nums[0];}
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); i++)
{
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[nums.size() - 1];
}
};
int rob(int* nums, int numsSize){
if (numsSize == 1) {return nums[0];}
else if (numsSize == 2) {return fmax(nums[0], nums[1]);}
int* dp = (int* )malloc(sizeof(int) * numsSize);
dp[0] = nums[0];
dp[1] = fmax(nums[0], nums[1]);
for (int i = 2; i < numsSize; i++)
{
dp[i] = fmax(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[numsSize - 1];
}
注意要点:
下面贴出代码:
class Solution {
private:
int robRange(const vector<int>& nums, int start, int end)
{
if (end - start == 1) {return nums[start];}
vector<int> dp(end - start, 0);
dp[0] = nums[start];
dp[1] = max(nums[start], nums[start + 1]);
for (int i = 2; i < end - start; i++)
{
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - start]);
}
return dp[end - start - 1];
}
public:
int rob(vector<int>& nums) {
if (nums.size() == 1) {return nums[0];}
int ret1 = robRange(nums, 0, nums.size() - 1);
int ret2 = robRange(nums, 1, nums.size());
return max(ret1, ret2);
}
};
int robRange(int* nums, int numsSize, int start, int end)
{
int* dp = (int* )malloc(sizeof(int) * numsSize);
dp[start] = nums[start];
dp[start + 1] = fmax(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++)
{
dp[i] = fmax(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[end];
}
int rob(int* nums, int numsSize){
if (numsSize == 1) {return nums[0];}
else if (numsSize == 2) {return fmax(nums[0], nums[1]);}
int ret1 = robRange(nums, numsSize, 0, numsSize - 2);
int ret2 = robRange(nums, numsSize, 1, numsSize - 1);
return fmax(ret1, ret2);
}
注意要点:
这道题是树形dp的比较典型的题目,可以好好钻研一下,下面就是推导的情况,老样子我白嫖了代码随想录的图:
下面贴出代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
vector<int> traversal(TreeNode* cur)
{
//下标0就是不偷,下标1就是偷
if (!cur) {return vector<int> {0, 0};}
vector<int> left = traversal(cur->left);
vector<int> right = traversal(cur->right);
//不偷
int val1 = max(left[0], left[1]) + max(right[0], right[1]);
//偷
int val2 = cur->val + left[0] + right[0];
return vector<int> {val1, val2};
}
public:
int rob(TreeNode* root) {
vector<int> ret = traversal(root);
return max(ret[0], ret[1]);
}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
int* robTree(struct TreeNode* root)
{
int* ans = (int* )malloc(sizeof(int) * 2);
memset(ans, 0, sizeof(int) * 2);
if (!root) {return ans;}
int* left = robTree(root->left);
int* right = robTree(root->right);
//当前节点要偷
ans[1] = root->val + left[0] + right[0];
//当前节点不偷
ans[0] = fmax(left[0], left[1]) + fmax(right[0], right[1]);
return ans;
}
int rob(struct TreeNode* root){
int* ret = (int* )malloc(sizeof(int) * 2);
ret = robTree(root);
return fmax(ret[0], ret[1]);
}
注意要点:
关于递推公式,为了详尽一点记录,我放在这里给出:
dp[i][0]可以如此推导:
dp[i][1]可以如此推导:
下面给出推理的过程图,白嫖了代码随想录,可以跟着图推理一下,比较方便写代码进行理解:
下面贴出代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(2, 0));
dp[0][0] -= prices[0];
for (int i = 1; i < len; i++)
{
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
};
int maxProfit(int* prices, int pricesSize){
int** dp = (int** )malloc(sizeof(int* ) * pricesSize);
for (int i = 0; i < pricesSize; i++)
{
dp[i] = (int* )malloc(sizeof(int) * 2);
}
//列为0代表持有股票,列为1代表不持有
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < pricesSize; i++)
{
dp[i][0] = fmax(dp[i - 1][0], -prices[i]);
dp[i][1] = fmax(dp[i - 1][0] + prices[i], dp[i - 1][1]);
}
return dp[pricesSize - 1][1];
}
注意要点:
下面贴出代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(2, 0));
dp[0][0] -= prices[0];
for (int i = 1; i < len; i++)
{
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
};
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ans = 0;
for (int i = 0; i < prices.size() - 1; i++)
{
int now = prices[i + 1] - prices[i];
ans += now > 0 ? now : 0;
}
return ans;
}
};
注意要点:
递推公式的推导如下所示:
达到dp[i][1]状态,有两个具体操作:
而为了利益最大化,一定是取两者的最大值,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
同理dp[i][2]也有两个操作:
所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
同理可推出剩下状态部分:
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
为了更好的便于理解,我把推导图也偷过来了,如下所示:以输入[1,2,3,4,5]为例
函数的返回值就是最右下角的值,原因就是,假设第一次卖出是最大值,那么完全可以最后一天再次进行买进卖出,这样最大值就会记录在第二次卖出之中。
下面贴出代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(5, 0));
dp[0][1] -= prices[0];
dp[0][3] -= prices[0];
//分成五种状态,0就是无操作,1就是第一次卖出,2是第一次买进
//3是第二次卖出,4是第二次买进
for (int i = 1; i < len; i++)
{
dp[i][0] = dp[i - 1][0];
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[len - 1][4];
}
};
int maxProfit(int* prices, int pricesSize){
int** dp = (int** )malloc(sizeof(int* ) * pricesSize);
for (int i = 0; i < pricesSize; i++)
{
dp[i] = (int* )malloc(sizeof(int) * 5);
}
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
for (int i = 1; i < pricesSize; i++)
{
dp[i][0] = dp[i - 1][0];
dp[i][1] = fmax(dp[i - 1][0] - prices[i], dp[i - 1][1]);
dp[i][2] = fmax(dp[i - 1][1] + prices[i], dp[i - 1][2]);
dp[i][3] = fmax(dp[i - 1][2] - prices[i], dp[i - 1][3]);
dp[i][4] = fmax(dp[i - 1][3] + prices[i], dp[i - 1][4]);
}
return dp[pricesSize - 1][4];
}
注意要点:
下面贴出代码:
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(2 * k + 1, 0));
for (int i = 1; i < 2 * k; i += 2) {dp[0][i] -= prices[0];}
for (int i = 1; i < len; i++)
{
dp[i][0] = dp[i - 1][0];
for (int j = 1; j < 2 * k + 1; j += 2)
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] + prices[i]);
}
}
return dp[len - 1][2 * k];
}
};
int maxProfit(int k, int* prices, int pricesSize){
int** dp = (int** )malloc(sizeof(int* ) * pricesSize);
for (int i = 0; i < pricesSize; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (2 * k + 1));
}
dp[0][0] = 0;
for (int i = 1; i < 2 * k + 1; i++)
{
if (i % 2) {dp[0][i] = -prices[0];}
else {dp[0][i] = 0;}
}
for (int i = 1; i < pricesSize; i++)
{
dp[i][0] = dp[i - 1][0];
for (int j = 1; j < 2 * k + 1; j++)
{
if (j % 2) {dp[i][j] = fmax(dp[i - 1][j - 1] - prices[i], dp[i - 1][j]);}
else {dp[i][j] = fmax(dp[i - 1][j - 1] + prices[i], dp[i - 1][j]);}
}
}
return dp[pricesSize - 1][2 * k];
}
注意要点:
这道题跟执勤的相比,多了一个冷冻期,所以复杂了许多,我按照动规五部曲来记录解题笔记:
这里光看文字还是有些晦涩,我把推导图白嫖来了,以[1,2,3,0,2]为例:
下面贴出代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(4, 0));
dp[0][0] -= prices[0];
for (int i = 1; i < len; i++)
{
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][1], dp[i - 1][3]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
}
return max(dp[len - 1][1], max(dp[len - 1][2], dp[len - 1][3]));
}
};
int maxProfit(int* prices, int pricesSize){
int** dp = (int** )malloc(sizeof(int* ) * pricesSize);
for (int i = 0; i < pricesSize; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (4));
}
//0代表保持持有,1代表保持不持有,2代表卖出,3代表冻结
dp[0][0] = -prices[0];
dp[0][1] = dp[0][2] = dp[0][3] = 0;
for (int i = 1; i < pricesSize; i++)
{
dp[i][0] = fmax(dp[i - 1][0], fmax(dp[i - 1][1], dp[i - 1][3]) - prices[i]);
dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
}
return fmax(dp[pricesSize-1][1], fmax(dp[pricesSize-1][2], dp[pricesSize-1][3]));
}
注意要点:
下面贴出代码:
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(2, 0));
dp[0][0] -= prices[0];
for (int i = 1; i < len; i++)
{
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
}
return max(dp[len - 1][0], dp[len - 1][1]);
}
};
int maxProfit(int* prices, int pricesSize, int fee){
int** dp = (int** )malloc(sizeof(int* ) * pricesSize);
for (int i = 0; i < pricesSize; i++)
{
dp[i] = (int* )malloc(sizeof(int) * 2);
}
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < pricesSize; i++)
{
dp[i][0] = fmax(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = fmax(dp[i - 1][0] + prices[i] - fee, dp[i - 1][1]);
}
return fmax(dp[pricesSize - 1][0], dp[pricesSize - 1][1]);
}
这道题是我二刷现在做笔记的时候没啥思路的题目,要好好看一下!
注意要点:
下面贴出代码:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int len = nums.size();
if (len <= 1) {return len;}
vector<int> dp(len, 1);
int result = 0;
for (int i = 1; i < len; i++)
{
for (int j = 0; j < i; j++)
{
if (nums[i] > nums[j]) {dp[i] = max(dp[i], dp[j] + 1);}
}
result = max(result, dp[i]);
}
return result;
}
};
int lengthOfLIS(int* nums, int numsSize){
if (numsSize == 1) {return 1;}
int* dp = (int* )malloc(sizeof(int) * numsSize);
for (int i = 0; i < numsSize; i++) {dp[i] = 1;}
int ret = 0;
for (int i = 1; i < numsSize; i++)
{
for (int j = 0; j < i; j++)
{
if (nums[i] > nums[j])
{
dp[i] = fmax(dp[i], dp[j] + 1);
}
}
ret = fmax(ret, dp[i]);
}
return ret;
}
注意要点:
下面贴出代码:
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int len = nums.size();
if (len <= 1) {return len;}
vector<int> dp(len, 1);
int result = 0;
for (int i = 1; i < len; i++)
{
if (nums[i] > nums[i - 1]) {dp[i] = dp[i - 1] + 1;}
result = max(result, dp[i]);
}
return result;
}
};
int findLengthOfLCIS(int* nums, int numsSize){
if (numsSize == 1) {return 1;}
int* dp = (int* )malloc(sizeof(int) * numsSize);
for (int i = 0; i < numsSize; i++) {dp[i] = 1;}
int ret = 0;
for (int i = 1; i < numsSize; i++)
{
if (nums[i] > nums[i - 1])
{
dp[i] = fmax(dp[i], dp[i - 1] + 1);
}
ret = fmax(dp[i], ret);
}
return ret;
}
注意要点:
下面贴出代码:
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size(), len2 = nums2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
int result = 0;
for (int i = 1; i <= len1; i++)
{
for (int j = 1; j <= len2; j++)
{
if (nums1[i - 1] == nums2[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + 1;}
result = max(result, dp[i][j]);
}
}
return result;
}
};
int findLength(int* nums1, int nums1Size, int* nums2, int nums2Size){
int m = nums1Size, n = nums2Size;
int** dp = (int** )malloc(sizeof(int* ) * (m + 1));
for (int i = 0; i < m + 1; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (n + 1));
}
for (int i = 0; i < m + 1; i++)
{
for (int j = 0; j < n + 1; j++) {dp[i][j] = 0;}
}
int ret = 0;
for (int i = 1; i < m + 1; i++)
{
for (int j = 1; j < n + 1; j++)
{
if (nums1[i - 1] == nums2[j - 1])
{
dp[i][j] = dp[i - 1][j - 1] + 1;
}
ret = fmax(ret, dp[i][j]);
}
}
return ret;
}
注意要点:
这一题不是那么好想,总的来说就是**dp[i][j]的递推是通过其左上角的三个元素来决定的!**以输入:text1 = “abcde”, text2 = “ace” 为例,dp状态如图:
下面贴出代码:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int len1 = text1.size(), len2 = text2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for (int i = 1; i <= len1; i++)
{
for (int j = 1; j <= len2; 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[len1][len2];
}
};
int longestCommonSubsequence(char * text1, char * text2){
int m = strlen(text1), n = strlen(text2);
int** dp = (int** )malloc(sizeof(int* ) * (m + 1));
for (int i = 0; i < m + 1; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (n + 1));
}
//全部初始化为0
for (int i = 0; i < m + 1; i++)
{
for (int j = 0; j < n + 1; j++)
{
dp[i][j] = 0;
}
}
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] = fmax(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
注意要点:
下面贴出代码:
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size(), len2 = nums2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for (int i = 1; i <= len1; i++)
{
for (int j = 1; j <= len2; j++)
{
if (nums1[i - 1] == nums2[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[len1][len2];
}
};
int maxUncrossedLines(int* nums1, int nums1Size, int* nums2, int nums2Size){
int m = nums1Size, n = nums2Size;
int** dp = (int** )malloc(sizeof(int* ) * (m + 1));
for (int i = 0; i <= m; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (n + 1));
}
for (int i = 0; i <= m; i++)
{
for (int j = 0; j <= n; j++)
{
dp[i][j] = 0;
}
}
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (nums1[i - 1] == nums2[j - 1])
{
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else
{
dp[i][j] = fmax(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
注意要点:
下面贴出代码:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int len = nums.size();
vector<int> dp(len, 0);
dp[0] = nums[0];
int result = dp[0];
for (int i = 1; i < len; i++)
{
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
result = max(result, dp[i]);
}
return result;
}
};
int maxSubArray(int* nums, int numsSize){
int* dp = (int* )malloc(sizeof(int) * numsSize);
dp[0] = nums[0];
int ret = nums[0];
for (int i = 1; i < numsSize; i++)
{
dp[i] = fmax(dp[i - 1] + nums[i], nums[i]);
ret = fmax(dp[i], ret);
}
return ret;
}
这道题可以直接双指针做,所以是一道简单题,思路我也不写了,直接贴一版代码,一看就懂了:
class Solution {
public:
bool isSubsequence(string s, string t) {
int ptr_s = 0, ptr_t = 0;
while (ptr_t < t.size())
{
if (s[ptr_s] == t[ptr_t]) {ptr_s++;}
if (ptr_s == s.size()) {break;}
ptr_t++;
}
if (ptr_s == s.size()) {return 1;}
return 0;
}
};
这是编辑距离的动规基础,这里按照动规五部曲来进行分析:
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
需要注意的是,判断s是否为t的子序列。即t的长度是大于等于s的。
在确定递推公式的时候,首先要考虑如下两种操作,整理如下:
if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1;
if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j]的数值就是看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];
从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],所以**dp[0][0]和dp[i][0]**是一定要初始化的,直接都给0就可以了。
这里就可以知道,定义dp[i][j]使用的是i-1的s和j-1的t就是为了方便我们的初始化!
从递推公式可以看出,左上角和左侧元素来进行当前状态的推导,所以从上到下,从左到右遍历。
输入:s = “abc”, t = “ahbgdc”,dp状态转移图如下:
下面贴出代码:
class Solution {
public:
bool isSubsequence(string s, string t) {
int lens = s.size(), lent = t.size();
vector<vector<int>> dp(lens + 1, vector<int>(lent + 1, 0));
for (int i = 1; i <= lens; i++)
{
for (int j = 1; j <= lent; j++)
{
if (s[i - 1] == t[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + 1;}
else dp[i][j] = dp[i][j - 1];
}
}
return dp[lens][lent] == lens;
}
};
bool isSubsequence(char * s, char * t){
int m = strlen(s), n = strlen(t);
int** dp = (int** )malloc(sizeof(int* ) * (m + 1));
for (int i = 0; i <= m; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (n + 1));
}
for (int i = 0; i <= m; i++)
{
for (int j = 0; j <= n; j++)
{
dp[i][j] = 0;
}
}
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (s[i - 1] == t[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + 1;}
else {dp[i][j] = dp[i][j - 1];}
}
}
if (dp[m][n] == m) {return true;}
return false;
}
这一题的大致思路与上一题类似:
以s:“baegg”,t:"bag"为例,推导dp数组状态如下:
下面贴出代码:
class Solution {
public:
int numDistinct(string s, string t) {
int lens = s.size(), lent = t.size();
vector<vector<uint64_t>> dp(lens + 1, vector<uint64_t>(lent + 1, 0));
for (int i = 0; i < lens; i++) {dp[i][0] = 1;}
for (int i = 1; i <= lens; i++)
{
for (int j = 1; j <= lent; j++)
{
if (s[i - 1] == t[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];}
else dp[i][j] = dp[i - 1][j];
}
}
return dp[lens][lent];
}
};
int numDistinct(char * s, char * t){
int m = strlen(s), n = strlen(t);
double** dp = (int** )malloc(sizeof(double* ) * (m + 1));
for (int i = 0; i <= m; i++)
{
dp[i] = (double* )malloc(sizeof(double) * (n + 1));
}
//初始化,根据dp定义,dp[i][0] = 1,除了dp[0][0]外dp[0][j] = 0;
for (int i = 0; i <= m; i++) {dp[i][0] = 1;}
for (int i = 1; i <= n; i++) {dp[0][i] = 0;}
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (s[i - 1] == t[j - 1])
{
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}
else
{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[m][n];
}
注意要点:
相等的时候很好理解,就不需要删除,所以dp[i][j]=dp[i-1][j-1];
不相等的时候,就有三种删除情况:
情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1;
情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1;
情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2;
因为 dp[i][j - 1] + 1 = dp[i - 1][j - 1] + 2,所以递推公式可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);(相等是因为同时删word1[i - 1]和word2[j - 1],dp[i][j-1] 本来就不考虑 word2[j - 1]了,那么我在删 word1[i - 1],就达到两个元素都删除的效果)
最后可以举例推导一下,以word1:“sea”,word2:"eat"为例,推导dp数组状态图如下:
下面贴出代码:
class Solution {
public:
int minDistance(string word1, string word2) {
int len1 = word1.size(), len2 = word2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for (int i = 0; i <= len1; i++) {dp[i][0] = i;}
for (int j = 0; j <= len2; j++) {dp[0][j] = j;}
for (int i = 1; i <= len1; i++)
{
for (int j = 1; j <= len2; 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, dp[i][j - 1] + 1);
}
}
return dp[len1][len2];
}
};
int minDistance(char * word1, char * word2){
int m = strlen(word1), n = strlen(word2);
int** dp = (int** )malloc(sizeof(int* ) * (m + 1));
for (int i = 0; i < m + 1; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (n + 1));
}
for (int i = 0; i <= m; i++) {dp[i][0] = i;}
for (int j = 0; j <= n; j++) {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] = dp[i - 1][j - 1];}
else dp[i][j] = fmin(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
}
return dp[m][n];
}
当然我感觉这种解题不是特别好理解,我把题目理解成求最长公共子序列不就好了嘛,对于要删除的元素不就是总长度-2*最长公共子序列就完事了。
下面贴出代码:
class Solution {
public:
int minDistance(string word1, string word2) {
int len1 = word1.size(), len2 = word2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for (int i = 1; i <= len1; i++)
{
for (int j = 1; j <= len2; j++)
{
if (word1[i - 1] == word2[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 len1 + len2 - 2 * dp[len1][len2];
}
};
int minDistance(char * word1, char * word2){
int m = strlen(word1), n = strlen(word2);
int** dp = (int** )malloc(sizeof(int* ) * (m + 1));
for (int i = 0; i < m + 1; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (n + 1));
}
//全部初始化为0
for (int i = 0; i < m + 1; i++)
{
for (int j = 0; j < n + 1; j++)
{
dp[i][j] = 0;
}
}
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] + 1;
}
else
{
dp[i][j] = fmax(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return m + n - 2 * dp[m][n];
}
本题是上一题的再进阶版,因为增、删、改都可以,所以递推公式还要改变,其余的操作都是类似的,就不再赘述了。
如果当前字符相等,这个简单,就是dp[i][j]=dp[i-1][j-1];
如果不相等:
操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。
即 dp[i][j] = dp[i - 1][j] + 1;
操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。
即 dp[i][j] = dp[i][j - 1] + 1;
word2添加一个元素,相当于word1删除一个元素,同理反过来也是一样,所以增删是可以写在一起的;
所以 dp[i][j] = dp[i - 1][j - 1] + 1;
基于以上分析,可以给出输入:word1 = “horse”, word2 = "ros"为例,dp矩阵状态图如下:
下面贴出代码:
class Solution {
public:
int minDistance(string word1, string word2) {
int len1 = word1.size(), len2 = word2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for (int i = 0; i <= len1; i++) dp[i][0] = i;
for (int j = 0; j <= len2; j++) dp[0][j] = j;
for (int i = 1; i <= len1; i++)
{
for (int j = 1; j <= len2; 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], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
}
}
return dp[len1][len2];
}
};
int minDistance(char * word1, char * word2){
int m = strlen(word1), n = strlen(word2);
int** dp = (int** )malloc(sizeof(int* ) * (m + 1));
for (int i = 0; i < m + 1; i++)
{
dp[i] = (int* )malloc(sizeof(int) * (n + 1));
}
for (int i = 0; i <= m; i++) {dp[i][0] = i;}
for (int j = 0; j <= n; j++) {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] = dp[i - 1][j - 1];}
else
{
dp[i][j] = fmin(dp[i-1][j], fmin(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
}
}
}
return dp[m][n];
}
这类题目如果用动态规划,dp的定义就不是那么直接了,所以有空还是要多看看,温故知新。
因为二刷的时候,只记得遍历顺序其他都记不清了,所以还是动规五部曲走起,加深印象。
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false;
当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况:
这个简单,肯定是全部初始化为false
本题的遍历顺序是很有技巧的:首先从递推公式可以看到,我们的元素是否true都是根据左下角来判断的,如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。
所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
最后可以举例推导一下,输入:“aaa”,dp[i][j]状态如下:
下面贴出代码:
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), 0));
int ret = 0;
for (int i = s.size() - 1; i >= 0; i--)
{
for (int j = i; j < s.size(); j++)
{
if (s[i] == s[j])
{
if (j - i <= 1) {ret++;dp[i][j] = 1;}
else if (dp[i + 1][j - 1]) {ret++;dp[i][j] = 1;}
}
}
}
return ret;
}
};
int countSubstrings(char * s){
int n = strlen(s);
bool** dp = (int** )malloc(sizeof(bool* ) * n);
for (int i = 0; i < n; i++) {dp[i] = (bool* )malloc(sizeof(bool) * n);}
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++) {dp[i][j] = 0;}
}
int ret = 0;
for (int i = n; i >= 0; i--)
{
for (int j = i; j < n; j++)
{
if (s[i] == s[j])
{
if (j - i <= 1 || dp[i + 1][j - 1])
{
ret++;
dp[i][j] = 1;
}
}
}
}
return ret;
}
由于不熟悉,还是看了看解答才有的思路,所以依然动规五部曲。
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
这道题的递推公式,只要dp定义好还是比较好推的:
如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;
如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
加入s[j]的回文子序列长度为dp[i + 1][j]。
加入s[i]的回文子序列长度为dp[i][j - 1]。
首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出递推公式是计算不到 i 和j相同时候的情况;所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1;其他情况dp[i][j]初始为0就行。
从递推公式可以看出,当前元素依赖于左侧、下方以及左下角元素,所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的;j就从左往右遍历就可以了。
最后可以举例推导一下dp,输入s:“cbbd” 为例,dp数组状态如图:
下面贴出代码:
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
for (int i = s.size() - 1; i >= 0; i--)
{
for (int j = i + 1; j < s.size(); j++)
{
if (s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
else dp[i][j] = max(dp[i][j - 1], dp[i + 1][j]);
}
}
return dp[0][s.size() - 1];
}
};
int longestPalindromeSubseq(char * s){
int n = strlen(s);
int** dp = (int** )malloc(sizeof(int* ) * n);
for (int i = 0; i < n; i++) {dp[i] = (int* )malloc(sizeof(int) * n);}
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++) {dp[i][j] = 0;}
}
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] = fmax(dp[i][j - 1], dp[i + 1][j]);}
}
}
return dp[0][n - 1];
}
具体的递推公式,可以对着笔记来进行查阅,基本都不是特别难推导,主要是dp的定义,即状态的分类要搞清楚,比如含冷冻期就要分成四种(两种不持有状态)。
下面是问题的总结,可以对应去leetcode看看原题和曾经自己刷过的解析:
需要判断的只有前两家是否偷过,所以递推公式为: dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
成环就去掉头算一次,去掉尾算一次;树形就后序遍历从下往上算,用两个状态记录偷和不偷的最大值。
该类问题代码随想录讲了已下的一些leetcode题目:
基本都会涉及i和j的元素是否相等的判断,然后有不同的推理方法;
重复子序列中dp[i][j]都是代表的i-1处和j-1处的元素,这一点要记一下;回文中是左下向右上遍历,其他问题基本都是左上往右下遍历。
这里贴出来代码随想录的一个总结图,好好研究一下:
二刷的时候其实动规都忘的差不多了,这几个大类要时常看一看,温故知新。
474题中,涉及vector的二维数组初始化定义问题,需要记住格式:
vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0));
139题中,可以用unordered_set直接进行字符串匹配:
unordered_set<string> hash(wordDict.begin(), wordDict.end());
string word = s.substr(i, j - i);
if (hash.find(word) != hash.end() && dp[i]) {dp[j] = 1;}
如果是C版本的话,就是用strcmp进行比较:
bool find(char* s, char** wordDict, int wordDictSize)
{
for (int i = 0; i < wordDictSize; i++)
{
if (!strcmp(s, wordDict[i])) {return 1;}
}
return 0;
}