算法课的小作业,写了挺多就发掉吧,应该写的挺详细的(。
浅谈背包问题
定义:背包问题是运筹学中的一个经典的优化难题,是一个 NP-完全问题,但其有 着广泛的实际应用背景,是从生活中一个常见的问题出发展开的: 一个背包,和很多件物品,要在背包中放一些物品,以达到一定的目标。
(摘自《2008年国家集训队论文——浅谈几类背包问题(徐持衡)》)
0-1背包
0-1 背包问题:给定 n 件物品和一个背包。物品 i 的价值是 Wi ,其体积为 Vi,背包 的容量为 C。可以任意选择装入背包中的物品,求装入背包中物品的最大总价值。
在选择装入背包的物品时,对每件物品 i ,要么装入背包,要么不装入背包。 不能将物品 i 多次装入背包,也不能只装入部分物品 i (分割物品 i)。因此,该问题称为 0-1 背包问题。
设 DP 状态dp[i] [j]为在只能放前i个物品的情况下,容量为j的背包所能达到的最大总价值。
考虑转移。假设当前已经处理好了前i-1个物品的所有状态,那么对于第i个物品,当其不放入背包时,背包的剩余容量不变,背包中物品的总价值也不变,故这种情况的最大价值为dp[i-1] [j];当其放入背包时,背包的剩余容量会减小Wi,背包中物品的总价值会增大Vi,故这种情况的最大价值为dp[i-1] [j-Wi]+Vi。
由此可以得出状态转移方程:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
但是我们注意到这里如果直接采用二维数组对状态进行记录,空间上会有很多浪费,因为我要计算当前的状态的话只需要只要上一个状态即可,没有必要把所有的状态都保存,这样可能会出现MLE的情况。因此我们可以考虑改用滚动数组的形式来优化。
dp[i%2][j]=max(dp[(i-1)%2][j],dp[(i-1)%2][j-w[i]]+v[i]);
但是这样就够了吗?我们还想继续优化,能不能只用一维进行处理?我们注意到对于某一个状态来说我只需要上一个状态中体积比当前体积-w[i]小的那些状态,因此我们考虑对于当前的这个物品,我先更新大体积的时候的状态,这样就保证了那些将来会被用于转移的小体积的状态的正确性。
所以该转移方程缩小到一维就是:
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
完整核心代码:
for (int i = 1; i <= n; i++){
for (int j = c; j >= w[i]; j--){
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
复杂度分析:对于每一个物品我们都要看它能不能转移上一次的每一个背包的状态,因此复杂度是O(nm)(n是物品个数,m是背包容量)
然而现在我们又会注意到一个问题,如果这个背包容量特别大的话怎么办?假设现在我这个背包的容量是1e9,一维数组都开不下怎么办(超大背包问题)?
首先,如果n<50,那么我们不用dp,直接整体二分双向爆搜然后合并答案即可。
如果n<100呢?整体二分也会t,那我们只能去dp了,注意到我们保存的状态中其实有很多是没有作用的,比如当dp[2] [4]=10,dp[2] [5]=8时,花费5的重量得到的价值反而比花费4的重量的小,那么此时我这个dp[2] [5]就是一个无效状态,完全没有必要保存,因为我能从dp[2] [5]转移过去的一定不如从dp[2] [4]转移过去。不仅如此,如果我们把状态定的严格一点:dp[i] [j]代表考虑前i个物品且刚好重量为j的情况,因为如果找我前面的写法我的状态中其实是隐含了一些重量任意,价值为0的物品用来凑状态的(因为这样好写),但如果按照这个严格定义的话,状态中就会存在一些你根本凑不出来的重量,那么这样的状态也是没有必要保存下来的,所以我们考虑用map把重量这个维度离散化,然后维护这个离散后的状态的单调性,保证j增加时val一定递增即可。
但是数据再开放一些的话就成了一个np问题。。。
例题:[USACO07DEC]Charm Bracelet S - 洛谷
ac代码:
//https://www.luogu.com.cn/problem/P2871
#include <bits/stdc++.h>
using namespace std;
#define visit _visit
#define next _next
#define pb push_back
#define fi first
#define se second
#define endl '\n'
#define fast ios::sync_with_stdio(0), cin.tie(0)
#define int long long
#define ll long long
#define pint pair<int,int>
const int mod = 998244353;
const int maxn = 200001;
const int INF = 1e18;
void read(int &x){
int f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
ll quick_pow(ll a,ll b) {ll res=1;a%=mod; assert(b>=0); for(;b;b>>=1){if(b&1)res=res*a%mod;a=a*a%mod;}return res;}
ll inv(ll x) {return quick_pow(x, mod-2);}
//----------------------------------------------------------------------------------------------------------------------//
int dp[maxn];
int v[maxn],w[maxn];
void solve(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i];
}
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[m]<<endl;
}
signed main(){
fast;
int t=1;
//cin>>t;
while(t--){
solve();
}
return 0;
}
完全背包
完全背包问题:与0-1背包问题类似,唯一不同的就是一个物品可以选取无限次,而并非一次。
我们依旧设 DP 状态dp[i] [j]为在只能放前i个物品的情况下,容量为j的背包所能达到的最大总价值。
首先因为上面已经说过了0-1背包,所以自然能想到一个算法就是我把每个物品看作C/w[i]个一模一样的物品,每个物品只有选和不选两种选择,这样就转化成了0-1背包问题。但是这样的算法复杂度太高了,自然是不行的,但是它的思想可以帮助我们找到正确的转移方程。