1.01背包
状态f[i][j]定义:前 i 个物品,背包容量 j 下的最优解(最大价值),据此定义可以理解为从初始状态
f[0][0] = 0开始决策,每个状态都有拿与不拿
可以得转移方程:f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define debug(a) cout << (#a) << " = " << (a) << '\n';
//wake up.
int n, m;
const int zq = 1e3 + 10;
int f[zq][zq];
int w[zq], v[zq];
void solve(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> v[i] >> w[i];
}
f[0][0] = 0;
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= m; j ++){
f[i][j] = f[i - 1][j];
if(j >= v[i]){
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
}
}
cout << f[n][m] << '\n';
// for(int i = 1; i <= m; i ++){
// cout << f[n][i] << " ";
// }
}
int32_t main(){
ios_base::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t = 1;
// cin >> t;
while(t --){
solve();
}
// system("pause");
return 0;
}
二维优化至一维:
根据f[i][j]我们可以求得在任意状态下合法的i与j对应的最优解,但是题目只要求f[n][m],一维优化至状态为f[j]:n件物品,容量为j时的最优解。
优化至一维,则需要逆序遍历,因为在二维状态下,f[i - 1][j] 与 f[i][j]是相互独立的,但是如果优化至一维,如果还是正序的话,则有可能本应该用第i - 1轮的状态却用的是第i轮的状态。
可以理解为:一维情况正序更新状态f[j]需要用到前面计算的状态已经被更新过了,逆序则不会有这样的问题。
可以得到状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i]);
并且根据上式可以知道,只有当j > v[i]的时候才会更新状态,所以我们可以修改循环的条件。
空间优化后的代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define debug(a) cout << (#a) << " = " << (a) << '\n';
//wake up.
int n, m;
const int zq = 1e3 + 10;
int f[zq];
int w[zq], v[zq];
void solve(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> v[i] >> w[i];
}
f[0] = 0;
for(int i = 1; i <= n; i ++){
for(int j = m; j >= v[i]; j --){ //逆序,并且当j >= v[i]的时候才会循环
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << '\n';
}
int32_t main(){
ios_base::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t = 1;
// cin >> t;
while(t --){
solve();
}
// system("pause");
return 0;
}
2.完全背包问题
完全背包与01背包的不同点就是每一个物品可以拿无限个。
所以由01背包可以推出递推式子
如果根据上式写三层循环的话肯定会超时,但是我们可以观察上下两个式子,
不难得出 f[i][j] = max(f[i - 1][j], f[i][j - v[i] + w[i]);
根据转移方程可以知道第三层循环已经被优化掉了,很容易写出代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int zq = 1e3 + 10;
int w[zq], v[zq];
int f[zq][zq];
int n, m;
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 = 1; j <= m; j ++){
f[i][j] = f[i - 1][j];
if(j >= v[i]){
f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
}
}
}
cout << f[n][m] << '\n';
return 0;
}
下面考虑空间优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int zq = 1e3 + 10;
int w[zq], v[zq];
int f[zq];
int n, m;
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 = v[i]; j <= m; j ++){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << '\n';
return 0;
}
可以发现完全背包问题与01背包不同的是它需要从前往后遍历,这是因为完全背包需要用到第i-1行和第i行的数据。但是如果按照01背包从后往前遍历的思路的话,由于f[i][j-w[i]]要用到第i行前面的数据,但前面的数据在j从后往前便利的时候是没有更新的,所以这是行不通的,完全背包需要从前往后遍历。
3.多重背包
多重背包与前面两个不同的是,每个物品的数量都是给定的。
当数据范围很小的时候,就可以根据完全背包的暴力做法来做,就是写三层循环,遍历并且不断更新最大值。代码如下
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int zq = 110;
int f[zq][zq];
int v[zq], w[zq], s[zq];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> v[i] >> w[i] >> s[i];
}
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= m; j ++){
for(int k = 0; k <= s[i] && k * v[i] <= j; k ++){
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);//这里在k = 0的时候就是f[i][j] = f[i - 1][j]
}
}
}
cout << f[n][m] << '\n';
return 0;
}
当数据范围变大之后,我们就需要效率更高的方法来解决这个问题。
①既然每件物品有固定的个数的话,我们可以把每件物品都加到给定的集合里面,就是如果第i个物品有j件,我们就把每一件都分别加到元素集合里面。这样就可以变为01背包问题,但是这样做的话在一些情况下,时间复杂度仍旧是非常高的,这时我们就需要用到
二进制优化
我们首先可以知道,任意一个整数都可以由二进制数来表示,并且这里的多重背包问题问的就是每件物品取多少个可以获得最大价值。
根据大佬的苹果,我们可知二进制优化思维就是:现在给出一堆苹果和10个箱子,选出n个苹果。将这一堆苹果分别按照1,2,4,8,16,.....512分到10个箱子里,那么由于任何一个数字x∈[0,1023](第11个箱子才能取到1024)都可以从这10个箱子里的苹果数量表示出来,但是这样选择的次数就是≤10次 。
但是如果一个一个选的话,那么选择的次数就是n次。
由此可知二进制优化可以大幅度减少代码的复杂度。
那么如何来确定二进制数,以至于能表示1 ~ s所有的数呢?
for(int k = 1; k <= s; k *= 2)
s -= k;
在此之后,如果s > 0 的话,我们只需把之后的s加进去,就可以使得确定的数来表示1 ~ s所有的数。
优化之后其实和01背包的思路差不多。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int zq = 2e3 + 10;
int f[zq];
int n, m;
signed main(){
cin >> n >> m;
vector<pair<int,int>>g;
for(int i = 0; i < n; i ++){
int v, w ,s;
cin >> v >> w >> s;
for(int k = 1; k <= s; k *= 2){
s -= k;
g.push_back(make_pair(v * k, w * k));
}
if(s > 0){
g.push_back(make_pair(v * s, w * s));
}
}
for(auto z : g){
for(int j = m; j >= z.first; j --){
f[j] = max(f[j], f[j - z.first] + z.second);
}
}
cout << f[m] << '\n';
return 0;
}
9.分组背包问题
每一组的物品只能选一件。
我们可以分别对每一组的物品进行决策。
我们把每组看做一个物品,而这个物品我们可以选择第0到si个;
通过从后向前的遍历顺序来确保,我们对组的决策只有一种:要么选这个组,要么不选;
然后在通过枚举组内的情况,来对组内进行决策:要么选0个,选1个.....;
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int zq = 1e3 + 10;
int v[zq], w[zq];
int n, m;
int f[zq];
int s;
int main(){
cin >> n >> m;
for(int i = 0; i < n; i ++){
cin >> s;
for(int j = 0; j < s; j ++){
cin >> v[j] >> w[j];
}
for(int j = m; j >= 0; j --){
for(int k = 0; k < s; k ++){
if(j >= v[k]){
f[j] = max(f[j], f[j - v[k]] + w[k]);
}
}
}
}
cout << f[m] << '\n';
return 0;
}