【动规】背包问题
01背包问题:
一. 01背包(牛客网)


01背包问题的本质就是一个商品选不选,同时注意背包的容量问题,那么状态表示一般用二维,一维来表示选不选一维来表示容量,要注意背包体积是否有剩余,完全背包问题要注意体积是否刚好合适,初始化也是采用多增加一行一列来避免越界问题
#include <iostream>
#include <cstring>
using namespace std;const int N=1010;
int n,V,v[N],w[N];
int dp[N][N];
int main() {//获取数据cin>>n>>V;for(int i=1;i<=n;i++)cin>>v[i]>>w[i];//解决第一问for(int i=1;i<=n;i++)for(int j=0;j<=V;j++){dp[i][j]=dp[i-1][j];if(j-v[i]>=0)dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);}//第一问答案输出cout<<dp[n][V]<<endl;//解决第二问memset(dp,0,sizeof(dp));//初始化第一行为-1的情况for(int i=1;i<=V;i++) dp[0][i]=-1;for(int i=1;i<=n;i++)for(int j=0;j<=V;j++){dp[i][j]=dp[i-1][j];if(j-v[i]>=0&&dp[i-1][j-v[i]]!=-1)dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);}cout<<(dp[n][V]==-1?0:dp[n][V])<<endl;return 0;
}
- 优化版本:
已知填表需要用到上一行的信息,用完之后就没有了,所以可以极限一点,只用一行一维数组来存储,但是遍历顺序要改为从后往前,因为一维数组中,dp[j] 既存储 i-1 层的旧状态,又要存储 i 层的新状态。正序遍历 j 时,较小的 j 会先被更新为 i 层状态(选了当前物品)。当计算较大的 j 时,j - v[i] 可能指向已经更新过的 i 层状态(而非原始的 i-1 层),导致同一物品被多次计入(违背 01 背包 “每个物品只能选一次” 的规则),而逆序遍历从大的位置开始覆盖,能保证只使用上一层的旧状态 - 方法:
所有的背包问题都可以进行空间上的优化
1.直接在原始代码上修改,删除第一维数组即可
2.改变j的遍历顺序
#include <iostream>
#include <cstring>
using namespace std;const int N=1010;
int n,V,v[N],w[N];
int dp[N][N];
int main() {//获取数据cin>>n>>V;for(int i=1;i<=n;i++)cin>>v[i]>>w[i];//解决第一问for(int i=1;i<=n;i++)for(int j=0;j<=V;j++){dp[i][j]=dp[i-1][j];if(j-v[i]>=0)dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);}//第一问答案输出cout<<dp[n][V]<<endl;//解决第二问memset(dp,0,sizeof(dp));//初始化第一行为-1的情况for(int i=1;i<=V;i++) dp[0][i]=-1;for(int i=1;i<=n;i++)for(int j=0;j<=V;j++){dp[i][j]=dp[i-1][j];if(j-v[i]>=0&&dp[i-1][j-v[i]]!=-1)dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);}cout<<(dp[n][V]==-1?0:dp[n][V])<<endl;return 0;
}
二. 分割等和子集


