1. 程式人生 > 實用技巧 >題解 P1450 【[HAOI2008]硬幣購物】

題解 P1450 【[HAOI2008]硬幣購物】

題目連結

Solution [HAOI2008]硬幣購物

題目大意:給定\(4\)種硬幣的面值,多次詢問,給定\(4\)種硬幣的數目,求有多少種方法拼湊出面值\(S\)

揹包、容斥


分析:

先給出暴力做法(我一開始就是這麼做的)

類似生成函式,對於每種面值為\(c\),數量為\(d\)的硬幣,我們將\(x^c,x^{2c},x^{3c},\cdots,x^{dc}\)的係數置為\(1\),暴力卷積四次即可

單次\(O(S^2)\)直接上天,NTT和FFT應該也是過不了的

這個時候我們可以發揮一下人類智慧

假定只有兩種硬幣,面值分別為\(c_1,c_2\),分別有\(d_1,d_2\)種,那麼它們拼湊\(S\)

的方案數,實際上就是不定方程\(c_1x+c_2y=S\)的滿足\(x\in[0,d_1],y\in[0,d_2]\)的解的個數

這個是非常好算的,\(ax+by=c\),假如有特解\(x_0,y_0\),那麼所有通解為\(x=x_0+k\frac{b}{(a,b)},y=y_0-k\frac{a}{(a,b)}\),列個不等式就可以算出\(k\)的個數即解的個數

人類智慧算出前兩個和後兩個硬幣的方案數,我們只需要求卷積後的第\(S\)項,那麼可以\(O(S)\)

本機開\(O2\)可以\(900ms\)出結果,洛谷可以獲得\(60pts\)好成績,理論上迴圈展開之類的卡一下常數是可以跑過去的,程式碼附在最末


然後正解,假設我們不考慮任何數量限制,那麼這就是一個完全揹包,可以直接\(O(s)\)預處理出\(f[n]\),表示不考慮任何數量限制下湊出\(n\)面值的方案數

然後我們減去非法方案,也就是第一個超限,第二個超限,一直到第四個超限,它們的方案並

這個可以容斥原理

如果欽定第一個超限,它的數量為\(d\),面值為\(c\),那麼我們的方案數就是\(f[S-c(d+1)]\),其它同理

正解:

#include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 100;
int c[8],d[8],s,n;
ll f[maxn] = {1ll};
inline void solve(){
	cin >> d[1] >> d[2] >> d[3] >> d[4] >> s;
	ll ans = f[s];
	for(int now = 1;now <= ((1 << 4) - 1);now++){
		int flg = -1,tmp = s;
		for(int i = 0;i < 4;i++)
			if((now >> i) & 1){
				flg *= -1;
				tmp -= (d[i + 1] + 1) * c[i + 1];
			}
		if(tmp >= 0)ans += flg * f[tmp];
	}
	cout << ans << '\n';
}
int main(){
	cin >> c[1] >> c[2] >> c[3] >> c[4] >> n;
	for(int i = 1;i <= 4;i++)
		for(int j = c[i];j < maxn;j++)
			f[j] += f[j - c[i]];
	while(n--)solve();
	return 0;
}

暴力:(貌似現在禁掉了指令集,不然AVX512是隨便跑的/kk)

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 100;
typedef long long ll;
inline int exgcd(int a,int b,int &x,int &y){
	if(!b){
		x = 1,y = 0;
		return a;
	}
	int res = exgcd(b,a % b,y,x);
	y -= (a / b) * x;
	return res;
}
inline int ceil1(int a,int b){return (a + b - 1) / b;}
inline int floor1(int a,int b){return (a / b);}
inline int fh(int x){return x >= 0 ? 1 : -1;}
inline int abs(int x){return x >= 0 ? x : -x;}
inline int ceil(int a,int b){
	if(!a)return 0;
	if(fh(a) * fh(b) == 1)return ceil1(a,b);
	else return -floor1(abs(a),abs(b));
}
inline int floor(int a,int b){
	if(!a)return 0;
	if(fh(a) * fh(b) == 1)return floor1(a,b);
	else return -ceil1(abs(a),abs(b));
}
ll ans;
int c[8],d[8],a,b,n,s,x,y,dd,aa,bb;
int tmp[4][maxn];
inline int solve(int c,int d1,int d2){
	int x = ::x,y = ::y,L = -0x7fffffff,R = 0x7fffffff;
	if(c % dd)return 0;
	x *= c / dd;
	y *= c / dd;
	L = max(L,ceil(-x,aa));
	R = min(R,floor(d1 - x,aa));
	if(L > R)return 0;
	L = max(L,ceil(y - d2,bb));
	R = min(R,floor(y,bb));
	if(L > R)return 0;
	return R - L + 1;
}
inline void solve(){
	scanf("%d %d %d %d %d",d + 1,d + 2,d + 3,d + 4,&s);
	ans = 0;
	
	a = c[1],b = c[2];
	dd = exgcd(a,b,x,y),aa = b / dd,bb = (a / dd);
	for(int i = 0;i <= s;i++)
		if(d[1] * c[1] + d[2] * c[2] >= i)tmp[1][i] = solve(i,d[1],d[2]);
		else tmp[1][i] = 0;
	
	a = c[3],b = c[4];
	dd = exgcd(a,b,x,y),aa = b / dd,bb = (a / dd);
	for(int i = 0;i <= s;i++)
		if(d[3] * c[3] + d[4] * c[4] >= i)tmp[2][i] = solve(i,d[3],d[4]);
		else tmp[2][i] = 0;
	
	for(int i = 0;i <= s;i++)
		ans += tmp[1][i] * tmp[2][s - i];
	printf("%lld\n",ans);
}
int main(){
	scanf("%d %d %d %d %d",c + 1,c + 2,c + 3,c + 4,&n);
	while(n--)solve();
	return 0;
}