bzoj 2142 礼物 (组合数学+数论)

2142: 礼物

Time Limit: 10 Sec   Memory Limit: 259 MB
Submit: 1139   Solved: 478
[ Submit][ Status][ Discuss]

Description

一年一度的圣诞节快要来到了。每年的圣诞节小E都会收到许多礼物,当然他也会送出许多礼物。不同的人物在小E心目中的重要性不同,在小E心中分量越重的人,收到的礼物会越多。小E从商店中购买了n件礼物,打算送给m个人,其中送给第i个人礼物数量为wi。请你帮忙计算出送礼物的方案数(两个方案被认为是不同的,当且仅当存在某个人在这两种方案中收到的礼物不同)。由于方案数可能会很大,你只需要输出模P后的结果。

Input

输入的第一行包含一个正整数P,表示模;第二行包含两个整整数n和m,分别表示小E从商店购买的礼物数和接受礼物的人数;以下m行每行仅包含一个正整数wi,表示小E要送给第i个人的礼物数量。

Output

若不存在可行方案,则输出“Impossible”,否则输出一个整数,表示模P后的方案数。

Sample Input

100 4 2 1 2

Sample Output

12


【样例说明】
下面是对样例1的说明。
以“/”分割,“/”前后分别表示送给第一个人和第二个人的礼物编号。12种方案详情如下:
1/23 1/24 1/34
2/13 2/14 2/34
3/12 3/14 3/24
4/12 4/13 4/23
【数据规模和约定】
设P=p1^c1 * p2^c2 * p3^c3 * … *pt ^ ct,pi为质数。
对于100%的数据,1≤n≤109,1≤m≤5,1≤pi^ci≤10^5。

HINT

Source

[ Submit][ Status][ Discuss]


题解:组合数学+数论

这道题就是求c(n,sum)*c(sum,w[1])*c(sum-w[1],w[2])...*c(w[m],w[m])%p

式子其实可以进一步化简变成

n!/(w[1]!*w[2]!*..*w[m]!)%p

如果这里的p,是质数的话,很容易想到用lucas定理来做,但是这里p不是质数,那怎么办呢?

首先考虑把p分解,P=p1^c1 * p2^c2 * p3^c3 * … *pt ^ ct,pi为质数。

我们考虑分开处理,得到t个ans%(pi^ci)=ai 的一次同余方程,然后用中国剩余定理合并。

中国剩余定理(CRT)的表述如下

设正整数两两互素,则同余方程组

 

                             

 

有整数解。并且在模下的解是唯一的,解为

 

                               

 

其中,而意义下的逆元。

知道中国剩余定理后,就要考虑怎么求c(n,m)%pi^ci ,c(n,m)=n!/(m!(n-m)!)  因为有除法所以我们需要用到乘法逆元,但是pi^ci不是质数不能用费马小定理,所以无法用快速幂来求,因为m!,(n-m)!与pi^ci不互质所以我们也不能直接用扩展欧几里德解ax=1 (mod b) gcd(a,b)=1 , ax+by=1 ,但是我们可以考虑提取公因数让其互质。

举例说明:

n!=t1*pi^c1

m!=t2*pi^c2

(n-m)!=t3*pi^c3

那么怎样快速求解t1,t2,t3,c1,c2,c3呢?

以19!为例,假设pi=3

19!=1*2*3...*19

   =1*2*4*5*7*8*10*11*13*14*16*17*19*(3*1*3*2*3*3*3*4*3*5*3*6)

   =1*2*4*5*7*8*10*11*13*14*16*17*19*3^6(1*2*3*4*5*6)

对于上面这个式子我们需要求出 [1, pi^ci - 1] 中除去 p 的倍数的其余数的前缀积(类似阶乘少了 p 的倍数)。

然后我们知道 [1, n] 中包含 pi的数有 n / pi 个,我们将这些数中都提取出 1 个 pi,那么就获得了 pi^(n/pi),然后这 n / pi 个数就变成了 [1, n/pi],就可以递归下去。

其余的部分可以分段来求,分成 [1, pi^ci - 1], [pi^ci + 1, pi^ci + pi^ci - 1] ..... 这样,每一段的积都是一样的 (mod pi^ci 意义下),(至于为什么一样我就不知道啦,自己手算验证一下好了)直接快速幂就可以了。

