动态规划入门之背包问题

01背包

概念:01背包是最基础的背包问题,大概意思就是从n件物品,m的背包容量下选出总价值最大的一些物品装入背包,每件物品只有一件并且每件物品只有选和不选两个状态,因此得名01背包。
状态转移方程:dp[i][j]的含义是前i件物品容量为j的情况下最大价值。

状态转移分析

1.dp[i][j] = dp[i-1][j],第i件物品的体积w[i] > j,也就是装不下,所以前i件物品j容量下所获得的最大价值就是前i-1件物品容量为j下所获得的最大价值。

2.dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]),如果当前容量j >= w[i],那么当前物品我可以选也可以不选。不选就是前i-1件物品的价值(dp[i-1][j],选的话就是dp[i-1][j-w[i]]+v[i]。两者中取一个最大值。

3.最后直接dp[n][m]就可以得到n件物品m容量下装入的最大价值是多少,如果我们需要知道选了那些物品怎么看呢?因为我们每件物品的状态只有选和不选两种状态,也就是对应的dp转移方程dp[i-1][j]和dp[i-1][j-w[i]]+v[i]。
在这里插入图片描述
黑色是当前的状态dp[i][j],如果选了那么必定是从蓝色dp[i-1][j-w[i]]转移过来,如果没选必定是从红色dp[i-1][j]转移过来,所以我们可以回溯去找。如果dp[i][j] = dp[i-1][j],那么第i件物品必定没有选进来,否则第i件物品一定放入了背包中。因此可以得到下面的时间复杂度为O(nm)复杂度,空间复杂度是O(nm)的代码。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1005;
const int maxm = 100005;
struct info{
	int w;			//重量 
	int v;			//价值 
}s[maxn];
int n,m;
int dp[maxn][maxn];
void dfs(int x,int y)
{
	if(x == 0 || y == 0){			//递归出口 
		return ;
	}
	if(dp[x][y] == dp[x-1][y]){		//从正上方转移过来说明没选 
		dfs(x-1,y);
	}
	else{				//当前这个物品选了,从左上角转移过来 
		dfs(x-1,y-s[x].w);
		printf("i = %d, w = %d, v = %d\n",x,s[x].w,s[x].v);
	}
}
void solve()
{
	memset(dp,0,sizeof(dp));
	for(int i = 1;i <= n;i++){			//枚举每一个商品 
		for(int j = 0;j <= m;j++){		//从左往右算 
			if(j < s[i].w){				//当前容量放不下i号物品 
				dp[i][j] = dp[i-1][j];	//那么当前最大价值就是前i-1件物品的最大价值 
			}
			else{	//当前容量可以放下,那么就考虑第i件物品放和不放产生的最大价值 
				dp[i][j] = max(dp[i-1][j],dp[i-1][j-s[i].w]+s[i].v);
			} 
		}
	}
	cout << dp[n][m] << endl;
	dfs(n,m); 
}
int main()
{
	while(cin>>n>>m){
		for(int i = 1;i <= n;i++){
			cin >> s[i].w >> s[i].v;
		}
		solve();
	}
	return 0;
} 
01背包滚动数组优化

在这里插入图片描述
关于优化问题,01背包在时间复杂度上已经无法在继续优化了,但是可以优化空间复杂度。根据上面的分析与代码可以发现,黑色当前状态只由蓝色区域或者红色区域转移过来,并不会由绿色部分转移过来。所以我们可不可以算到dp[i][j]的值覆盖写在dp[i-1][j]的位置上呢?
在这里插入图片描述
如果直接将得到的值覆盖发现更新的值会不对,比如我们更新dp[i][4]的时候应该是dp[i-1][4-2] + 6 = 12,很明显不对,正确答案应该是3 + 6 = 9。为什么会不对?我们将dp[i][j]的值写入了dp[i-1][j]的位置,而dp[i][j]的值只可能大于等于dp[i-1][j],所以我们在更新dp[i][4]的时候实际用的是dp[i][2] + 6 = 12,而不是dp[i-1][2]的值去更新换句话说我们用了第i行j前面的值更新了dp[i][j],所以这个覆盖是失败的。为了保证每次更新都是正确的如果我们从m开始往前更新就不会出现这种错误。但是这样优化空间也有一个缺点就是不知道选了哪些物品,丢失了中间过程,无法回溯找到装入背包的商品编号

int dp[maxm];
void solve_extend()
{
	//dp数组可以优化空间把空间复杂度从O(n*m)降低到O(m);
	//计算dp[i][j]的值时候只用到了dp[i-1][j]的值和dp[i-1][j-s[i].w]的值
	//也就是(i,j)正上方和左上方的值。这是从左往右算的时候的情况 
	//如果想进行滚动数组的优化那么必须从右往左算,因为从左往右算的时候
	//dp[j]的值可能是当前第i行的值不是第i-1行的值 
	memset(dp,0,sizeof(dp));
	for(int i = 1;i <= n;i++){
		for(int j = m;j >= s[i].w;j--){		//从后往前更新保证每次使用的值都是覆盖之前的值
			dp[j] = max(dp[j],dp[j-s[i].w]+s[i].v);
		}
	}
	cout << dp[m] << endl;
} 

完全背包

概念:完全背包大概意思就是从n件物品,m的背包容量下选出总价值最大的一些物品装入背包,每件物品只有无数件,可以任意选择0件,1件,2件,… k件。当然由于有背包的限制所以不可能选出无数件,每种物品最多是m/w[i]件。

状态转移方程:dp[i][j]的含义是前i件物品容量为j的情况下最大价值。

状态转移分析

1.dp[i][j] = dp[i-1][j],第i件物品的体积w[i] > j,也就是装不下,所以前i件物品j容量下所获得的最大价值就是前i-1件物品容量为j下所获得的最大价值。

2.dp[i][j] = max(dp[i-1][j],dp[i-1][j-kw[i]]+kv[i]),如果当前容量j >= kw[i],如果不选第i件物品也就是前i-1件物品的价值(dp[i-1][j],选的话就是dp[i-1][j-kw[i]]+kv[i]。两者中取一个最大值。很容易得到下面的代码,时间复杂度O(nm∑m/w[i]),空间复杂度是O(nm);

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
const int maxn = 1001;
using namespace std;
struct info{
	int w;
	int v;
}s[maxn]; 
int n,m;
int dp[maxn][maxn];
void solve()
{
	memset(dp,0,sizeof(dp));
	for(int i = 1;i <= n;i++){
		for(int j = 0;j <= m;j++){
			for(int k = 0;k*s[i].w <= j;k++){
				dp[i][j] = max(dp[i][j],dp[i-1][j-k*s[i].w] + k*s[i].v);
			}
		}
	}
	cout<<dp[n][m]<<endl;
}
int main()
{
	while(cin>>n>>m){
		for(int i = 1;i <= n;i++){
			cin>> s[i].w >> s[i].v;
		}
		solve();
	}
	return 0;
} 
完全背包与优化

完全背包优化可以优化时间复杂度到O(n*m),空间复杂度可以优化到O(m)。还是刚才那个问题,01背包的优化代码为什么要从后往前更新,是不是因为01背包从前往后更新会导致复用更新后的值再去更新,也就是说当前物品重复选了放入了背包,那我们是不是可以利用这一点来做完全背包呢?01背包滚动数组优化最怕的是不是刚好就是完全背包想要的?

在这里插入图片描述
更新dp[i][4]的时候如果复用dp[i][2]的值会怎么样,是不是相当于dp[i][2]这个状态放入了一个第i件物品,dp[i][4]又放入了一个第i件物品,也就是从dp[i][j] = dp[i-1][j-2w[i]] + 2v[i]。是不是很奇妙?这样就可以把复杂度降低到O(n*m),如果想优化空间也可以把空间降低到O(m)。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1005;
const int maxm = 100005;
struct info{
	int w;			//重量 
	int v;			//价值 
}s[maxn];
int n,m;

int dp[maxn][maxn];
void solve()
{
	memset(dp,0,sizeof(dp));
	for(int i = 1;i <= n;i++){
		for(int j = 0;j <= m;j++){
			if(j < s[i].w){	//如果当前容量放不下第i件物品,那么当前的最大价值就是前i-1件的价值 
				dp[i][j] = dp[i-1][j];
			}
			else{		//如果当前容量够得话,那么考虑第i件物品 不放 和  放多少件的最大价值 
				dp[i][j] = max(dp[i-1][j],dp[i][j-s[i].w]+s[i].v);
			//该转移方程神奇就神奇在从左向右更新过程中dp[i][j]的值是可能被不同数量的第i件物品更新过的
			//这是01背包最忌惮的事情,但是确实完全背包想要达到的目的。
			//dp[i][j] = max(dp[i-1][j],dp[i][j-k*s[i].w] + k*s[i].v);	k = 0恰好是不选的情况 
			}
		}
	}
	cout << dp[n][m] << endl;
}

/*
int dp[maxm];
void solve_extend()				//时间优化 + 空间优化
{
	//dp数组的复杂度从O(nm)降到O(m);
	//从左往右更新恰好能在更新dp[j-w[i]]的情况从已经被第i件物品更新过dp[j]引用过来 
	//01背包滚动数组做法最怕的就是更新dp[j-w[i]]的时候前面的dp[j]以前被第i件商品更新过的值 
	memset(dp,0,sizeof(dp));
	for(int i = 1;i <= n;i++){
		for(int j = s[i].w;j <= m;j++){		//从左往右更新恰好和01背包相反 
			dp[j] = max(dp[j],dp[j-s[i].w]+s[i].v);
		}
	}	
	cout << dp[m] << endl;
} 
*/
int main()
{
	while(cin>>n>>m){
		for(int i = 1;i <= n;i++){
			cin>> s[i].w >> s[i].v;
		}
		solve();
	//	solve_extend();
	} 
	return 0;
} 

多重背包

概念:完全背包大概意思就是从n件物品,m的背包容量下选出总价值最大的一些物品装入背包,每件物品Ci件。当然由于有背包的限制所以也不可能选出无数件。多重背包和完全背包很像,但是又不像,因为完全背包每件商品是无数件,多重背包每件商品都有固定的件数。

状态转移方程:dp[i][j]的含义是前i件物品容量为j的情况下最大价值。

状态转移分析

1.dp[i][j] = dp[i-1][j],第i件物品的体积w[i] > j,也就是装不下,所以前i件物品j容量下所获得的最大价值就是前i-1件物品容量为j下所获得的最大价值。

2.dp[i][j] = max(dp[i-1][j],dp[i-1][j-kw[i]]+kv[i]),如果当前容量j >= kw[i],如果不选第i件物品也就是前i-1件物品的价值(dp[i-1][j],选的话就是dp[i-1][j-kw[i]]+k*v[i]。两者中取一个最大值。很容易得到下面的代码,时间复杂度O(nm∑Ci),空间复杂度是O(nm);

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 105;
struct info{
    int w,v,n;
}s[maxn];
int dp[maxn][maxn];
int n,m;
void solve()
{
    memset(dp,0,sizeof(dp));
    for(int i = 1;i <= n;i++){
        for(int j = 0;j <= m;j++){
            for(int k = 0;k <= s[i].n;k++){
            	if(k*s[i].w <= j){
            		dp[i][j] = max(dp[i][j],dp[i-1][j-k*s[i].w] + k*s[i].v);
				} 
            }
        }
    }
    cout << dp[n][m] << endl;
}
int main()
{
    ios::sync_with_stdio(false);cin.tie(0);
    while(cin >>n >> m){
        for(int i = 1;i <= n;i++){
            cin >> s[i].w >> s[i].v >> s[i].n;
        }
        solve();
    }
    return 0;
}
多重背包与优化

多重背包的优化大致有三类:

  1. 将多重背包拆成最裸的01背包来做,也就是∑Ci件物品,m的容量下的一个朴素01背包。

  2. 采用二进制分解优化。

3.单调队列优化,目前还不会。

二进制分解优化

从2 ^ 0, 2 ^ 1,2 ^ 2,2 ^ 3,2 ^ (k-1)这k个2的整数次幂中选出若干个相加,可以表示出0~2 ^ k - 1之间的任何整数。进一步的,我们求出满足
2 ^ 0 + 2 ^ 1 + 2 ^ 2 + … + 2 ^ p <= Ci的最大整数p,设R = Ci - 2 ^ 0 - 2 ^ 1 - 2 ^ 2 - … - 2 ^ p。

  1. 根据最大性,有2 ^ 0 + 2 ^ 1 + 2 ^ 2 + … + 2 ^ p + 2 ^ (p+1) > Ci;因此从2 ^ 0, 2 ^ 1,2 ^ 2,2 ^ 3,2 ^ p,中选出若干个相加可以表示出0~R之间的任何整数。

  2. 从2 ^ 0, 2 ^ 1,2 ^ 2,2 ^ 3,2 ^ p,以及R中选出若干个相加可以表示出R~R+2^(p+1) - 1之间的任何整数。而根据R的定义,R + R+2 ^ (p+1) - 1 = Ci;因此从2 ^ 0, 2 ^ 1,2 ^ 2,2 ^ 3,2 ^ p,R中选出若干个相加可以表示出R ~ Ci之间的任何整数。

  3. 综上所述,我们可以将数量Ci拆成 p + 2个物品,体积分别是 2 ^ 0×W[i], 2 ^ 1×W[i],2 ^ 2×W[i],2 ^ 3×W[i],2 ^ p×W[i],R×W[i]。这样物品总数就变成了O(∑logCi)。转化完就可以继续套用01背包解题了。总体时间复杂度还是相当棒的O(m*∑logCi)。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
struct info{
	int h;
	int p;
}s[2005];
int dp[105];
inline void solve(int n,int m)
{
	memset(dp,0,sizeof(dp));
	for(int i = 0;i < n;i++){
		for(int j = m;j >= s[i].p;j--){
			dp[j] = max(dp[j],dp[j-s[i].p]+s[i].h);
		}
	}
	cout << dp[m] << endl;
}
int main()
{
	ios::sync_with_stdio(false);cin.tie(0);
	int t,n,m;;
	cin >> t;
	while(t--){
		cin >> m >> n;
		int cnt = 0;
		for(int i = 0;i < n;i++){
			int x,y,z;
			cin >> x >> y >> z;
			int t = 1;
			while(z - t > 0){
				s[cnt].p = t*x;
				s[cnt++].h = t*y;
				z -= t;
				t <<= 1;
			} 
			s[cnt].p = z*x;s[cnt++].h = z*y;
		}
		solve(cnt,m);
	}
	return 0;
}

愿你走出半生,归来仍是少年~

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值