背包问题汇总

0-1背包

题目:https://www.acwing.com/problem/content/2/

分析:

$$
dp[i][j]意思是只放前i个物品在总体积是j的情况下的物品的贡献
$$

  • 易得状态方程为:

$$
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]+w[i]])
$$

$$
dp[i][j]=dp[i-1][j]意思是不选第i个物品(dp[i][j]直接原封不动保留上一个i的状态)
$$

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <vector>
#include <cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
int n,m,v[1005],w[1005],dp[1005][1005];
int main() {
cin>>n>>m;
for (int i = 1; i <=n ; ++i) {
cin>>v[i]>>w[i];
}
for (int i = 1; i <=n; ++i) {
for (int j =0; j <=m ; ++j) {
dp[i][j] = dp[i - 1][j];//不放
if (j >= v[i]) //要当前背包的总体积大于v[i]才能把i物品放进背包
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
}
// for (int i = 1; i <=n ; ++i) {
// for (int j = 0; j <=m ; ++j) {
// cout<<dp[i][j]<<' ';
// }
// cout<<endl;
// }
int res=0;
for (int i = 0; i <=m ; ++i) {
res=max(res,dp[n][i]); //dp[n]意思是将n个物品都权衡利弊后选择n个物品中的部分放入背包(不超过背包最大容量),
} //但不知道这些物品的总体积是多少,所以遍历所有可能的体积输出价值最大值即为答案。
cout<<res<<endl;

}

  • 测试数据:

4 5
1 2
2 4
3 4
4 5

  • dp数组内容:

0 2 2 2 2 2
0 2 4 6 6 6
0 2 4 6 6 8
0 2 4 6 6 8

  • 最后输出:8

优化后的代码:

==只用了一维==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <vector>
#include <cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
int n,m,v[1005],w[1005],dp[1005];
int main() {
cin>>n>>m;
for (int i = 1; i <=n ; ++i) {
cin>>v[i]>>w[i];
}
for (int i = 1; i <=n; ++i) {
for (int j =m; j >=v[i] ; --j) { //j从大到小循环是为了让dp[j-v[i]]的值是上一层i的值
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);//(dp[j-v[i]]后于dp[j]更新)
//cout<<dp[j]<<' ';
}
//cout<<endl;
}
cout<<dp[m];
}

  • dp数组内容:

2 2 2 2 2
6 6 6 4
8 6 6
8 6
8

==注:若要求达到最大价值时的最大体积(恰好)需将初始化dp[N]={0}改为dp[0]=0,剩余dp为-INF; ans从直接输出dp[m]变为max(dp[0…m]).==

状态方程的详细求解过程:

acwing算法笔记本-2

二维优化成一维的推导:

IMG_988F8EF4F15E-1

完全背包

题目:https://www.acwing.com/problem/content/3/

$dp[i][j]$同样也是表示从前i个物品中选,总体积不超过j的方案集合,唯一的区别就是完全背包问题中每个物品都可以取无限个

所以可得到:
$$
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i],dp[i-1][j-2v[i]]+2w[i],…)​
$$
按道理来说需要三层循环,但是可以被优化成二层循环:
$$
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i])​
$$

具体证明:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n,m,dp[1005][1005];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n,m,v[1005],w[1005];
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
dp[i][j]=dp[i-1][j];
if(j>=v[i])dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
}
}
cout<<dp[n][m];
}

二维优化成一维:

$$
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i])​\to dp[j]=max(dp[j],dp[j-v[i]]+w[i])
$$

与0-1背包不同的是,0-1背包的$dp[j-v[i]]$对应的是i上一层也就是i-1层的值,而这里的$dp[i][j-v[i]]$要求对应本层i的值,也就是让$dp[j-v[i]]$先更新再更新$dp[j]$,所以把j从大到小循环改为从小到大循环即可。

优化后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n,m,dp[1005];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n,m,v[1005],w[1005];
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){
for(int j=v[i];j<=m;j++){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[m];
}

多重背包

题目(数据范围小):https://www.acwing.com/problem/content/4/

朴素做法(三维):

多一维遍历第i个物品选$0~s[i]$个的所有情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;
int main() {
ios::sync_with_stdio(false);
int n,m;
cin>>n>>m;
int v[105],w[105],s[105],dp[105]={0};
//memset(dp,0,105);
for (int i = 1; i <=n ; ++i) {
cin>>v[i]>>w[i]>>s[i];
}
for (int i = 1; i <=n ; ++i) {
for (int j = m; j >=v[i] ; --j) {
//cout<<dp[j]<<' ';
for (int k = 1; k <=s[i] ; ++k) {
if(j>=k*v[i])dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
}
}
//cout<<'\n';
}
cout<<dp[m]<<endl;
// for (int i = 0; i <=m ; ++i) {
// cout<<dp[i]<<' ';
// }
}

题目(数据范围大):https://www.acwing.com/problem/content/5/

思考到多重背包问题可以将每个物品拆开然后重新存入数组里,但如果直接拆开根据数据范围$sNV=200010002000=2*10^9$

还是会超过时间限制,所以考虑到将物品通过二进制的方法拆解:假如某物品有7件用1,2,4就可以表示全部可能。所以得出s件物品只需要$\lceil log(s) \rceil$的时间,所以就是$1110002000\approx2*10^7$,可以在规定时间内通过。但在具体实现中还会有问题,假如某物品有10件,不能直接用1,2,4,8表示,因为1,2,4,8可以表示1~15,而11~15不是我们所需要的。所以采取一个策略:用s减去1,2,4…当s<0的时候停止,并将最后剩余的那个数存进数组。假如10,$10-1-2-4=3$,所以就将1,2,4,3存入数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <vector>

using namespace std;
struct struc {
int v, w;
};
int v[2005], w[2005], s[2005], dp[2005];
vector<struc> vec; //用结构体矢量存拆开的物品的数据

int main() {
ios::sync_with_stdio(false);
int n, m;
cin >> n >> m;
//memset(dp,0,105);
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> w[i] >> s[i];
}
for (int i = 1; i <= n; ++i) {
for (int k = 1; k <= s[i]; k *= 2) {
s[i] -= k;
vec.push_back({k * v[i], k * w[i]});
//cout<<k<<' '<<"s[i]"<<s[i]<<' ';
}
//cout<<endl;
if (s[i] > 0)vec.push_back({s[i] * v[i], s[i] * w[i]}); //将剩下的那个数存进矢量
}
for (int i = 0; i < vec.size(); ++i) {
for (int j = m; j >= vec[i].v; --j) {
dp[j] = max(dp[j], dp[j - vec[i].v] + vec[i].w);
}
}
cout << dp[m] << endl;
}

分组背包

题目:https://www.acwing.com/problem/content/9/

解析:701A36AD-E016-4371-A8B8-E8A9C82BC085_1_105_c

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
#define pb push_back
using namespace std;
typedef long long ll;
int n,m,dp[105],s[105],v[105][105],w[105][105];
int main() {
cin>>n>>m;
for (int i = 1; i <=n ; ++i) {
cin>>s[i];
for (int j = 1; j <=s[i] ; ++j) {
cin>>v[i][j]>>w[i][j];
}
}
for (int i = 1; i <=n ; ++i) {
for (int j = m; j >=0 ; --j) {
for (int k = 1; k <=s[i] ; ++k) {
if(v[i][k]<=j)dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]); //遍历第i组物品中的所有可能
}
}
}
cout<<dp[m];
}