P3092 [USACO13NOV]没有找零No Change

知识点 : 状压DP

原题面

分析题意:

  • 由于 每次只能花费一枚硬币 , 且硬币数 \(\le 16\)
    则可以使用 一二进制串 , 表示硬币的花费状况

  • 为什么会导致 无法完成购买 ?
    可以发现 , 若花费完所有硬币后, 还无法买到第n个物品
    则 导致无法完成购买.

    则对于每一个 硬币花费状态,
    都需要最大化其 能购买的物品编号
    考虑 状压 \(DP\) .

  • 设计状态:
    \(f[i]\) : 花费状态 i 的硬币 , 能买到的最远的物品 .
    显然有: \(f[0] = 0\) ;

  • 设将第 \(j\) 个硬币, 价值为 \(w[j]\) , 添加到状态i中,
    则有状态转移方程 为:
    \(f[i + 2^j] = max(f[i + 2^j] , f[i] + k)\ \ (sum[f[i] +1 \sim k] \le w[j])\)

    • \(sum[l,r]\)\([l,r]\) 内 物品的价值总和
      可以使用前缀和进行维护, 并 \(O(1)\) 查询

    • 如何快速查询 最优的 位置增量 \(k (sum[f[i] +1 \sim k] \le w[j])\) ?
      由于前缀和 单调递增 , 则可以进行 二分查找
      \(\log n\) 的时间内 求得最优的 位置增量 \(k\)

  • 终止条件 :
    若出现 \(f[i] = n\) , 花费说明状态 \(i\) 的硬币, 已经可以购买所有物品
    统计其价值 , 并取最大值为答案 即可

#include<cstdio>
#include<ctype.h>
#define max(a,b) (a>b?a:b)
#define ll long long
const int MARX = 1e5+10;
const int MARXALL = 131080;
//=============================================================
int k, n, all, coin[20], c[MARX], f[MARXALL];;
ll ans = -1, sum[MARX];
//=============================================================
inline int read()
{
    int s=1, w=0; char ch=getchar();
    for(; !isdigit(ch);ch=getchar()) if(ch=='-') s =-1;
    for(; isdigit(ch);ch=getchar()) w = w*10+ch-'0';
    return s*w;
}
int upper_search(int value, int l1, int r1)//找到区间[l1,r1] 大于value的第一个位置 
{
	int pos = r1 + 1;
	for(int l = l1, r = r1; l<=r;)
	{
	  int mid = (l + r) >> 1;
	  if(sum[mid] - sum[l1 -1] > value)  pos = mid,r = mid-1;
	  else l = mid + 1;
	}
	return pos;
}
//=============================================================
signed main()
{
	k = read(), n = read(); all = (1 << k) -1;//构造全集 
	for(int i = 1; i <= k; i ++) coin[i] = read();//读入并维护前缀和 
	for(int i = 1; i <= n; i ++) c[i] = read(), sum[i] = sum[i-1] + c[i];
	
	for(int i = 0; i <= all; i ++)
	{
	  if(f[i] == n)//状态满足条件, 统计答案 
	  {
	  	ll sum1 = 0;
	  	for(int j = 0; j < k; j ++) sum1 += (!(1 << j & i))*coin[j+1];//求得 未使用的硬币总和 
	  	ans = max(ans,sum1);//取最大值 
	  	continue;
	  }
	  
	  int l = f[i] + 1, r = n, now = 1;//构造二分查找边界, 与当前枚举到的硬币编号 
	  for(; ; )
	  {
		for(;(1 << (now-1) & i) && now <= k;) now++;//循环枚举 ,直到 一个未被使用的硬币 
		if(now > k) break;
		
		int pos = upper_search(coin[now],l,r) - 1;//查找 最大增量 
		f[i | (1 << (now-1))] = max(f[i | (1 << (now-1))], pos);//转移 
	    now ++;//编号++ 
	  }
	}
	printf("%lld",ans);
}
posted @ 2019-10-16 16:19  Luckyblock  阅读(98)  评论(0编辑  收藏  举报