多重背包的优化

好久没写博客,今天认真写一波~
多重背包问题:
有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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值