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;
}
凡所不能将我击倒的,都将使我更加强大