动态规划背包问题
无后效性
无后效性是一个问题可以用动态规划求解的标志之一,理解无后效性对求解动态规划类题目非常重要
概念:某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响(某度上找的定义)
理解:无后效性指的是之前做过的事现在还可以继续去做,这便是前一阶段做的事对后一阶段无影响。如果前面做过了后面便不能去做或者做的事受限这便是有后效性
01背包问题
问题描述:
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
我们可以用一个数组dp[i][c]来表示考虑前i个物品并且剩余容量为c时的最大价值。
首先要说明它满足最优子结构性质:
- 假设最优解为($x_1, x_2$, …).$x_i$可以选取0或1.
- 现在随机选取最优解中的部分($x_1, x_2,…x_i$)。假设它不是子问题的最优解,那么一定存在一个比他更优的解($y_1, y_2,…y_i$)。使得value($x_1, x_2,…x_i$)<value($y_1, y_2,…y_i$)
- 则value($y1, y_2, …y_i, x{i+1}…x_n$)>value($x_1, x_2,…x_n$)。与假设矛盾
- 因此($x_1, x_2, … x_i$)一定是子问题的最优解
我们只考虑第i件和第i-1件,如果第i件没有装,那么dp[i][j] = dp[i-1][j].如果第i件装了,那么剩余容量就会减小并且加上第i件物品的价值为dp[i][j] = dp[i-1][j-goods[i].w] + goods[i].v;
递推关系式为
struct Good{ |
实际上这里的二维数组可以优化变为一维数组
dp[i][j] = max{dp[i-1][j-w[i]]+v[i],dp[i-1][j],这里的i与i-1实际上是第i个物体与第i-1个物体,而这个可以用数组下标直接代替,这样便可以用一维数组解决背包问题,但是一维数组与二维数组的区别是二维数组保存了前i个物品所可以获得的最大价值,而一维数组只能保存题目要求的s个物品的最大价值,因此用一维还是用二维因题目而异
dp[j] = max{dp[j],dp[j-w[i]]+v[i]}。从这个方程中我们可以发现,有两个dp[j],但是要区分开。等号左边的dp[j]是当前i的状态,右边中括号内的dp[j]是第i-1状态下的值。
所以为了保证状态的正确转移,我们需要先更新等号左边中的dp[j](当前状态的dp[j])。
代码:
using namespace std;
int main()
{
int n,maxv;
cin>>n>>maxv;
int w[1001],v[1001];
int dp[1001];
for(int i=1;i<=n;i++)
{
cin>>w[i]>>v[i];
}
for(int i=0;i<=maxv;i++)
{
dp[i]=0;
}
for(int i=1;i<=n;i++)
{
for(int k=maxv;k>=w[i];k--)/*这里是因为当背包体积小于物品体积时不可能成立,相当于if(k<w[i]){dp[i][k]=dp[i-1][k];}*/
{
dp[k]=max(dp[k],dp[k-w[i]]+v[i]);
}
}
cout<<dp[maxv]<<endl;
return 0;
}
在这段代码中,k之所以要从maxv出发,是因为现在我们更新的dp[k]可能会对之后的求解造成影响。
例如:
- 当i=1时,这个物品重量是2,价值是3,也就是dp[2] = 3
- 然后求到dp[4]时,dp[4] = dp[4-2]+3 > dp[4].所以现在dp[4] = 6.但是实际上应该是3,这就是前面更新的dp[2]影响到了后面求解,破坏了无后效性。
- 最开始那个之所以可以从前往后求解是因为它是根据dp[i-1]进行转移的,dp[i-1]的值已经确定了,这时我们更新就不会影响求解。
拓展问题
继续0-1背包问题,如果在上面的问题加上一个限制条件,所选择的物品必须恰好装满背包,否则输出-1。这时数组初始化为负无穷
#include <iostream> |
其它大致相同,现在dp[i][j]表示的是恰好装j空间时价值的最大值
元组法:
这是动态规划数组,可以看到里面存了大量重复的值,我们可以只记录变化的值(图中红色部分)从而减小空间消耗。
首先i=4时是在i=5基础上变化的。i=4时w=5,v=4,所以跳点可能是(0, 0) (5,4) (4, 6) (9, 10). 因为(5, 4)比(4, 6)重量大价值低所以无论后面怎么增加一定没有(4, 6)好,因此可以把(5, 4)省略
完全背包
题目:有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。for (i = 1; i <= n; i++)
{
for (j = goods[i].w; j <= S; j++)
dp[j] = max(dp[j], dp[j - goods[i].w] + goods[i].v);
}//不是很理解,但是先把模板记下吧
这个代码和01背包代码十分相似,只有循环方向不同,为什么可以这样做呢?因为01背包要求每个物品只能选一次,因此根据这个式子,有可能会导致多选的就是dp[j - goods[i].w],如果从前往后循环,j-goods[i].w可能是已经选择了当前物品的情况
多重背包
有N种物品和一个容量为V的背包。第 i 种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
实际上可以转化为01背包,把每一种物品中多件拆开int k = n + 1;
for (int i = 1; i <= n; i++) {
while (number[i]>1) {
w[k] = w[i];
value[k] =value[i];
k++;
number[i]--;
}
}
for (int i = 1; i <= n; i++) {
for (int j = v; j >= w[i]; j--) {
f[j] = max(f[j], f[j - w[i]] + value[i]);
}
}
但是这样可能导致时间复杂度过高,可以考虑采用二进制思想,每一个数例如7可以用二进制来表示,而这样我们就可以把它拆为3个数,7二进制为111,所以可以拆为100,010,001,这样我们就只需要储存3个数,降低了时间复杂度。但是假如不是正好的话
例如13 ,二进制为1101,则可以分解为0110,0001,0010,0100(如果最好不足2^i,则取x-2^i-1)int num[maxn][2], dp[maxn];
int N, V, c, w, n, tot;
memset(dp, 0, sizeof dp);
cin >> V >> N; tot = 1;
for(int i = 1; i <= N; ++i)
{
cin >> c >> w >> n;
for(int k = 1; k < n; k<<=1)//左移求下一个所需二进制数
{
num[tot][0] = k*c;
num[tot++][1] = k*w;//注意这时我们把若干物体看为1个物体,它的总重量也要变
n -= k;
}
num[tot][0] = n*c;
num[tot++][1] = n*w;
}
for(int i = 1; i < tot; ++i)
{
for(int j = V; j >= num[i][0]; --j)
dp[j] = max(dp[j], dp[j-num[i][0]]+num[i][1]);
}