Topcoder 13859 ClassicProblem

题目链接

Topcoder 13859 ClassicProblem

题目大意

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

\(1\leq n\leq 80\)\(1\leq cnt_i\leq 10^9\)\(1\leq w_i\leq 80\)\(1\leq v_i\leq 10^9\)\(1\leq limit\leq 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_i\leq 80\),而 \(limit\leq \sum cnt_iw_i\leq 512000\),值域较小,可以直接做多重背包 \(DP\) 了,单调队列或二进制优化都是可以通过时限的,最后枚举 \(DP\) 部分占了多少空间,剩下部分贪心即可。

时间复杂度 \(O(n^2W^2\log 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\) 的上限就应为 \(2\cdot 2n W\),前面的 \(2\) 是考虑进位的情况。常规地,\(DP\) 要压着上限做,所以是从高位往低位转移 \(dp\)

  • \(b\)\(limit\) 在第 \(p\) 位的值, \(dp_{p,i}\leftarrow dp_{p+1,\lfloor\frac{i-1+b}{2}\rfloor}\)
  • 正常背包: \(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,使用二进制分解将「 值域极大,但是对象种类很少」的问题化大为小。

posted @ 2021-10-20 11:03  Neal_lee  阅读(111)  评论(0编辑  收藏  举报