P3092 [USACO13NOV]No Change G

题目链接

点我跳转

题目大意

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

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

在依次进行的购买N个物品的过程中,约翰可以随时停下来付款,每次付款只用一个硬币,支付购买的内容是从上一次支付后开始到现在的这些所有物品

(前提是该硬币足以支付这些物品的费用)

不幸的是,商场的收银机坏了,如果约翰支付的硬币面值大于所需的费用,他不会得到任何找零。

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

解题思路

注意每次付款只能使用一个硬币,且约翰必须按顺序购买

定义 \(dp[i]\) 表示状态为 \(i\) 的集合所能购买到最远的物品的位置

那么 \(dp[i]\) 可以由 \(dp[j]\) 转移得到 (\(j\)\(i\) 的子集 , 且 \(i - j = 2^k\) , \(k∈N\)

\(i\)\(j\) 的差别在于 \(i\) 状态多了一个硬币,硬币价值为 \(a[K - k]\) , 其中 \(k\) 满足 \(i - j = 2^k\)

那么 \(dp[i] = dp[j]\) + {从$ dp[j] + 1$ 开始 \(a[K - k]\) 所能购买到的最多的商品数量}

因为必须按顺序购买,所以能购买的商品数量满足单调性

可以维护一个商品前缀和然后二分求出

AC_Code

#include<bits/stdc++.h>

#define int long long

using namespace std;

const int N = 1e5 + 10;

int a[N] , b[N] , sum[N];

int dp[N]; 

int get(int l , int r){
	return sum[r] - sum[l - 1];
}

int calc(int l , int r , int x){
	int ans = l - 1 , L = l , R = r;
	while(L <= R)
	{
		int mid = L + R >> 1;
		if(x >= get(l , mid)) L = mid + 1 , ans = mid;
		else R = mid - 1; 
	}
	return ans - l + 1;
}

signed main()
{
	
	int K , n , cnt = 0;
	
	cin >> K >> n;
	
	int up = (1 << K) - 1;
	
	for(int i = 1 ; i <= K ; i ++) cin >> a[i] , cnt += a[i];
	
	for(int i = 1 ; i <= n ; i ++) cin >> b[i] , sum[i] = sum[i - 1] + b[i];
	
	int res = -1;
	
	for(int i = 1 ; i <= up ; i ++)
	{
		int m = 0;
		
		for(int j = 0 ; j <= K - 1 ; j ++) if(i >> j & 1) m += a[K - j];
		
		for(int j = 0 ; j <= K - 1 ; j ++) if(i >> j & 1)
		{
			int now = i - (1 << j);
			dp[i] = max(dp[i] , dp[now] + calc(dp[now] + 1 , n , a[K - j]));			
		}
		
		if(dp[i] == n) res = max(res , cnt - m);
	}
	
	cout << res << '\n';
	
	return 0;
}
posted @ 2021-02-10 16:11  GsjzTle  阅读(61)  评论(0编辑  收藏  举报