Topcoder 13859 ClassicProblem

题目链接

Topcoder 13859 ClassicProblem

题目大意

背包问题,有 (n) 种物品,第 (i) 种物品有 (cnt_i) 个,重量 (w_i),价值 (v_i),背包容量 (limit),求能放下的物品最大价值总和。

(1leq nleq 80)(1leq cnt_ileq 10^9)(1leq w_ileq 80)(1leq v_ileq 10^9)(1leq limitleq 10^9)

法一

物品种类很少,重量范围也很小,想象一下填充的过程,可以发现大部分地方都是可以直接按照性价比((frac{v_i}{w_i}))从大到小放的,只有最后的一点空隙那边,直接放会出现无法充分利用空余空间的情况,需要去做背包 (DP)

具体来说,若 (frac{v_i}{w_i}<frac{v_j}{w_j}),那么把 (w_j) 个物品 (i) 替换成 (w_i) 个物品 (j) 是更优的,所以在所有物品的数量都 (<max{w_i}) 的时候贪心策略才不适用,这时 (cnt_ileq 80),而 (limitleq sum cnt_iw_ileq 512000),值域较小,可以直接做多重背包 (DP) 了,单调队列或二进制优化都是可以通过时限的,最后枚举 (DP) 部分占了多少空间,剩下部分贪心即可。

时间复杂度 (O(n^2W^2log W))(O(n^2W^2))

Code

// O(n^2*W^2logW)
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
#define mem(a,b) memset(a, b, sizeof(a))
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 82
#define W 80
#define ll long long
using namespace std;

class ClassicProblem{
    public:
    ll dp[N*W*W];
    struct item{
        ll cnt, w, v;
        bool operator < (const item b) const{ return v*b.w > b.v*w; }
    } a[N];
    vector<item> items;
    ll maximalValue(vector<int> cnt, vector<int> w, vector<int> v, int limit){
        int n = cnt.size();
        rep(i,0,n-1) a[i].cnt = cnt[i], a[i].w = w[i], a[i].v = v[i];
        sort(a, a+n);
        rep(i,0,n-1){
            int x = min(cnt[i], W);
            for(ll p = 1; p <= x; p <<= 1)
                items.push_back({1, w[i]*p, v[i]*p}), x -= p;
            if(x) items.push_back({1, (ll)w[i]*x, (ll)v[i]*x});
        }
        for(item t : items) per(i,n*W*W,t.w) dp[i] = max(dp[i], dp[i-t.w] + t.v);
        ll ans = 0;
        rep(s,0, min(n*W*W, limit)){
            ll lft = limit-s, tot = 0;
            rep(i,0,n-1){
                ll num = min(max(0ll, a[i].cnt-W), lft/a[i].w);
                tot += num*a[i].v, lft -= num*a[i].w;
            }
            ans = max(ans, tot+dp[s]);
        }
        return ans;
    }
} solve;

法二

法一是挺好想的做法,然而本人开始实现错了,误以为做法无法保证真确性,于是就有了法二。

考虑直接对它 (DP),如此大的值域容易想到 (log) 做法,于是考虑二进制分解,这里不仅要对 (cnt) 分解,也要对 (limit) 二进制分解,相当于数位 (DP) 里的一位位计算了。

(dp_{p,i}) 表示当前考虑到了第 (p) 位,在第 (p) 位往上还剩 (s) 的容量,即实际上是剩 (2^ps) 的容量。

先对 (cnt_i) 二进制分解,要对应到每个位上,因此分解后不能剩下余项,当前位为 (1) 时拆 (1) 个,当前位为 (0) 时拆 (2) 个。这样 (i) 的上限就应为 (2cdot 2n W),前面的 (2) 是考虑进位的情况。常规地,(DP) 要压着上限做,所以是从高位往低位转移 (dp)

  • (b)(limit) 在第 (p) 位的值, (dp_{p,i}leftarrow dp_{p+1,lfloorfrac{i-1+b}{2} floor})
  • 正常背包: (dp_{p,i}leftarrow dp_{p,i+w_j}+2^pv_j)

最后 (dp_{0,i}) 中的最大值即为答案。

时间复杂度 (O(log Vn^2W))

这个时间复杂度应该是比法一更加优秀的,本人的实现法二比法一快了 (16) 倍多一点。

// O(logV*n^2*W)
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
#define mem(a,b) memset(a, b, sizeof(a))
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 82
#define LOG 30
#define ll long long
#define PII pair<int, int>
#define fr first
#define sc second
using namespace std;

class ClassicProblem{
    public:
    ll dp[LOG+5][4*N*N];
    vector<PII> knapsack[LOG+5];
    ll maximalValue(vector<int> cnt, vector<int> w, vector<int> v, int limit){
        int n = cnt.size(), mx = 0;
        rep(i,0,n-1){
            mx = max(mx, w[i]);
            rep(p,0,LOG){
                if(!cnt[i]) break;
                rep(_,1,2-cnt[i]%2) knapsack[p].push_back({w[i], v[i]});
                if(cnt[i]&1) cnt[i] >>= 1;
                else cnt[i] >>= 1, cnt[i]--;
            }
        }
        mem(dp, 0x80), dp[LOG][0] = 0;
        per(p,LOG,0){
            int b = (limit>>p)&1;
            rep(i,0,4*n*mx) dp[p][i] = max(dp[p][i], dp[p+1][(i-b+1)/2]);
            for(PII a : knapsack[p]) rep(i,0,4*n*mx-a.fr) if(dp[p][i+a.fr] >= 0)
                dp[p][i] = max(dp[p][i], dp[p][i+a.fr] + (1ll<<p)*a.sc);
        }
        ll ans = 0;
        rep(i,0,4*n*mx) ans = max(ans, dp[0][i]);
        return ans;
    }
} solve;

和法二思路类似的题目我记得还有 CF1290F Making Shapes,使用二进制分解将「 值域极大,但是对象种类很少」的问题化大为小。

原文地址:https://www.cnblogs.com/Neal-lee/p/15428029.html