【BZOJ】1042: [HAOI2008]硬币购物(dp+容斥原理)

http://www.lydsy.com/JudgeOnline/problem.php?id=1042

一开始写了个O(nv)的背包,果断tle。。。

看了题解,,好神。。用了组合数学中的多重集合方案的容斥原理。

设$A_i$表示i超过d[i]的性质

则我们要求:

$$| \overline{A_1} \cap \overline{A_2} \cap ... \cap \overline{A_n} |$$

而我们可以根据容斥求出这个值,即:

$$| \overline{A_1} \cap \overline{A_2} \cap ... \cap \overline{A_n} | = | S | - (| A_1 | + | A_1 | + ... + | A_n |) + (|A_1 \cap A_2| + |A_1 \cap A_3| + ... + |A_{n-1} \cap A_n|) - ...$$

本题中,$|S|$就是硬币有无限制的数量来放满s元钱,这个用完全背包来搞就行了。而容斥的其它部分就有些神奇。

首先处理完完全背包的数组f[i]后,我们可以观察,怎么将$A_i$算出来?假如有$d_i$个i硬币,容量是s的背包,思考为什么会多出方案?

可以发现,多出方案的情况是每一种方案里必定有$d_i+1$枚硬币,那么可以看做是,在所有容量为$s-(d_i+1)*c_i$的背包的方案添加$d_i+1$枚i硬币,这样就保证了至少有$d_i+1$枚硬币。那么可以完美得出了$A_i$。而性质$A_i$之间的交集其实就是$s-(d_i+1)*c_i-(d_j+1)*c_j-...-(d_k+1)*c_k$,这个很容易看出来吧。。

然后问题就解决了。

2015.4.19 upd:

设Ai为第i种硬币数量超过Di的性质,则问题就是求:
Ai补的交集
那么也就是容斥一下答案要求的性质就是:
全集S-(Ai)+(Ai交Aj)-...
令f[i]表示恰好花费i的完全背包方案数。
发现Ai的性质就是:已经在背包里放了至少Di+1个i物品,然后剩下的背包随便放的方案,即f[s-(Di+1)*Ci]!
然后求这些性质的交集,发现其实和上面分析一样!Ai与Aj的交集的方案就是f[s-(Di+1)*Ci-(Dj+1)*Cj]!
于是容斥一下就行了!!!

 

#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#include <iostream>
#include <algorithm>
#include <queue>
#include <set>
#include <map>
using namespace std;
typedef long long ll;
#define pii pair<int, int>
#define mkpii make_pair<int, int>
#define pdi pair<double, int>
#define mkpdi make_pair<double, int>
#define pli pair<ll, int>
#define mkpli make_pair<ll, int>
#define rep(i, n) for(int i=0; i<(n); ++i)
#define for1(i,a,n) for(int i=(a);i<=(n);++i)
#define for2(i,a,n) for(int i=(a);i<(n);++i)
#define for3(i,a,n) for(int i=(a);i>=(n);--i)
#define for4(i,a,n) for(int i=(a);i>(n);--i)
#define CC(i,a) memset(i,a,sizeof(i))
#define read(a) a=getint()
#define print(a) printf("%d", a)
#define dbg(x) cout << (#x) << " = " << (x) << endl
#define error(x) (!(x)?puts("error"):0)
#define printarr2(a, b, c) for1(_, 1, b) { for1(__, 1, c) cout << a[_][__]; cout << endl; }
#define printarr1(a, b) for1(_, 0, b) cout << a[_] << '\t'; cout << endl
inline const int getint() { int r=0, k=1; char c=getchar(); for(; c<'0'||c>'9'; c=getchar()) if(c=='-') k=-1; for(; c>='0'&&c<='9'; c=getchar()) r=r*10+c-'0'; return k*r; }
inline const int max(const int &a, const int &b) { return a>b?a:b; }
inline const int min(const int &a, const int &b) { return a<b?a:b; }

const int N=100005;
ll f[N], ans;
int n, c[5], d[5], s;

inline void cmp(int v) { for1(i, v, s) f[i]+=f[i-v]; }
void dfs(int dep, int x, int sum) {
	if(dep==5) {
		if(s-sum<0) return;
		if(x&1) ans-=f[s-sum];
		else ans+=f[s-sum];
		return;
	}
	dfs(dep+1, x, sum);
	dfs(dep+1, x+1, sum+d[dep]);
}
int main() {
	for1(i, 1, 4) read(c[i]); int tot=getint();
	f[0]=1;
	s=100005;
	for1(i, 1, 4) cmp(c[i]);
	while(tot--) {
		for1(i, 1, 4) read(d[i]);
		for1(i, 1, 4) d[i]=(d[i]+1)*c[i];
		read(s);
		ans=0;
		dfs(1, 0, 0);
		printf("%lld\n", ans);
	}
	return 0;
}

  

 


 

 

Description

硬币购物一共有4种硬币。面值分别为c1,c2,c3,c4。某人去商店买东西,去了tot次。每次带di枚ci硬币,买si的价值的东西。请问每次有多少种付款方法。

Input

第一行 c1,c2,c3,c4,tot 下面tot行 d1,d2,d3,d4,s

Output

每次的方法数

Sample Input

1 2 5 10 2
3 2 3 1 10
1000 2 2 2 900

Sample Output

4
27

HINT

 

数据规模

di,s<=100000

tot<=1000

 

Source

 

 
posted @ 2014-11-13 17:34  iwtwiioi  阅读(...)  评论(... 编辑 收藏