可以将该问题转化为背包问题,选每个数就相当于在选商品,等和子集就相当于背包容量,注意容量要是偶数才能划分否则直接返回false,注意初始化第一列可以的情况
class Solution {
public:bool canPartition(vector<int>& nums) {int n=nums.size();int sum=0;for(auto e:nums) sum+=e;if(sum%2==1) return false;int s=sum/2;vector<vector<bool>>dp(n+1,vector<bool>(s+1));//初始化for(int i=1;i<=n;i++) dp[i][0]=1;//填表for(int i=1;i<=n;i++)for(int j=1;j<=s;j++){dp[i][j]=dp[i-1][j];if(j>=nums[i-1]) dp[i][j]=dp[i][j]||dp[i-1][j-nums[i-1]];}return dp[n][s];}
};
空间优化:
1.删掉第⼀维;
2.修改第⼆层循环的遍历顺序即可,从后往前
class Solution {
public:bool canPartition(vector<int>& nums) {int n=nums.size();int sum=0;for(auto e:nums) sum+=e;if(sum%2==1) return false;int s=sum/2;vector<bool>dp(s+1);//初始化dp[0]=1;//填表for(int i=1;i<=n;i++)for(int j=s;j>=nums[i-1];j--){dp[j]=dp[j]||dp[j-nums[i-1]];}return dp[s];}
};
三. (494.) 目标和


关键点在于如何转化成背包问题,后面的分析思路就比较统一,本质就是在数组中选一些数能否等于目标值。
注意初始化:
由于需要用到上⼀行的数据,因此我们可以先把第⼀行初始化。第⼀行表示不选择任何元素,要凑成目标和 j 。只有当目标和为 0 的时候才能做到,因此第⼀
行仅需初始化第⼀个元素 dp[0][0] = 1
class Solution {
public:int findTargetSumWays(vector<int>& nums, int target) {int s=0,sum=0;int n=nums.size();for(auto e:nums) s+=e;sum=(s+target)/2;if(sum<0||(s+target)%2==1)return 0;vector<vector<int>>dp(n+1,vector<int>(sum+1));dp[0][0]=1;for(int i=1;i<=n;i++)for(int j=0;j<=sum;j++){dp[i][j]=dp[i-1][j];if(j>=nums[i-1]) dp[i][j]+=dp[i-1][j-nums[i-1]];}return dp[n][sum];}
};
空间优化:
class Solution {
public:int findTargetSumWays(vector<int>& nums, int target) {int s=0,sum=0;int n=nums.size();for(auto e:nums) s+=e;sum=(s+target)/2;if(sum<0||(s+target)%2==1)return 0;vector<int>dp(sum+1);dp[0]=1;for(int i=1;i<=n;i++)for(int j=sum;j>=nums[i-1];j--){dp[j]+=dp[j-nums[i-1]];}return dp[sum];}
};
四. (1049.) 最后一块石头的重量 II

可以将问题转化为01背包问题,已知任意两块石头放在一起重量相同会被丢弃,重量不同会进行差值计算并保留,也就是说任意两块石头重量相同部分会被丢弃,重量差异部分相当于在原数据前加上加减符号,所以可以将该题转化为上题目标和一样的思路
当所有元素的和固定时,分成的两部分越接近数组总和的⼀半,两者的差越小。因此问题就变成了:在数组中选择⼀些数,让这些数的和尽量接近 sum / 2 ,如果把数看成物品,每个数的值看成体积和价值,问题就变成了01 背包问题

class Solution {
public:int lastStoneWeightII(vector<int>& s) {int sum=0;int n=s.size();for(auto e:s) sum+=e;int aim=sum/2;vector<vector<int>>dp(n+1,vector<int>(aim+1,0));for(int i=1;i<=n;i++)for(int j=0;j<=aim;j++){dp[i][j]=dp[i-1][j];if(j>=s[i-1]) dp[i][j]=max(dp[i][j],dp[i-1][j-s[i-1]]+s[i-1]);}return sum-2*dp[n][aim];}
};
- 空间优化版本:
class Solution {
public:int lastStoneWeightII(vector<int>& s) {int sum=0;int n=s.size();for(auto e:s) sum+=e;int aim=sum/2;vector<int>dp(aim+1,0);for(int i=1;i<=n;i++)for(int j=aim;j>=s[i-1];j--){dp[j]=max(dp[j],dp[j-s[i-1]]+s[i-1]);}return sum-2*dp[aim];}
};
完全背包问题:
五. 完全背包


- 关于空间优化01背包和完全背包的区别:
都是通过空间覆盖来减少空间消耗,区别在于遍历顺序,遍历顺序的本质是状态依赖方向。01背包怕重复选,所以从右往左保护旧状态,完全背包需要重复选,所以从左往右利用新状态
#include <iostream>
#include<cstring>
using namespace std;const int N=1010;
int dp[N][N],v[N],w[N];
int main() {//获取输入int n, V;cin>>n>>V;for(int i=0;i<n;i++) cin>>v[i]>>w[i];//第一题for(int i=1;i<=n;i++)for(int j=0;j<=V;j++){dp[i][j]=dp[i-1][j];if(j>=v[i-1])dp[i][j]=max(dp[i][j],dp[i][j-v[i-1]]+w[i-1]);}cout<<dp[n][V]<<endl;//第二问memset(dp,0,sizeof(dp));for(int i=1;i<=V;i++)dp[0][i]=-1;for(int i=1;i<=n;i++)for(int j=0;j<=V;j++){dp[i][j]=dp[i-1][j];if(j>=v[i-1]&&dp[i][j-v[i-1]]!=-1)dp[i][j]=max(dp[i][j],dp[i][j-v[i-1]]+w[i-1]);}cout<<(dp[n][V]==-1?0:dp[n][V])<<endl;return 0;
}
- 空间优化版本:
#include <iostream>
#include<cstring>
using namespace std;const int N=1010;
int dp[N],v[N],w[N];
int main() {//获取输入int n, V;cin>>n>>V;for(int i=0;i<n;i++) cin>>v[i]>>w[i];//第一题for(int i=1;i<=n;i++)for(int j=v[i-1];j<=V;j++){dp[j]=max(dp[j],dp[j-v[i-1]]+w[i-1]);}cout<<dp[V]<<endl;//第二问memset(dp,0,sizeof(dp));for(int i=1;i<=V;i++)dp[i]=-1;for(int i=1;i<=n;i++)for(int j=v[i-1];j<=V;j++){if(dp[j-v[i-1]]!=-1)dp[j]=max(dp[j],dp[j-v[i-1]]+w[i-1]);}cout<<(dp[V]==-1?0:dp[V])<<endl;return 0;
}
六. (322.) 零钱兑换


class Solution {
public:int coinChange(vector<int>& coins, int amount) {int n=coins.size();vector<vector<int>>dp(n+1,vector<int>(amount+1));//初始化for(int i=1;i<=amount;i++) dp[0][i]=0x3f3f3f3f;for(int i=1;i<=n;i++)for(int j=0;j<=amount;j++){dp[i][j]=dp[i-1][j];if(j>=coins[i-1]) dp[i][j]=min(dp[i][j],dp[i][j-coins[i-1]]+1);}return dp[n][amount]>=0x3f3f3f3f?-1:dp[n][amount];}
};
空间优化:
class Solution {
public:int coinChange(vector<int>& coins, int amount) {int n=coins.size();vector<int>dp(amount+1);//初始化for(int i=1;i<=amount;i++) dp[i]=0x3f3f3f3f;for(int i=1;i<=n;i++)for(int j=coins[i-1];j<=amount;j++){dp[j]=min(dp[j],dp[j-coins[i-1]]+1);}return dp[amount]>=0x3f3f3f3f?-1:dp[amount];}
};
七. (518.) 零钱兑换 II


class Solution {
public:int change(int amount, vector<int>& coins) {int n=coins.size();vector<vector<unsigned long long>>dp(n+1,vector<unsigned long long>(amount+1));dp[0][0]=1;for(int i=1;i<=n;i++)for(int j=0;j<=amount;j++){dp[i][j]=dp[i-1][j];if(j>=coins[i-1]) dp[i][j]+=dp[i][j-coins[i-1]];}return dp[n][amount];}
};
八. (279.) 完全平方数


class Solution {
public:int numSquares(int n) {int m=sqrt(n);vector<int>dp(n+1,0x3f3f3f3f);dp[0]=0;for(int i=1;i<=m;i++)for(int j=i*i;j<=n;j++){dp[j]=min(dp[j],dp[j-i*i]+1);}return dp[n];}
};
二维费用的背包问题:
九. (474.) ⼀和零


class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int k=strs.size();vector<vector<vector<int>>>dp(k+1,vector<vector<int>>(m+1,vector<int>(n+1)));for(int i=1;i<=k;i++){//统计0(a)、1(b)个数int a=0,b=0;for(auto e:strs[i-1]){if(e=='0') a++;else b++;}for(int j=0;j<=m;j++)for(int x=0;x<=n;x++){dp[i][j][x]=dp[i-1][j][x];if(j>=a&&x>=b)dp[i][j][x]=max(dp[i][j][x],dp[i-1][j-a][x-b]+1);}}return dp[k][m][n];}
};
空间优化:
所有的背包问题,都可以进⾏空间上的优化
对于⼆维费⽤的 01 背包类型的,优化策略是:
i. 删掉第⼀维;
ii. 修改第⼆层以及第三层循环的遍历顺序即可
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int k=strs.size();vector<vector<int>>dp(m+1,vector<int>(n+1));for(int i=1;i<=k;i++){//统计0(a)、1(b)个数int a=0,b=0;for(auto e:strs[i-1]){if(e=='0') a++;else b++;}for(int j=m;j>=a;j--)for(int x=n;x>=b;x--) dp[j][x]=max(dp[j][x],dp[j-a][x-b]+1);}return dp[m][n];}
};
十. (879.) 盈利计划


第三维盈利越多越好肯定,所以k-price[i-1]可能会小于0,但是数组下标不能用负数来表示,所以用0来统一表示下标为负数的情况,取max如果盈利目标-当前盈利小于0就取0,大于0就取本身
class Solution {
public:int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) { const int mod=1e9+7;int m=group.size();vector<vector<vector<int>>>dp(m+1,vector<vector<int>>(n+1,vector<int>(minProfit+1)));//初始化,选择为空集的情况for(int i=0;i<=n;i++) dp[0][i][0]=1;//填表for(int i=1;i<=m;i++)for(int j=0;j<=n;j++)for(int k=0;k<=minProfit;k++){dp[i][j][k]=dp[i-1][j][k];if(j>=group[i-1])dp[i][j][k]+=dp[i-1][j-group[i-1]][max(0,k-profit[i-1])];dp[i][j][k]%=mod;}return dp[m][n][minProfit];}
};
空间优化:
class Solution {
public:int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) { const int mod=1e9+7;int m=group.size();vector<vector<int>>dp(n+1,vector<int>(minProfit+1));//初始化,选择为空集的情况for(int i=0;i<=n;i++) dp[i][0]=1;//填表for(int i=1;i<=m;i++)for(int j=n;j>=group[i-1];j--)for(int k=minProfit;k>=0;k--){dp[j][k]+=dp[j-group[i-1]][max(0,k-profit[i-1])];dp[j][k]%=mod;}return dp[n][minProfit];}
};
似包非包问题:
十一.(377.) 组合总和 Ⅳ


- 背包问题本质求的是组合问题,是无序的,该题是排列问题,是有序的,不能用背包问题的思路来解决,用常规的动规思路来解决.dp[i]通过最后一个元素的位置,依次遍历整个原数组,若小于当前索引值就把dp[i-1]的情况累加起来
class Solution {
public:int combinationSum4(vector<int>& nums, int target) {vector<double>dp(target+1);dp[0]=1;for(int i=1;i<=target;i++)for(auto e:nums)if(i>=e) dp[i]+=dp[i-e];return dp[target];}
};
十二. (96.) 不同的二叉搜索树


从1到当前下标位置依次遍历每一个元素作为根节点时有多少个二叉树。
外层循环 i 表示 “当前要组成的 BST 有 i 个节点(用 1到i 这 i 个数)”;内层循环 j 表示 “选择 j 作为当前 BST 的 根节点”(j 必须是 1到i 中的某个数,因为 BST 的节点值是 1~i 连续的),0不能当作根节点,与题目要求不符,不是合法节点值,也会造成左子树计算越界
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];}
};
总结:
- 关于状态转移方程的优化:
- 01背包:
商品只能选一次,依赖上一层的商品状态,所以状态转移方程一般形如:
dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]) - 完全背包:
可以重复选择商品,所以可以依赖当前层状态,由于重复选择所以无法穷尽状态转移方程,可采用数学等价替换的方式来优化表达式。一般完全背包的优化方式如下:

- 关于空间优化:
- 01背包:
将第一维去掉,利用滚动数组的方式来覆盖计算。改变遍历顺序,从右往左,因为只能选一次商品,依赖上一层的状态,如果从左往右计算可能导致状突被提前覆盖,从右往左就不会 - 完全背包:
将第一维去掉,利用滚动数组的方式来覆盖计算。遍历顺序可以不用改变,因为可以重复选商品,所以不怕当前层元素被覆盖
