P3092 [USACO13NOV]没有找零No Change

首先可以想到 **枚举** 用硬币的顺序.($O(K!)$) 假如硬币顺序确定了, 我们只需要确定每一个硬币买哪一段即可. 发现可以贪心, 即每一个硬币都尽量买, 直到剩余价值不够为止. 证明很简单: 假如我这个硬币可以多买某一个商品但是没有买, 只可能是为了用下一个硬币去买这个商品来让下一个硬币浪费的少一些. 但是下一个硬币少浪费的其实等于这个硬币多浪费的, 也就是这个商品的价格. 换言之, 这样不会更好, 但有可能更差(钱不够). 于是, 算出前缀和以后大力二分每一个硬币用到哪儿即可. 理论复杂度$O(K!KlogN)$, 显然无法承受.

考虑状态压缩优化. 用一个数组 f[1 << 16] 记录下某一个状态时浪费价值的最小值.
因为对于给定的状态, 金币的总价值确定, 可以很容易的结合记录下来的值推出当前买到哪一个商品, 再用前文所提到的二分更新状态.
理论复杂度(O(2^N logN))

#include <cstdio>
#include <cstring>
#include <cassert>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int INF = 0x7f7f7f7f;
const int MAXN = 1e5 + 10;
inline int read()
{
    int x = 0; char ch = getchar();
    while(!isdigit(ch)) ch = getchar();
    while(isdigit(ch)) x = x * 10 + ch - '0', ch = getchar();
    return x;
}

int N, K;
ll tot[1 << 16];
int c[MAXN], a[MAXN]; ll sum[MAXN];
int f[1 << 16];

int main()
{
    cin>>K>>N;
    for(int i = 0; i < K; i++) c[i] = read();
    for(int i = 1; i <= N; i++) a[i] = read();
    for(int i = 1; i <= N; i++) sum[i] = sum[i - 1] + a[i];
    sum[N + 1] = (1LL << 60); 
    for(int i = 1; i < (1 << K); i++) 
        for(int j = 0; j < K; j++) if(i & (1 << j)) tot[i] += c[j];

    memset(f, 0x7f, sizeof(f)); f[0] = 0;
    ll ans = -(1LL << 60);
    for(int state = 0; state < (1 << K); state++) if(f[state] != INF){
        int cur = lower_bound(sum, sum + N + 2, tot[state] - f[state]) - sum;

        for(int i = 0; i < K; i++) if(!(state & (1 << i))){
            int tmp = (state | (1 << i));
            int nxt = upper_bound(sum, sum + N + 2, sum[cur] + c[i]) - sum;
            --nxt;
            f[tmp] = min(1LL * f[tmp], f[state] + c[i] - (sum[nxt] - sum[cur]));
            if(nxt == N) ans = max(ans, tot[((1 << K) - 1) ^ tmp]);
        }
    }
    if(ans == -(1LL << 60)) puts("-1");
    else cout<<ans<<endl;
    return 0;
}
原文地址:https://www.cnblogs.com/wsmrxc/p/9587949.html