动态规划——完全背包问题
动态规划——完全背包问题
- 完全背包问题
- 二维数组定义状态(不要求装满)
- 二维数组定义状态(要求装满)
- 二维数组定义状态优化
- 用一维数组进行空间优化
- 完全背包二进制优化
- 求完全背包的方案数
- 体积和价值同数值
- 体积和价值不同数值
- 求完全背包的具体方案
- 完全背包的变种
- Buying Hay S(限定条件变更)
- 纪念品(股票问题 贪心+dp)
- 完全背包的OJ汇总
完全背包问题
有
N
N
N 种物品和一个容量是
V
V
V 的背包,每种物品都有无限件可用(但碍于
包的容量每件物品最多可装floor(V/w[i])
件,即不超过V/w[i]
的最大整数件,w[i]
是1个第i
种物品的质量)。
第 i i i 种物品的体积是 w i w_i wi,价值是 v i v_i vi(weight,重量(体积);value,价值)。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
二维数组定义状态(不要求装满)
1268:【例9.12】完全背包问题
3. 完全背包问题 - AcWing题库
P1616 疯狂的采药 - 洛谷
这个问题类似于01背包问题,不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件…等很多种。
- 状态定义
令dp[i][j]
表示前i
种物品,每种选若干个,这些物品恰好放入一个容量为j
的背包时的最大价值。
- 转移方程
当只有1种物品的时候,没得选,包能塞多少就装多少。
当不止有1种物品时,假设前i-1
种物品都已经选择好了最大价值的方案,第i
种物品的:数量和最大价值的关系:
当i
物品不选时,dp[i][j]=dp[i-1][j]
,
当选1件i
物品时,dp[i][j]=max{dp[i-1][j],dp[i-1][j-w[i]]+v[i]}
当选2件i
物品时,
dp[i][j]=max{dp[i-1][j],dp[i-1][j-w[i]]+v[i],dp[i-1][j-2*w[i]]+v[i]}
⋯
\cdots
⋯
当选k
件i
物品时(k=V/w[i]
),
dp[i][j]=max{dp[i-1][j],dp[i-1][j-w[i]]+v[i],...,dp[i-1][j-k*w[i]]+k*v[i]}
当选k+1
件物品时,包已经放不下了,所以就不选。
在众多的方案每次都选择最大价值的那个。
所以状态转移方程:
dp[i][j]=max{
dp[i-1][j],
dp[i-1][j-w[i]]+v[i],
dp[i-1][j-2*w[i]]+2*v[i],
...
dp[i-1][j-k*w[i]]+k*v[i]
}
- 初始化&填表
dp[0][j]
表示无物品的情况下容量为j
的背包能存放的最大价值,直接初始化为0即可。
填表的循环方式是i
在外层,之后j
嵌套k
。i
表示某种物品,j
表示背包容量,注意j
要从0开始,确保不漏一个状态哪怕是非法状态,否则可能最后的答案不对。即确定好包的容量和物品的数量后对不同数量的同种物品进行枚举。
所以代码实现:
for(int i=1;i<=n;i++)
for(int j=0;j<=V;j++)//正向枚举和逆向枚举都能得到结果
for(int k=0;k<=V/w[i];k++)
if(j>=k*w[i])
dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
假设所有种类的物品平均被划分的个数是 K K K,则时间复杂度是 O ( N ∗ V ∗ K ) O(N*V*K) O(N∗V∗K),当三个量的数量级相同时,这个算法的时间复杂度便来到 O ( n 3 ) O(n^3) O(n3),在数据量稍微大一点时(比如1000种物品放进容积为1000的包里)表现并不是特别理想。
但解决数量级小的OJ还是没问题的。
数量级小的比如1268:【例9.12】完全背包问题,参考程序之一:
#include<iostream>
#include<vector>
using namespace std;
void ac1(){
int dp[101][201]={0};
int w[101]={0},v[101]={0};
int n,V;
cin>>V>>n;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++)
for(int j=0;j<=V;j++)
for(int k=0;k<=V/w[i];k++)
if(j>=k*w[i])
dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
cout<<"max=";
cout<<dp[n][V];
}
int main() {
ac1();
return 0;
}
但在3. 完全背包问题 - AcWing题库、P1616 疯狂的采药 - 洛谷,这个思路就不行了。所以还要继续优化。
二维数组定义状态(要求装满)
【模板】完全背包
和01背包一样,方程没有变化,变化的只有初始化。
即dp[0][j]
除了dp[0][0]=0
,其他都是初始化为无穷小。
参考程序(部分样例超时):
#include<iostream>
#include<vector>
using namespace std;
int N, V;
vector<int>w, v;
vector<vector<int> >dp;
void init() {
cin >> N >> V;
w.resize(N + 1, 0);
v = w;
dp.resize(N + 1, vector<int>(V + 1, 0));
for (int i = 1; i <= N; i++)
cin >> w[i] >> v[i];
}
void ac() {
//第1问
for (int i = 1; i <= N; i++)
for (int j = 0; j <= V; j++) {
for (int k = 0; k <= j / w[i]; k++)
if (j >= k * w[i])
dp[i][j] = max(dp[i][j],
dp[i - 1][j - k * w[i]] + k * v[i]);
}
cout << dp[N][V]<<endl;
//第2问
for (int i = 0; i <= N; i++)
for (int j = 0; j <= V; j++)
dp[i][j] = -0x3f3f3f3f;
dp[0][0] = 0;
for (int i = 1; i <= N; i++)
for (int j = 0; j <= V; j++) {
for (int k = 0; k <= j / w[i]; k++)
if (j >= k * w[i])
dp[i][j] = max(dp[i][j],
dp[i-1][j - k * w[i]] + k * v[i]);
}
if (dp[N][V] < 0)
cout << 0;
else
cout << dp[N][V];
}
int main() {
init();
ac();
return 0;
}
很明显,三个循环的转移方程在处理【模板】完全背包时会超时,所以还需要进行优化。
二维数组定义状态优化
完全背包的状态转移方程:
dp[i][j]=max{
dp[i][j],
dp[i-1][j-w[i]]+v[i],
dp[i-1][j-2*w[i]]+2*v[i],
...
dp[i-1][j-k*w[i]]+k*v[i]
}
再看这个状态:前i
种物品放进容量为j-w[i]
的包时的最大价值dp[i][j-w[i]]
的转移方程:
dp[i][j-w[i]]=max{
dp[i][j-w[i]],
dp[i-1][j-2*w[i]]+v[i],
dp[i-1][j-3*w[i]]+2*v[i],
...
dp[i-1][j-(k+1)*w[i]]+k*v[i]
}
第一个方程中j-k*w[i]
随着k
增大无限接近于0,所以dp[i-1][j-k*w[i]]
→
\rightarrow
→dp[i-1][j]
。而第一个方程中{}
内的表达式数量只比第二个方程的多1个dp[i][j]
。所以两边都加一个v[i]
:
dp[i][j-w[i]]+v[i]=max{
dp[i][j-w[i]]+v[i],
dp[i-1][j-2*w[i]]+2*v[i],
dp[i-1][j-3*w[i]]+3*v[i],
...
dp[i-1][j-k*w[i]]+k*v[i]
}
对比三个方程,发现第一个方程的大部分式子可以用第三个方程等效替换:
dp[i][j]=max{dp[i-1][j],dp[i][j-w[i]]+v[i]};
到这里,第一个转移方程经过数学思维的简单替换已经得到了很好的优化。但还是要注意就是j-w[i]
可能不存在,即j<w[i]
。
初始化:转移方程用二维数组代替。根据方程,转移方向如图:
假设红色三角形是dp[i][j]
的位置,两个方向的箭头表示的是方程的转移方向。可以看到,当dp[i-1][j]
是往低层的状态转移,而dp[i][j-w[i]]
是同层状态往左。类比到二维数组就是,二维数组当前的值要么往上找,要么往左找,往上找是找最近的一个,而往左则不一定。
所以递推表(dp表)的填表顺序:是从左往右,从上往下,保证每个状态都可以正常转移。在程序中表现就是两个循环嵌套,标记循环状态的变量都是升序。
代码实现:
//二选一即可
void f(){
for(int i=1;i<=N;i++)
for(int j=1;j<=V;j++)
if(j>=w[i])
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
else
dp[i][j]=dp[i-1][j];
}
void f2(){
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++){
dp[i][j]=dp[i-1][j];
if(j>=w[i])
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
}
}
3. 完全背包问题 - AcWing题库参考程序:
#include <iostream>
using namespace std;
int m, n;
int dp[1001][1001] = { 0 };
int w[1001] = { 0 }, v[1001] = { 0 };
int maxx = 0;
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> w[i] >> v[i];
//枚举到第几号物品 (01背包是第几件),因为可放数量不固定。
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {//01背包是逆向
if (j >= w[i])
dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + v[i]);
else
dp[i][j] = dp[i - 1][j];
}
}
cout << dp[n][m] << endl;
return 0;
}
同样的,Nowcoder的这道OJ【模板】完全背包的第2问也是一样的方程,除了dp[0][j]
初始化为无穷小,dp[0][0]=0
。
AC参考程序:
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#include<iostream>
#include<vector>
using namespace std;
int N, V;
vector<int>w, v;
vector<vector<int> >dp;
void init() {
cin >> N >> V;
w.resize(N + 1, 0);
v = w;
dp.resize(N + 1, vector<int>(V + 1, 0));
for (int i = 1; i <= N; i++)
cin >> w[i] >> v[i];
}
void ac() {
//第1问
for (int i = 1; i <= N; i++)
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= w[i])
dp[i][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
}
cout << dp[N][V] << endl;
//第2问
//因为用的是同一个数组,为了将影响降到最小,
//将数组还原为最初的状态
for (int i = 0; i <= V; i++)
dp[0][i] = -0x3f3f3f3f;
for (int i = 1; i <= N; i++)
for (int j = 0; j <= V; j++)
dp[i][j] = 0;
dp[0][0] = 0;
for (int i = 1; i <= N; i++)
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= w[i])
dp[i][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
}
if (dp[N][V] < 0)
cout << 0;
else
cout << dp[N][V];
}
int main() {
init();
ac();
return 0;
}
P1616 疯狂的采药 - 洛谷这题因为数据量太大,空间会超限。所以还需要继续优化。
用一维数组进行空间优化
观察二维状态的转移过程,以及状态转移方程:
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
发现在每次转移时可以先做初始化:dp[i][j]=dp[i-1][j]
,
再用max
进行判断,因为枚举到这一行时,上方的表已经填完。
因此定义状态dp[j]
为包容量为j
的包,塞哪些物品可以获得最大价值。
于是转移方程变成了
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
但填表顺序也就是程序中的内层循环方向要升序,因为每个格子更新要用到左边的格子,只有左边的状态是最优解,才能保证正确更新。
同时为了优化循环次数,内层循环范围从[1,V]
变成[w[i],V]
。
3. 完全背包问题 - AcWing题库参考程序:
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#include<iostream>
#include<vector>
using namespace std;
int N, V;
vector<int>w, v;
vector<int>dp;
void init() {
cin >> N >> V;
w.resize(N + 1, 0);
v = w;
dp.resize(V + 1, 0);
for (int i = 1; i <= N; i++)
cin >> w[i] >> v[i];
}
void ac() {
//第1问
for (int i = 1; i <= N; i++)
for (int j = w[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
cout << dp[V] << endl;
//第2问
//因为用的是同一个数组,为了将影响降到最小,
//将数组还原为最初的状态
for (int i = 0; i <= V; i++)
dp[i] = -0x3f3f3f3f;
dp[0] = 0;
for (int i = 1; i <= N; i++)
for (int j = w[i]; j <= V; j++)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
if (dp[V] < 0)
cout << 0;
else
cout << dp[V];
}
int main() {
init();
ac();
return 0;
}
P1616 疯狂的采药 - 洛谷
这题和01背包的采药对应,但每种草药无限选,而且因为数据量大,只能用空间优化。
翻译成完全背包问题,部分要素:
采药时间==
体积,价值不变,等待时间==
背包容量。
但需要注意一种情况,背包容量是
1
0
7
10^7
107,假设某种物品的体积和价值分别是
1
1
1和
1
0
4
10^4
104,则全选这个物品的总价值是
1
0
11
10^{11}
1011,所以int
无法存储这么大的数,需要用long long
。
参考程序:
#include<bits/stdc++.h>
using namespace std;
int main(){
int V,N;
cin>>V>>N;
vector<long long>w(N+1,0),v(w);
vector<long long>dp(V+1,0);
for(int i=1;i<=N;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=N;i++)
for(int j=w[i];j<=V;j++)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
cout<<dp[V];
return 0;
}
完全背包二进制优化
二进制优化的具体思路:
将第 i i i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1 , 2 , 4 , . . . , 2 k − 1 , V w i − 2 k + 1 1,2,4,...,2^{k-1}, \frac{V}{w_i}-2^{k}+1 1,2,4,...,2k−1,wiV−2k+1,且 k k k是满足 V w i − 2 k + 1 > 0 \frac{V}{w_i}-2^k+1>0 wiV−2k+1>0的最大整数。
这些系数已经可以组合出
[
1
,
[
V
w
i
]
]
[1,[\frac{V}{w_i}]]
[1,[wiV]]内的所有数字。例如,如果
[
V
w
i
]
[\frac{V}{w_i}]
[wiV]为13,就将这种物品分成系数分别为{1,2,4,6}
的四件物品。
分成的这几件物品的系数和为
[
V
w
i
]
[\frac{V}{w_i}]
[wiV],表明不可能取多于
[
V
w
i
]
[\frac{V}{w_i}]
[wiV]件的第i
种物品。
每种物品都按类似的方法去分,就将 i i i 种物品分成了约 Σ ( log 2 [ V w i ] + 1 ) \Sigma (\log_2 [\frac{V}{w_i}]+1) Σ(log2[wiV]+1)种物品,将原问题转化为了时间复杂度为 O ( V ⋅ [ log 2 [ V w i ] ] ) O(V\cdot [\log_2 [\frac{V}{w_i}]]) O(V⋅[log2[wiV]])的01背包问题,是很大的改进。代价是额外申请了多余的空间,算是空间换时间的一种做法。
记 [ V w i ] [\frac{V}{w_i}] [wiV]为 n i n_i ni。
数列 { 1 , 2 , 4 , . . . , 2 k − 1 , n i − 2 k + 1 } \{1,2,4,...,2^{k-1},n_i-2^{k}+1\} {1,2,4,...,2k−1,ni−2k+1}一共有 k + 1 k+1 k+1项,
设前 k k k项的和为 S S S,根据等比数列前 n n n项和公式,
S = 1 − 2 k 1 − 2 = 2 k − 1 S=\frac{1-2^{k}}{1-2}=2^{k}-1 S=1−21−2k=2k−1,于是 k = log 2 ( S + 1 ) k=\log_2(S+1) k=log2(S+1)。
已知 n i ≥ S n_i\geq S ni≥S,所以第 i i i种物品分成了 [ log 2 n i ] + 1 [\log_2n_i]+1 [log2ni]+1(大于 log 2 n i \log_2 n_i log2ni的最小整数)种系数为2的若干次幂的物品。
所有物品也就分成了 Σ ( [ log 2 n i ] + 1 ) \Sigma ([\log_2n_i]+1) Σ([log2ni]+1)种。
多余的空间也可以根据
[
log
2
[
V
w
i
]
]
+
1
[\ \log_2 [\frac{V}{w_i}]\ ]+1
[ log2[wiV] ]+1进行计算。例如
[
V
w
i
]
≤
20
[\frac{V}{w_i}]\leq 20
[wiV]≤20,则每种物品最多能分成{1,2,4,8,5}
这5个不同系数的物品,最多有
T
T
T种类似的物品,所以用到的空间最多为
5
T
5T
5T。
1268:【例9.12】完全背包问题参考程序之一:
#include <iostream>
using namespace std;
int m, n;
int dp[201] = { 0 };
int w[10001] = { 0 }, v[10001] = { 0 };
int num;
int main() {
cin >> m >> n;
//在读取数据的阶段用二进制优化
for (int i = 1, tw, tv, s, tn; i <= n; i++) {//tw: template weight
tn = 1;
cin >> tw >> tv;//template wight
s = m / tw;//完全背包每种物品的上限
while (s >= tn) {
w[++num] = tn * tw;
v[num] = tn * tv;
s -= tn;
tn *= 2;
}
w[++num] = s * tw;
v[num] = s * tv;
}
//01背包的状态转移方程
for (int i = 1; i <= num; i++)
for (int j = m; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
cout << "max=" << dp[m];
return 0;
}
求完全背包的方案数
体积和价值同数值
1273:【例9.17】货币系统
1293:买书
这两个OJ都可以看成是体积和价值同数值的物品,装满容量有限的背包的方法数。
1273:【例9.17】货币系统是问不同纸币的面额组成新面额的方法数。
借用之前01背包的方法数的思路:
- 状态定义:
dp[i][j]
表示从[1,i]
中选物品,使得价值恰好为j
的方法数。 - 转移方程:从最后一个状态出发。
i
面值的纸币都有不选或选到底两种决策,所以
dp[i][j]= dp[i-1][j]+//不选
dp[i-1][j-w[i]]+//选1个
dp[i-1][j-2*w[i]]+//选两个
...
发现
dp[i][j-w[i]]= dp[i-1][j-w[i]]+//不选
dp[i-1][j-2*w[i]]+//选1个
dp[i-1][j-3*w[i]]+//选两个
...
将相同的部分替换,即可得到转移方程
dp[i][j]=dp[i-1][j]+dp[i][j-w[i]]
。
- 初始化:
dp[0][0]
表示从0个物品中选择物品凑成0的方法数,默认不选所以为1。 - 填表:两层循环,外层枚举物品,内层枚举要凑成的价值
j
,其中内层需要正向枚举。 - 最终答案:
dp[n][m]
。 - 空间优化:可进行空间优化和枚举范围优化。
参考程序:
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#include<bits/stdc++.h>
using namespace std;
void ac1() {
int n, m;
cin >> n >> m;
vector<long long>w(n + 1, 0);
vector<vector<long long> >dp(n + 1, vector<long long>(m + 1, 0));
for (int i = 1; i <= n; i++)
cin >> w[i];
dp[0][0] = 1ll;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= w[i])
dp[i][j] += dp[i][j - w[i]];
}
cout << dp[n][m];
}
void ac2() {
int n, m;
cin >> n >> m;
vector<long long>w(n + 1, 0);
vector<long long>dp(m + 1, 0);
for (int i = 1; i <= n; i++)
cin >> w[i];
dp[0] = 1ll;
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= m; j++) {
dp[j] += dp[j - w[i]];
}
cout << dp[m];
}
int main() {
//ac1();//朴素求解
ac2();//空间优化
return 0;
}
1293:买书是1273:【例9.17】货币系统的样例缩小版,因为只有4种价格的书。思路也是一样的,这里贴个参考程序就行。
#include<iostream>
#include<vector>
using namespace std;
void ac1(){
int n;
cin>>n;
int a[5]={0,10,20,50,100};
vector<int>dp(n+1,0);
dp[0]=1;
for(int i=1;i<=4;i++)
for(int j=1;j<=n;j++)
if(j>=a[i])
dp[j]=dp[j]+dp[j-a[i]];
cout<<dp[n];
}
int main() {
ac1();
return 0;
}
体积和价值不同数值
目前没有遇到类似的题目。所以这里仅作为个人的想法。
和01背包一样,同样需要一个朴素的完全背包问题进行辅助。
例如样例
5 5
2 2
5 10
3 4
2 1
1 2
最大价值是10,方案数是2。
参考程序:
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#include<bits/stdc++.h>
using namespace std;
void f1() {
int N, V;
cin >> N >> V;//物品数量、背包容量
vector<int>w(N + 1, 0), v(w);
for (int i = 1; i <= N; i++)
cin >> w[i] >> v[i];//体积、价值
//dp[i][j]:前i个物品任选,组成最大价值的方法数;bk[j]:最大价值
vector<vector<int> >dp(N+1, vector<int>(V + 1, 0)), bk(dp);
dp[0][0] = 1;
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
bk[i][j] = bk[i - 1][j];
dp[i][j] = dp[i - 1][j];
if (j < w[i]) continue;
if (bk[i][j] < bk[i][j - w[i]] + v[i]) {
bk[i][j] = bk[i][j - w[i]] + v[i];
dp[i][j] = dp[i][j - w[i]];
}
else if (bk[i][j] == bk[i][j - w[i]] + v[i])
dp[i][j] += dp[i][j - w[i]];
}
}
cout << bk[N][V] << ' ' << dp[N][V] << endl;
}
void f2() {
int N, V;
cin >> N >> V;//物品数量、背包容量
vector<int>w(N + 1, 0), v(w);
for (int i = 1; i <= N; i++)
cin >> w[i] >> v[i];//体积、价值
//dp[j]:组成最大价值的方法数;bk[j]:不超过j的最大价值
vector<int>dp(V + 1, 0), bk(dp);
dp[0] = 1;
for (int i = 1; i <= N; i++) {
for (int j = w[i]; j <= V; j++) {
if (bk[j] < bk[j - w[i]] + v[i]) {
bk[j] = bk[j - w[i]] + v[i];
dp[j] = dp[j - w[i]];
}
else if (bk[j] == bk[j - w[i]] + v[i])
dp[j] += dp[j - w[i]];
}
}
cout << bk[V] << ' ' << dp[V] << endl;
}
int main() {
//f1();
f2();//空间优化
return 0;
}
求完全背包的具体方案
同样是没遇到类似的题目,所以是个人想法。
在得到最大价值后,还可以通过回溯的方式记录选择了哪些物品。
以二维数组为例,在计算完dp
数组后,可以从包的容量V
开始,对于每一种物品i
,若
dp[i][j]==dp[i][j-w[i]]+v[i]
,说明该物品中的一件是包里物品的最优方案的一部分。此时可以取出物品V-=w[i]
,再继续看包内还有没有i物品。
如果j<w[i]
或dp[i][j]!=dp[i][j-w[i]]+v[i]
,则这件物品没了,换i+1
物品。
这个过程和01背包的思路很像,都是回溯,不同的是同一件物品可能有多个,所以需要用循环来判断。
参考程序:
#include <iostream>
using namespace std;
int m, n;
int dp[1001][1001] = { 0 };
int w[1001] = { 0 }, v[1001] = { 0 };
int maxx = 0;
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> w[i] >> v[i];
//枚举到第几号物品 (01背包是第几件),因为可放数量不固定。
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {//01背包是逆向
if (j >= w[i])
dp[i][j] = max(dp[i - 1][j],
dp[i][j - w[i]] + v[i]);
else
dp[i][j] = dp[i - 1][j];
}
}
cout << dp[n][m] << endl;
//回溯求具体方案
int j = m;
for (int i = n; i >= 1; i--) {
int flag = 0;
//和01背包不同的点是这个因为会装很多件相同的,
//所以用循环去判断,01背包是用if
while (j >= w[i] && dp[i][j] == dp[i][j - w[i]] + v[i]) {
cout << i << ' ';
j -= w[i];
flag = 1;
}
if (flag == 1)
cout << endl;
}
return 0;
}
完全背包的变种
Buying Hay S(限定条件变更)
[P2918 USACO08NOV] Buying Hay S - 洛谷
分析出属于完全背包的要素:
重量==
价值,开销==
体积,但要求的是最小开销,也就是价值是负面效果。
- 状态定义
dp[i][j]
表示从[1,i]
中挑选,总重量至少为j
时的最小开销。
- 转移方程
完全背包的3种限定情况:
- 体积不超过
j
,说明在前i
个物品中挑选,总重小于等于j
,所以 j ≥ v i j\geq v_i j≥vi。所以用第2个状态时需要做判断,或直接 j < v i j<v_i j<vi砍掉这部分枚举。- 体积正好为
j
,说明前i
个物品中挑出来的总重要等于j
,此时需要判断dp[i][j]
中某些状态是否合法(例如dp[0][i]
),不合法的情况可以初始化为-1
、负无穷和正无穷,防止这些不合法的状态影响判断。- 体积至少为
j
,说明挑出来的物品的体积大于等于j
,也就是说 j − w i < 0 j-w_i<0 j−wi<0也是合法情况。
例如这里的买干草,假设[1,i]
公司中,第i
个公司的草的单位重量刚好大于j
,则最优解就是第i
个公司的草。
但问题是dp表,也就是数组不能用负数表示下标,所以根据问题求的最小开销,将dp[i][0]
记录j<w[i]
时的情况。所以处理这种合法的状态但无法用dp表存储的情况,需要进行特殊判断例如max(0,j-w[i])
放在0这个特殊位置,要么另外开一个数组或变量存储。
完全背包问题,根据最后一种情况划分位置,所有的决策:
dp[i][j]=dp[i-1][j],
dp[i][j]=dp[i-1][max(0,j-w[i])]+v[i],
...
完全背包可以只保留地1个,其他的用一个式子表示。
所以最终转移方程:
dp[i][j]=min(dp[i-1][j],dp[i][max(0,j-w[i])]+v[i]);
- 初始化&填表顺序
因为是求最小开销,所以不能初始化为0,因为开销再小也不为负数。
将非法状态初始化为正无穷,除了dp[0][0]
,它有特殊含义。
填表时和朴素完全背包一致。
参考程序:
#include<bits/stdc++.h>
using namespace std;
void ac1() {
int n, h;
cin >> n >> h;
vector<int>p(n + 1, 0), c(p);
vector<vector<int> >dp(n + 1, vector<int>(h + 1, 0x3f3f3f3f));
for (int i = 1; i <= n; i++)
cin >> p[i] >> c[i];
dp[0][0]=0;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= h; j++)
dp[i][j] = min(dp[i - 1][j], dp[i][max(0, j - p[i])] + c[i]);
cout << dp[n][h];
}
void ac2() {
int n, h;
cin >> n >> h;
vector<int>p(n + 1, 0), c(p);
vector<int>dp(h + 1, 0x3f3f3f3f);
for (int i = 1; i <= n; i++)
cin >> p[i] >> c[i];
dp[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= h; j++)
dp[j] = min(dp[j], dp[max(0, j - p[i])] + c[i]);
cout << dp[h];
}
int main() {
//ac1();//二维状态
ac2();//空间优化
return 0;
}
纪念品(股票问题 贪心+dp)
[P5662 CSP-J2019] 纪念品 - 洛谷
这个题源自于生活中的股票,简单来说就是囤积商品后,在未来看市场价格选择将囤积的商品卖出去,用于赚取利润。这便是股票问题,通过买卖操作来赚钱。
股票问题中重要的结论:在股票问题中,任意一笔跨天的交易,都可以转换为连续的某天买,隔天卖的形式。这可以看成一种相对成熟的贪心策略,以后的(类)股票问题都可以参考这个思路。
因为题目有一个某个纪念品允许当天买当天卖的不赚钱的操作,所以跨天的交易可以拆解成连续几天的交易。在后面的解题中可以进行贪心策略:只需要考虑连续两天的交易即可,比如第1天买进,第2天是否卖出即可。否则第1天买,后面还要选时间卖,算法很难设计。
虽然题目给的样例和这里的贪心策略不一样,但题目可以转换成这个谈心策略。
这里分析的都是1个纪念品的情况,若是多个纪念品,每个纪念品在每天都有不同的价格,利用贪心策略,第1天选择买,第2天选择卖,同理第2天买、第3天卖的操作,第3天买、第4卖天的操作…第i
天买、第i+1
天买。
研究任意两天,第1天买纪念品的时候不能超过当天拥有的金币数,在这基础上可以无限购买,第2天卖出的物品还能赚取利润,相当于每个物品的价值是利润,可以发现,这两天的交易就是一个完全背包问题。
所以整个问题被拆分成了T-1
次完全背包问题,但价值是两天的差价,每次完全背包的答案是之前的本金加背包问题求得的利润。
这里省略完全背包的分析过程。参考程序:
#include<bits/stdc++.h>
using namespace std;
void ac1() {
int T, N, M;
cin >> T >> N >> M;
vector<vector<int> >p(T + 1, vector<int>(N + 1, 0));
for (int i = 1; i <= T; i++)
for (int j = 1; j <= N; j++)
cin >> p[i][j];
//完全背包
auto backdp = [&](vector<int>& v1, vector<int>& v2, int m) {
vector<int>dp(M + 1, 0);
for (int i = 1; i <= N; i++)
for (int j = v1[i]; j <= m; j++)
dp[j] = max(dp[j], dp[j - v1[i]] + v2[i] - v1[i]);
return dp[m]+m;
};
//贪心
for (int i = 1; i < T; i++)
M = backdp(p[i], p[i + 1], M);
cout << M;
}
int main() {
ac1();
return 0;
}
完全背包的OJ汇总
- 模板题
1268:【例9.12】完全背包问题
3. 完全背包问题 - AcWing题库
P1616 疯狂的采药 - 洛谷
【模板】完全背包
- 求完全背包的方案数
1273:【例9.17】货币系统
1293:买书
- 完全背包的变种
[P2918 USACO08NOV] Buying Hay S - 洛谷
[P5662 CSP-J2019] 纪念品 - 洛谷