好久没写博客,今天认真写一波~
多重背包问题:
有N种物品和容量为V的背包,若第i种物品,容量为v[i],价值为w[i],共有num[i]件。怎样装才能使背包内的物品总价值最大?
复杂度O(n * V * sigma(num[i]))的算法大家学完01背包应该就会了
dp[j] = max(dp[j], dp[j - k*v[i]] + k * w[i]);(0<=k<=min(num[i], V/v[i]));
而想要优化复杂度,我们只能化简式子
令j = a*v[i]+b, m = min(num[i], V/v[i]); 则a = j/v[i], b = j%v[i];
把j = a*v[i]+b代入原来的dp方程得到
dp[j] = max(dp[j], dp[(a - k)* v[i]+b] + k*w[i]);
令t = a-k,则原式继续变为dp[j] = max(dp[j], dp[t* v[i]+b] - t* w[i] + a* w[i]);(a-m<=t<=m)
我们可以发现,无论t等于多少,末尾那个a*w[i]都是要加上去比较大小的,因此可以提出来。
dp[j] = max(dp[j], dp[t* v[i]+b] - t*w[i]) + a * w[i];(a - m <=t <= a)
于是我们可以考虑先枚举b,再枚举t,那么对于每一个t* v[i]+b, 我们重新计算当前的a,发现a每次随着t的增加++,因而区间(t的取值范围)是递增的,然后可以用递减的单调队列维护a-m到a的值,每次用队列的最左端更新dp[t* v[i]+b]的值,在这之前把上一维更新的dp[t* v[i]+b]的值加进单调队列,这样每次枚举一个b,就把dp[(0~t)* v[i]+b]都更新了,当b枚举完时,dp[0~V]的答案也就更新完了,没做一个物品,会把0~V都扫一遍,总复杂度O(n*V)。巧妙而优秀的算法!!!
#include<bits/stdc++.h>
#define ll long long
#define inf 999999999
#define mm(a,b) memset(a,b,sizeof(a))
using namespace std;
int read(){
int sum = 0,fg = 1;
char c = getchar();
while(c < '0' || c > '9'){if(c == '-')fg = -1;c = getchar();}
while(c >='0' && c <='9')sum = (sum<<1) + (sum<<3) + c-'0',c = getchar();
return sum * fg;
}
const int maxn = 100010;
int v[maxn], w[maxn], n, V, cnt, dp[maxn], num[maxn], m;
struct node{
int id, val;
node(){
id = 0,val = 0;
}
}q[maxn];
void solve(){
for(int i = 1;i <= n; ++i){
for(int b = 0;b < v[i]; ++b){
int l = 1, r = 0;
for(int k = 0; b + k * v[i] <= V; ++k){
int ToT = b + k * v[i];
m = min(num[i], ToT / v[i]);//计算限制
if(r - l + 1 && q[l].id < k - m)++l;//弹掉不能更新答案的解
int Now = dp[ToT] - k * w[i];//把上一维的值加进队列
while(l <= r && Now > q[r].val) --r;
q[++r].val = Now, q[r].id = k;
dp[ToT] = q[l].val + k * w[i];//更新答案
}
}
}
printf("%d\n", dp[V]);
}
int main(){
#ifndef ONLINE_JUDGE
freopen("ai.in","r",stdin);
freopen("ai.out","w",stdout);
#endif
n = read(), V = read();
for(int i = 1;i <= n; ++i){
v[i] = read(), w[i] = read(), num[i] = read();
}
solve();
return 0;
}
单调队列只是优化多重背包的其中一种方法,二进制分组在我看来则更加巧妙
比如说一个物品的数量是20,价值为1,体积为2,那么我们把20分解成2的幂次方相加(从(2^0)开始)20 = 1 + 2 + 4 + 8 + 5, 不足直接用原数减去补上
然后把这一个数量为20的物品,分为5个物品,一个数量为1,价值为1 *1, 体积为2 *1的物品,
一个数量为2,价值为2 1,体积为2 *2的物品,一个数量为4,价值为4 1的,体积为2 * 4的物品,一个数量为8,价值为8 * 1的,体积为2 * 8的物品,一个数量为5,价值为5 * 1的,体积为2 * 5的物品。
最后做对每个物品做01背包即可。
原理就是用这分解出的物品任意组合,可以构造出数量为(1~num[i])的物品,上面这个数量为20的物品,1~20就可以用1,2,4,8,5任意组合得到,于是就玄学的得到了一个复杂度优秀的算法~
我十分崇拜第一个发现这个性质的同志~
#include<bits/stdc++.h>
#define ll long long
#define inf 999999999
#define mm(a,b) memset(a,b,sizeof(a))
using namespace std;
int read(){
int sum = 0,fg = 1;
char c = getchar();
while(c < '0' || c > '9'){if(c == '-')fg = -1;c = getchar();}
while(c >='0' && c <='9')sum = (sum<<1) + (sum<<3) + c-'0',c = getchar();
return sum * fg;
}
const int maxn = 100010;
int v[maxn], w[maxn], n, V, cnt, dp[maxn];
void solve(){
for(int i = 1;i <= cnt; ++i){
for(int j = V;j >= v[i]; --j){
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
printf("%d\n", dp[V]);
}
int main(){
#ifndef ONLINE_JUDGE
freopen("ai.in","r",stdin);
freopen("ai.out","w",stdout);
#endif
n = read(), V = read();
for(int i = 1;i <= n; ++i){
int weight = read(), val = read(), num = read();
int now = 0;
for(int j = 0;j <= 20; ++j){
if(now + (1 << j) <= num) {
now += (1 << j);
w[++cnt] = val * (1 << j), v[cnt] = weight * (1 << j);
}
else break;
}
if(now != num)w[++cnt] = val * (num - now), v[cnt] = weight * (num - now);
}
solve();
return 0;
}
因篇幅问题不能全部显示,请点此查看更多更全内容