P1450 [HAOI2008] 硬币购物 题解

发布时间 2023-12-19 11:54:04作者: Creeper_l

原题链接:P1450

这道题被教练放到了状压 \(DP\) 的题单里面,但是正解却不是状压 \(DP\),而是背包 \(+\) 神奇容斥,只不过是用到了一些二进制状压的思想。

思路

首先看到题目立马就想到了多重背包,但是时间复杂度肯定接受不了,于是考虑优化背包。我们可以想到一个很神奇的性质:假设只有一种硬币,看到限制条件每种硬币只能用 \(d\) 个,发现其实方案数就等于每个硬币可以用无限次的方案数减去硬币用了 \(d\) 次以上的方案数。通过这个性质,我们考虑可以用容斥来做这道题。先用完全背包求出每个硬币都可以用无限次的方案数,再用容斥计算出最终的答案。

  • 设计状态

显然,设 \(dp_{i}\) 表示每个硬币都可以用无限次,拼出 \(i\) 的方案数。

  • 方程转移

和完全背包类似:\(dp_{j}=dp_{j}+dp_{j-c_{i}}\)

  • 容斥

这是本道题最重要的一部分了。

我们已经算出了每个硬币都可以用无限次,拼出 \(s\) 的方案数 \(dp_{s}\) 了,只需要减掉每种硬币用了超过 \(d\) 个的情况。可以把第 \(i\) 种硬币强制支付 \(d+1\) 个,这样就一定是不合法(需要减掉)的。那么强制支付 \(d+1\) 个后的方案数是多少呢?其实就是 \(dp_{s-(d+1)\times c_{i}}\)。这个转化十分巧妙。但是如果减掉了这四种情况还是错的,因为多减了两个及以上都超过 \(d\) 个的情况。于是用容斥原理算一遍就可以了。

容斥原理:card(A∪B)=card(A)+card(B)−card(A∩B)

多个元素的容斥可以递推了,可以发现一些性质:奇数加,偶数减。这样我们可以用一个四位的二进制数来表示,第 \(i\) 位为 \(1\) 就表示第 \(i\) 个硬币超过了题目的条件。然后通过 \(1\) 的个数来判断是加还是减,再用 \(sum\) 来记录 \(\sum (d+1)\times c_{i}\) 就可以了。

Code

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define inf 0x3f
#define inf_db 1x27
#define ls id << 1 
#define rs id << 1 | 1
#define re register
typedef pair <int,int> pii;
const int MAXN = 1e5 + 10;
const int MAXM = 10;
int c[MAXM],d[MAXM],n,s,dp[MAXN];
signed main()
{
	for(int i = 1;i <= 4;i++) cin >> c[i];
	cin >> n;
	dp[0] = 1;
	for(int i = 1;i <= 4;i++)
	    for(int j = c[i];j <= 1e5;j++) dp[j] += dp[j - c[i]];
	while(n--)
	{
		for(int i = 1;i <= 4;i++) cin >> d[i];
		cin >> s;
		int ans = dp[s];
		for(int i = 1;i <= 15;i++)
		{
			int k = 0,num = 0,fl;
			for(int j = 1;j <= 4;j++)
			{
				if(!(i & (1 << j - 1))) continue;
				num++;
				k += c[j] * (d[j] + 1);
			}
			if(num % 2 == 0) fl = 1;
			if(num % 2 == 1) fl = -1;
			if(s >= k) ans += dp[s - k] * fl; 
		}
		cout << ans << endl;
	}
	return 0;
}