Loading

P3092 [USACO13NOV]No Change G 状压 + 二分

P3092 [USACO13NOV]No Change G 状压 + 二分

题意

约翰到商场购物,他的钱包里有\(K\)(\(1 <= K <= 16\))个硬币,面值的范围是\([1,1e9]\)

约翰想按顺序买 \(N\)个物品(\(1 <= N <= 100,000\)),第i个物品需要花费\(c(i)\)块钱,(\(1 <= c(i) <= 10,000\))。

在依次进行的购买\(N\)个物品的过程中,约翰可以随时停下来付款,每次付款只用一个硬币,支付购买的内容是从上一次支付后开始到现在的这些所有物品(前提是该硬币足以支付这些物品的费用)。不幸的是,商场的收银机坏了,如果约翰支付的硬币面值大于所需的费用,他不会得到任何找零。

请计算出在购买完\(N\)个物品后,约翰最多剩下多少钱。如果无法完成购买,输出\(-1\)

分析

  • 注意审题,每次只能投一枚硬币
  • \(k\)比较小,考虑状压DP,\(dp[i]\)表示状态\(i\)下的最多能买到第几个物品
  • 显然第\(k\) 枚硬币是从上一个硬币转移而来的,转移方程\(dp[i] = max \lbrace dp[i异或k] + f(k) \rbrace\)
  • \(f(k)\)可以二分,于是总体复杂度\(O(2^kklogn)\)
  • 由于要知道多少钱,再开一个辅助数组即可

代码

int n,k;

int dp[1 << 16];
ll f[1 << 16];
ll sum[100005];
int a[100005];

int check(int x,int t){
	int l = t;
	int r = n;
	while(l < r){
		int mid = l + r + 1 >> 1;
		if(sum[mid] - sum[t - 1] <= x) l = mid;
		else r = mid - 1;
	}
	return l;
}

int main(){
	ll res = 1e18;
	ll tot = 0;
	k = readint();
	n = readint();
	for(int i = 0;i < k;i++)
		a[i] = readint(),tot += a[i];
	for(int i = 1;i <= n;i++)
		sum[i] = sum[i - 1] + readll();
	for(int i = 1;i < (1 << k);i++)
		for(int j = 0;j < k;j++)
			if(i & (1 << j)) {
				int x = i ^ (1 << j);
				ll tmp = check(a[j],dp[x] + 1);
				if(tmp > dp[i]) {
					dp[i] = tmp;
					f[i] = f[x] + a[j];
				}
				if(tmp == n) 
					res = min(res,f[i]);
			}
	if(tot - res < 0) puts("-1");
	else cout << tot - res;
}
posted @ 2020-11-25 15:36  MQFLLY  阅读(176)  评论(0编辑  收藏  举报