【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
3 2 3 1 10
1000 2 2 2 900
Sample Output
27
HINT
数据规模
di,s<=100000
tot<=1000
Source