最后c(n,m)%pi^ci 的答案就是t1*inverse(t2)*inverse(t3)*pi^(c1-c2-c3) %pi^ci

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define LL long long 
#define N 200003
#define pa pair<int,int>
using namespace std;
LL n,m,p,sum;
LL w[N],prime[N],cnt[N],mod[N],a[N];
int tot;
void get_prime()//模数分解成P=p1^c1 * p2^c2 * p3^c3 * … *pt ^ ct,pi为质数的形式 
{
	LL x=p;
	for (LL i=2;i*i<=x;i++)
	 if (x%i==0)
	  {
	  	prime[++tot]=i; cnt[tot]=0; mod[tot]=1;
	  	while (x%i==0)
	  	 {
	  	 	x/=i;
	  	 	cnt[tot]++; mod[tot]*=i;
	  	 }
	  }
	if (x>1)
	 {
	 	prime[++tot]=x;
	 	cnt[tot]=1;
	 	mod[tot]=x;
	 }
}
LL quickpow(LL num,LL x,LL m)//快速幂 
{
	LL ans=1;
	LL base=num%m;
	while (x)
	{
		if (x&1)  ans=(ans*base)%m;
		x>>=1;
		base=(base*base)%m;
	}
	return ans%m;
}
void exgcd(LL a,LL b,LL &x,LL &y)//扩展欧几里德算法  
//ax=1 (mod b) gcd(a,b)==1 ax+by=1的解,x为a的逆元  
{
	if (b==0)
	 {
	 	x=1; y=0;
	 	return;
	 }
	exgcd(b,a%b,x,y);
	LL t=y;
	y=x-(a/b)*y;
	x=t;
}
LL inverse(LL a,LL b)
{
	LL x,y;
	exgcd(a,b,x,y);
	return (x%b+b)%b;
}
pa solve(int k,LL n)// 将当前模数的倍数全部提取出来,这样保证a,b互质,就可以用exgcd求逆元 ,对于倍数直接做减法就可以了 
{
	if (n==0)  return make_pair(0,1);
	LL x=n/prime[k],y=n/mod[k];
	LL ans=1;
	if (y)
	{
		for (LL i=2;i<mod[k];i++)
		 {
		 	if (i%prime[k]!=0)
		 	 ans=(ans*i)%mod[k];
		 }
		ans=quickpow(ans,y,mod[k]);
	}
	for (LL i=y*mod[k]+1;i<=n;i++)
	 if (i%prime[k]!=0)
	  ans=(ans*i)%mod[k];
	pa t=solve(k,x);
	return make_pair(t.first+x,ans*t.second%mod[k]);
}
LL calc(int k,LL n,LL m)
{
	if (n<m)  return 0;
	pa a=solve(k,n),b=solve(k,m),c=solve(k,n-m);
	LL t1=quickpow(prime[k],a.first-b.first-c.first,mod[k])*a.second%mod[k];
	LL t2=inverse(b.second,mod[k])%mod[k];
	LL t3=inverse(c.second,mod[k])%mod[k];
	return t1*t2*t3%mod[k];
}
LL china()//中国剩余定理 
{
    LL x,y,ans=0;
    for(int i=1;i<=tot;i++)
    {
        LL r=p/mod[i]; 
        exgcd(r,mod[i],x,y);
        ans=(ans+r*x*a[i])%p;
    }
    return (ans+p)%p;
}
LL work(LL n,LL m)
{
	for (int i=1;i<=tot;i++)
	 a[i]=calc(i,n,m);
	return china();
}
int main()
{
	scanf("%lld%lld%lld",&p,&n,&m);
	for (LL i=1;i<=m;i++)
	 scanf("%lld",&w[i]),sum+=w[i];
    if (sum>n)  {
    	printf("Impossible\n");
    	return 0;
    }
    get_prime(); 
    LL ans=work(n,sum)%p; 
    for (LL i=1;i<=m;i++)
    {
    	ans=ans*work(sum,w[i])%p;
    	sum-=w[i];
    }
    printf("%lld\n",(ans%p+p)%p);
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值