01背包问题
问题描述:
有N件物品和一个容量为W的背包。第i件物品的费用(即体积,下同)是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
思路:
定义
d
p
[
i
+
1
]
[
j
]
dp[i+1][j]
dp[i+1][j]从0到i+1个物品中选出总重量不超过j的物品时总价值的最大值。
d
p
[
0
]
[
j
]
=
0
dp[0][j]=0
dp[0][j]=0。
递推关系式:
d
p
[
i
+
1
]
[
j
]
=
{
d
p
[
i
]
[
j
]
j
<
w
[
i
]
m
a
x
(
d
p
[
i
]
[
j
]
,
d
p
[
i
]
[
j
−
w
[
i
]
]
+
v
[
i
]
)
其他
dp[i+1][j]= \begin{cases} dp[i][j] & \text{$j<w[i]$ }\\ max(dp[i][j],dp[i][j-w[i]]+v[i]) & \text{其他} \end{cases}
dp[i+1][j]={dp[i][j]max(dp[i][j],dp[i][j−w[i]]+v[i])j<w[i] 其他
将动态规划问题视作填表过程:
(图与本题无关,摘自其他博主,仅作示例)
代码:
int dp[MAX_N+1][MAX_W+1];
void solve(){
for(int i=0;i<n;i++){
for(int j=0;j<=W;j++){
if(j<w[i])
dp[i+1][j]=dp[i][j];
else
dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]);
}
}
cout<<dp[n][W];
}
但如果背包问题的规模扩大,例如当
1
⩽
n
⩽
100
1\leqslant n\leqslant 100
1⩽n⩽100,
1
⩽
W
⩽
1
0
9
1 \leqslant W \leqslant 10^9
1⩽W⩽109时,则上述的动态规划由于规模限制而无法进行。
可以更换思路,之前是针对不同重量计算最大价值,可以更改为,针对不同价值计算最小重量。
定义dp[i+1][j]:前i个物品挑选出价值总和为j时总重量最小值(不存在时就令为充分大的数INF)
初始值为:
d
p
[
0
]
[
0
]
=
0
d
p
[
0
]
[
j
]
=
I
N
F
dp[0][0]=0\\ dp[0][j]=INF
dp[0][0]=0dp[0][j]=INF
代码:
int dp[MAX_N+1][MAX_N*MAX_V+1];
void solve(){
fill(dp[0],dp[0]+MAX_N*MAX*V+1,INF);
dp[0][0]=0;
for(int i=0;i<n;i++){
for(int j=0;j<=MAX_N*MAX_V;j++){
if(j<v[i])
dp[i+1][j]=dp[i][j];
else
dp[i+1][j]=min(dp[i][j],dp[i][j-v[i]]+w[i]);
}
}
int res=0;
for(int i=0;i<MAX_N*MAX_V;i++)if(dp[n][i]<=W)res=i;
cout<<res;
}
法二:
通过重复利用一个数组:
只要我们在求 d p [ j ] dp[ j ] dp[j]时不覆盖 d p [ j − w [ i ] ] dp[ j - w[i] ] dp[j−w[i]],那么就可以不断递推至所求答案。所以我们采取倒序循环.
int dp[MAX_W+1];
void solve(){
for(int i=0;i<n;i++){
for(int j=W;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[W];
}
完全背包问题
思路:
因为同一种类的物品可以选择任意多件,所以相当于在内层增加了一个件数的循坏,寻找最优的件数选择。
但是三重循环的复杂度太高,因此进行改进,利用递推的特性,将循环部分替代成
d
p
[
i
+
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
dp[i+1][j-w[i]]+v[i]
dp[i+1][j−w[i]]+v[i],因为从
d
p
[
i
+
1
]
[
j
]
dp[i+1][j]
dp[i+1][j]中选择k个的计算,
k
⩾
1
k\geqslant 1
k⩾1的情况在
d
p
[
i
+
1
]
[
j
−
w
[
i
]
]
dp[i+1][j-w[i]]
dp[i+1][j−w[i]]中已经计算完成了。
递推关系式:
d
p
[
0
]
[
j
]
=
0
d
p
[
i
+
1
]
[
j
]
=
m
a
x
{
d
p
[
i
]
[
j
−
k
×
w
[
i
]
]
+
k
×
v
[
i
]
∣
0
≤
k
}
dp[0][j]=0\\ dp[i+1][j]=max\{dp[i][j-k\times w[i]]+k\times v[i]|0\leq k\}
dp[0][j]=0dp[i+1][j]=max{dp[i][j−k×w[i]]+k×v[i]∣0≤k}
优化后的递推关系式:
d
p
[
i
+
1
]
[
j
]
=
m
a
x
{
d
p
[
i
]
[
j
−
k
×
w
[
i
]
]
+
k
×
v
[
i
]
∣
0
≤
k
}
⟺
m
a
x
(
d
p
[
i
]
[
j
]
,
d
p
[
i
+
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
)
dp[i+1][j]=max\{dp[i][j-k\times w[i]]+k\times v[i]|0\leq k\} \Longleftrightarrow \\ max(dp[i][j],dp[i+1][j-w[i]]+v[i])
dp[i+1][j]=max{dp[i][j−k×w[i]]+k×v[i]∣0≤k}⟺max(dp[i][j],dp[i+1][j−w[i]]+v[i])
代码:
注意:跟01背包非常相似。
void solve(){
for(int i=0;i<n;i++){
for(int j=0;j<=W;j++){
if(j<w[i])
dp[i+1][j]=dp[i][j];
else
dp[i+1][j]=max(dp[i][j],dp[i+1][j-w[i]]+v[i]);
}
}
cout<<dp[n][W];
}
法二:
同样利用一维数组
int dp[MAX_W+1];
void solve(){
for(int i=0;i<n;i++){
for(int j=w[i];j<=W;j++){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[W];
}
变式题1
oxx 和 xjj 决定和小伙伴们一同坐船前往 Xiamen。去 Xiamen 的船票一张 p 元。
当他们满怀兴致地来到港口时发现居然只有不设找零的自动售票机,只能使用一元,五元,十元,二十元,五十元,一百元的纸币,且一次至多买 k 张船票。因此他们不得不去银行取钱。而 oxx 是个大懒人,他希望取的纸币数量越少越好,因此他想知道他们一行 n 人要都买到票至少需要取多少张纸币。
输入格式
第一行三个整数 n,k,p (1≤n≤103,1≤k≤10,1≤p≤103) 分别表示 oxx 需要购买船票张数,一次至多买船票数量,单张船票价格。
输出格式
输出一个整数,表示 oxx 至少要取多少张纸币。
思路:
可以看成完全背包问题,不同的物品就是每次购买的船票从1~k所需要的钱数。
#include <algorithm>
#include <iostream>
using namespace std;
int dp[1111], n, p, k;
int mod[6] = {100, 50, 20, 10, 5, 1};
int item[11];
int main() {
cin >> n >> k >> p;
for (int i = 1; i <= k; i++) {
int money = i * p;
for (int j = 0; j <= 5; j++) {
item[i] += money / mod[j];
money %= mod[j];
}
}
for (int i = 1; i <= n; i++)
dp[i] = 11111111;
for (int i = 1; i <= k; i++) {
for (int j = i; j <= n; j++)
dp[j] = min(dp[j - i] + item[i], dp[j]);
}
cout << dp[n];
return 0;
}
多重背包问题
问题描述:
有N件物品和一个容量为W的背包。第i件物品的费用(即体积,下同)是w[i],价值是v[i],最多可选m[i]个,求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
朴素想法:
设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]为到第i个物品为止总重量不超过j的所有选法中最大可能的价值。
递推关系式:(复杂度
O
(
N
W
m
)
O(NWm)
O(NWm))
d
p
[
0
]
[
j
]
=
0
d
p
[
i
+
1
]
[
j
]
=
m
a
x
{
d
p
[
i
]
[
j
−
k
×
w
[
i
]
]
+
k
×
v
[
i
]
∣
0
≤
k
≤
m
[
i
]
}
dp[0][j]=0\\ dp[i+1][j]=max\{dp[i][j-k\times w[i]]+k\times v[i]|0\leq k\leq m[i]\}
dp[0][j]=0dp[i+1][j]=max{dp[i][j−k×w[i]]+k×v[i]∣0≤k≤m[i]}
代码:
(进行了优化)
将原本的
m
i
m_i
mi个物品转化为用
2
k
2^k
2k表示的k+2个物品(
m
i
=
1
+
2
+
4
+
.
.
.
+
2
k
+
a
m_i=1+2+4+...+2^k+a
mi=1+2+4+...+2k+a),然后看作普通的01背包DP。
vector<int> weight;
vector<double> value;
for (int i = 0; i < n; i++)
{
int w,v,num;
cin >> w >> v >> num;
for (int j = 1; j <= sum; j <<= 1)
{
weight.push_back(w * j);
value.push_back(v * j);
sum -= j;
}
weight.push_back(w * sum);
value.push_back(v * sum);
}
void solve() {
int dp[W + 1];
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= N; i++) {
int num = m[i];
for (int k = 1; num > 0; k <<= 1) {
int mul = min(k, num);
for (int j = W; j >= w[i] * mul; j--) {
dp[j] = max(dp[j], dp[j - w[i] * mul] + v[i] * mul);
}
num -= mul;
}
}
cout << dp[W];
}
变式题1
题目
蒜头君酷爱收集萌萌的娃娃。蒜头君收集了 6种不同的娃娃,第 i 种娃娃的萌值为 i(1≤i≤6)。现在已知每种娃娃的数量 mi,蒜头君想知道,能不能把娃娃分成两组,使得每组的娃娃萌值之和相同。
输入格式
输入一行,输入 6 个整数,代表每种娃娃的数量 mi(0≤mi≤20,000)。
输出格式
输出一行。如果能把所有娃娃分成萌值之和相同的两组,请输出Can be divided.,否则输出Can’t be divided.。
分析
核心思想:01背包+二进制思想。把n个价值为i的物品划分成log2n个价值为i_1,i_2…的商品,然后再用01背包。
代码
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int dp[1000000];
int m[7];
int val[200];
int main()
{
int sum=0,flag=1;
memset(dp,0,sizeof(dp));
//对m[i]个物品进行划分1,2,4,8....2的k次方,n-(1+2+..2的k次方)
for(int i=1;i<=6;i++){
cin>>m[i];
sum+=i*m[i];
for(int k=1;k<=m[i];k*=2){
val[flag++]=k*i;
m[i]-=k;
}
if(m[i]>0){
val[flag++]=m[i]*i;
}
}
//val是新的价值,flag-1是物品数
if(sum%2==0){//01背包问题
sum/=2;
for(int i=1;i<flag;i++){//外层是物品的依次递增
for(int j=sum;j>=val[i];j--){//限制条件是萌值不超过总和的一半,
dp[j]=max(dp[j],dp[j-val[i]]+val[i]);
}
}
if(dp[sum]==sum)
cout<<"Can be divided."<<endl;
else
cout<<"Can't be divided."<<endl;
}
else
cout<<"Can't be divided."<<endl;
return 0;
}
分数背包问题
与01背包条件类似,但可以取分数件物品。
贪心:
求出单位质量的物品价值,从高到低取。
#include <algorithm>
#include <iostream>
using namespace std;
double W;
int n;
struct goods {
double w, v;
double argv;
} good[111];
bool cmp(goods a, goods b) { return a.argv > b.argv; }
int main() {
cin >> n >> W;
for (int i = 0; i < n; i++) {
cin >> good[i].w >> good[i].v;
good[i].argv = good[i].v / good[i].w;
}
sort(good, good + n, cmp);
double ans = 0;
int index = 0;
while (W > 0 && index < n) {
if (W > good[index].w) {
ans += good[index].v;
W -= good[index].w;
} else {
ans += good[index].argv * W;
W = 0;
}
index++;
}
printf("%.6lf", ans);
return 0;
}
DP:
(无优化版本)类似于完全背包的dp
#include <cstring>
#include <iostream>
using namespace std;
int n;
double W;
double w[111], v[111], s[111];
double dp[111][1000];
int main() {
cin >> n >> W;
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
s[i] = v[i] / w[i];
}
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
for (int k = 0; k <= w[i]; k++) {
if (j >= k)
dp[i][j] = max(dp[i][j], dp[i - 1][j - k] + k * s[i]);
}
}
}
printf("%.6lf\n", dp[n][(int)W]);
return 0;
}
优化版本:类似于多重背包的优化。优化后作为01背包处理。
for (int i = 0; i < n; i++) {
int w, v;
cin >> w >> v;
double argv = (double)v / w;
for (int j = 1; j <= w; j <<= 1) {
w[cnt] = j;
v[cnt++] = argv * j;
w -= j;
}
w[cnt] = w;
v[cnt++] = argv * w;
}