USACO13NOV-No Change

这里是简介

思路

前言

刚看到这道题$k\leq 16$的范围时我以为是暴力枚举…
于是就高高兴兴敲了一个暴枚+贪心骗分…
最后觉得骗分不实在,于是就改成暴枚+全排列+二分查找了…

正文

其实可以用DP来做,不过会MLE,怎么办呢?
这时我们发现: $k\leq 16$ 于是可以压硬币的状态!
于是就变成了状态压缩DP!
转移方程:
设$dp_i$表示我们选硬币集合 i(状压)从一号物品开始最多能买的物品数。
于是有:$dp_i=max(dp_i,dp_{subset(i)}+coin)$
这里的coin表示选了当前这块硬币后又能买多少东西!
怎么处理coin呢?——用二分查找啊,在原基础上利用前缀和进行二分查找…(前缀和单调递增啊)
当然,你也可以用尺取预先处理(空间换时间)

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using std::upper_bound;
typedef long long ll;

inline ll max (ll a, ll b) {return a > b ? a : b;}
const ll K = 16, N = 1e5 + 10;
ll k, n, ce[K], co[N], sum[N], cnt, ans = -1, dp[70000];

//计算当前所剩钱数 
inline ll num (ll i) {
    ll val = cnt;
    for (register ll j = 0; j < k; ++j)
        if (i & (1 << j)) val -= ce[j];
    return val;
}

int main () {
    scanf ("%lld%lld", &k, &n);
    for (register ll i = 0; i < k; ++i) {
        scanf ("%lld", ce + i);
        cnt += ce[i];
    }
    for (register ll i = 1; i <= n; ++i) {
        scanf ("%lld", co + i);
        sum[i] = sum[i - 1] + co[i];
    }
    for (register ll i = 1; i < (1 << k); ++i) 
        for (register ll j = 0; j < k; ++j) 
            if (i & (1 << j)) {
                ll tmp = i ^ (1 << j);
                tmp = upper_bound(sum + 1, sum + n + 1, sum[dp[tmp]] + ce[j]) - sum - 1;
                dp[i] = max(dp[i], tmp);
            }
    for (register ll i = 1; i < (1 << k); ++i)
        if (dp[i] == n)
            ans = max (ans, num(i));
    printf ("%lld\n", ans);
    return 0;
}