本人的第一篇blog,可能问题比较多,权当是个人整理的笔记吧
现有一个容量大小为 m m m的背包和 n n n件物品,每件物品有两个属性,体积和价值,请问这个背包最多能装价值为多少的物品?
这种方法就不用说了吧,来学DP的肯定会枚举
时间复杂度 O ( 2 n ) O(2^n) O(2n),一般情况下必然TLE
这种方法比上一种好一点,仍然是一种暴力的方法,常数上有所优化,但仍然是 O ( 2 n ) O(2^n) O(2n),一般情况下一定会TLE
这里可以注意到,DFS中出现了重复计算某种情况的问题,而时间复杂度正是堆积在这一点上,那么我们就可以对此进行解决,这衍生出了:
这种方法记录状态的答案,可以达到每种状态仅遍历一次,也就是时间复杂度减到了 O ( n m ) O(nm) O(nm)
这里对状态的记录,以及通过计算来转移至其他状态,就可以算是一种动态规划(DP)
同时这个方法也不难理解,下面给出代码:
#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[105][2005];
int dfs(int a,int b){
if(dp[a][b]){
return dp[a][b];
}
if(a==0){
return 0;
}
dp[a][b]=max(dfs(a-1,b-w[a-1])+v[a-1],dfs(a-1,b));
return dp[a][b];
}
signed main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++){
scanf("%d",w+i);
scanf("%d",v+i);
}
printf("%d",dfs(n,m));
return 0;
}
下面我们正式开始动态规划,令dp[i][j]
为前i
个物品用容量为j
的物品(你品,你细品)
然后我们就得到了以下的状态转移方程:
d p i , j = m a x ( d p i − 1 , j , d p i − 1 , j − w i + c i ) dp_{i,j}=max(dp_{i-1,j},dp_{i-1,j-w_i}+c_i) dpi,j=max(dpi−1,j,dpi−1,j−wi+ci)
就是两种情况:取或不取
然后,我们直接通过循环算出所有范围内的dp的值就可以得到 d p n , m dp_{n,m} dpn,m也就是当前问题的解。
程序如下:
#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[105][2005];
signed main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++){
scanf("%d",w+i);
scanf("%d",v+i);
}
for(int i=1;i<=n;i++){
for(int j=w[i];j<=m;j++){
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
}
printf("%d",dp[n][m]);
return 0;
}
时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度也是 O ( n m ) O(nm) O(nm)
这里的时间复杂度无法继续优化,但是空间可以。
我们注意到方程中第一维一直是 i − 1 i-1 i−1或 i i i,也就是说我们可以直接省去它,反正我们只需要最终的 d p n , m dp_{n,m} dpn,m,于是,我们需要保证计算到 d p j dp_{j} dpj时, d p j − w [ i ] dp_{j-w[i]} dpj−w[i]是未被计算的,那么我们就需要逆序循环。
代码如下:
#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[2005];
signed main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++){
scanf("%d",w+i);
scanf("%d",v+i);
}
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
dp[i][j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
printf("%d",dp[m]);
return 0;
}
时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度 O ( n + m ) O(n+m) O(n+m)
时间复杂度
现有一个容量大小为 m m m的背包和 n n n种物品,每种物品有两个属性,体积和价值,且都有无数件,请问这个背包最多能装价值为多少的物品?
令dp[i][j]
为前i
个物品用容量为j
的物品
由题意可得,
d p i , j = m a x ( d p i − 1 , j , d p i , j − w i + c i ) dp_{i,j}=max(dp_{i-1,j},dp_{i,j-w_i}+c_i) dpi,j=max(dpi−1,j,dpi,j−wi+ci)
也是两种情况,一种是进入下一个物品,一种是再选一种当前物品
代码如下:
#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[105][2005];
signed main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++){
scanf("%d",w+i);
scanf("%d",v+i);
}
for(int i=1;i<=n;i++){
for(int j=w[i];j<=m;j++){
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
}
}
printf("%d",dp[n][m]);
return 0;
}
时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度 O ( n m ) O(nm) O(nm)
参照上文,状态转移方程变为
d p j = m a x ( d p j , d p j − w i + c i ) dp_{j}=max(dp_{j},dp_{j-w_i}+c_i) dpj=max(dpj,dpj−wi+ci)
也是两种情况,一种是进入下一个物品,一种是再选一种当前物品
跟之前的01背包完全一样?这里要注意正序遍历,看看之前的状态转移方程就清楚了
代码如下:
#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[2005];
signed main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++){
scanf("%d",w+i);
scanf("%d",v+i);
}
for(int i=1;i<=n;i++){
for(int j=w[i];j<=m;j++){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
printf("%d",dp[m]);
return 0;
}
时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度 O ( n + m ) O(n+m) O(n+m)
现有一个容量大小为 m m m的背包和 n n n种物品,每种物品有三个属性,体积和价值和数量,请问这个背包最多能装价值为多少的物品?
直接把每个物品展开,然后直接01背包
十分暴力,下面是代码:
#include
#define int long long
using namespace std;
int n,m,sum,g,ans;
int w[100005],v[100005],a,b,c;
int dp[200005];
signed main(){
scanf("%lld%lld",&m,&sum);
for(int i=1;i<=sum;i++){
scanf("%lld",&c);
scanf("%lld",&a);
scanf("%lld",&b);
for(int j=1;j<=c;j++){
n++;
w[n]=a*1;
v[n]=b*1;
}
}
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
printf("%lld",dp[m]);
return 0;
}
时间复杂度 O ( m ∑ i = 0 n c i ) O(m\sum_{i=0}^n c_i) O(m∑i=0nci),空间复杂度 O ( m + ∑ i = 0 n c i ) O(m+\sum_{i=0}^n c_i) O(m+∑i=0nci)
显然之前那种直接展开的方式极为不必要
我们只是需要凑出0至c[i]的所有组合就行了,那么,自然会想到伟大的二进制,设有 2 x ≤ c [ i ] ≤ 2 x + 1 2^x\leq c[i]\leq 2^{x+1} 2x≤c[i]≤2x+1,那么把c[i]拆成 2 0 , 2 1 , 2 2 … … 2 x − 1 2^0,2^1,2^2……2^{x-1} 20,21,22……2x−1,可以凑出0至 2 x − 1 2^x-1 2x−1的所有数,加上剩下的那一部分,正好够凑出0至c[i]的所有数。
拆完之后仍然是直接01背包,下面是代码:
#include
#define int long long
using namespace std;
int n,m,sum,g,ans;
int w[100005],v[100005],a,b,c;
int dp[200005];
signed main(){
scanf("%lld%lld",&m,&sum);
for(int i=1;i<=sum;i++){
scanf("%lld",&c);
scanf("%lld",&a);
scanf("%lld",&b);
for(int j=0;(1<<j)<=c;j++){
c-=(1<<j);
n++;
w[n]=a*(1<<j);
v[n]=b*(1<<j);
}
if(c){
n++;
w[n]=a*c;
v[n]=b*c;
}
}
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
printf("%lld",dp[m]);
return 0;
}
时间复杂度 O ( m ∑ i = 0 n log 2 c i ) O(m\sum_{i=0}^n \log_{2}c_i) O(m∑i=0nlog2ci),空间复杂度 O ( m + ∑ i = 0 n log 2 c i ) O(m+\sum_{i=0}^n \log_{2}c_i) O(m+∑i=0nlog2ci)
现有一个容量大小为 m m m的背包和 n n n件物品,这 n n n件物品分为 k k k组,每组物品有c[i]件,每件物品有对应的体积和价值,每组物品至多选择一件,请问这个背包最多能装价值为多少的物品?
同样是01背包的思路,但是原先的“每个物品”变成了现在的“每组物品”,相应的,要考虑的情况也从“选和不选”变成了“不选,选第一个,选第二个……选第 c i c_i ci个”,再加一层循环即可。
和01背包相差不大,以下是最为直接的做法:
#include
using namespace std;
int n,m;
int dp[105][105],w[105][105],v[105][105],c[105];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",c+i);
for(int j=1;j<=c[i];j++){
scanf("%d%d",&w[i][j],&v[i][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
dp[i][j]=dp[i-1][j];
for(int k=1;k<=c[i];k++){
if(j>=w[i][k]){
dp[i][j]=max(dp[i][j],dp[i-1][j-w[i][k]]+v[i][k]);
}
}
}
}
printf("%d\n",dp[n][m]);
return 0;
}
时间复杂度 O ( m ∑ i = 0 n c i ) O(m\sum_{i=0}^n c_i) O(m∑i=0nci),空间复杂度 O ( m ∑ i = 0 n c i ) O(m \sum_{i=0}^n c_i) O(m∑i=0nci)
这个已经讲过很多次了,不用再一次描述了吧
直接放出代码:
#include
using namespace std;
int n,m;
int dp[105],w[105][105],v[105][105],c[105];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",c+i);
for(int j=1;j<=c[i];j++){
scanf("%d%d",&w[i][j],&v[i][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
dp[j]=dp[j];
for(int k=1;k<=c[i];k++){
if(j>=w[k]){
dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
}
}
}
}
printf("%d\n",dp[m]);
return 0;
}
时间复杂度 O ( m ∑ i = 0 n c i ) O(m\sum_{i=0}^n c_i) O(m∑i=0nci),空间复杂度 O ( m + ∑ i = 0 n c i ) O(m+\sum_{i=0}^n c_i) O(m+∑i=0nci),仍然是非常有效的优化