コンビネーション数モデリングのまとめ
本稿では,C(N,M)modPについて,異なるデータ範囲に対する3つのアルゴリズムを紹介する.
例一:FZU 2020
データ範囲n,m,p(1<=m<=n<=10^9,m<=10^4,m
例二:BZOJ 2982
データ範囲1<=m<=n<=2,000,000,000,p=10007.
本題の割合は1 mの範囲を2*10^8に増やした.pは10007しかなく、質量数である.
そこでlucasの定理を直接運用することができる.
Lucasの定理:
where
and
これがあれば、m,nを直接p進法に分割することができます.
C(Nk,Mk)mod pの求め方は,例1のO(P)時間で解くことに変換できる.
時間複雑度O(p),空間複雑度O(p).
code:
例3:HDU 3439
nの全配列の中でn-k転位列の方案数mod pだけあることを求めます.
データ範囲(0<=k<=n<=10^9,1<=p<=10^5,n!=0)
ターゲットの答えはC(n,k)*f(n-k)mod pで、f(i)はiです!全配列誤りスキーム数.
f(n−k)mod pは直接ループノードを探すことができ,ループノード長はm*2である.
次にC(n,k)mod pを要求する.本題ではpはまだ小さいが,pは素数を保証しないため,例2の方法は使用できない.
どうしようかな、まずp分解素因数p=p 1^k 1*p 2^k 2.*.*pt^kt.そしてc(n,k)mod pi^kiの値をそれぞれ求め,中国の残りの定理を用いて方程式群と同列に統合すればよい.
肝心なのはc(n,k)mod pi^kiを求めることだ.
c(n,k)=n!/(k!(n-k)!).分子分母中のすべての素因数piを蹴り出すと、分母はpi^kiと互いに質を交換し、拡張ユークリッドでmod pi^kiの逆元を求めればよい.
O(logPi)の時間内に分子分母に含まれる素因数piの個数を求め,それらの差をTとすることができる.
そしてすべての問題はnを求めることに転化します!素因数pを蹴ったmod p^cの値にpi^Tを乗じます.
前処理F[i]は、1からiまでpで割り切れない数の積を表す.P=p^cとする.1からnを求めてP個ごとに節を分けます.各グループの前p-1個の数mod Pは同余であるため、素因数pを含まない積はF[n%P]*F[P-1]^(n/P)であり、残りの素因数pの数に対してpを除去した後に積は(n/p)!に変換される.このようにして同じサブ問題に変換します!
1つのインスタンスでシミュレーション:20!Mod 9.
20!=(1*2*4*5*7*8*…*16*17*19)*(3*6*9*12*15*18)=(1*2*4*5*7*8)^2*(19*20)*3^6(1*2*3*4*5*6)=F[9-1]^(20/9)*F[20%9]*((20/3)! Mod 9).
ここで、F[0]からF[9−1]までの値は1,1,2,2,8,4,4,1,8である.
このように問題は完璧に解決された.
時間的複雑度はO(Σpi^ki).空間複雑度O(m).
code:
例一:FZU 2020
データ範囲n,m,p(1<=m<=n<=10^9,m<=10^4,m
C(N,M)=N*(N-1)*(N-2)*...*(N-M+1)/M!
mは非常に小さく,分子分母にはm項があることに気づき,O(m)暴力で分子分母mod pの値を求めることができる.
またm
時間複雑度O(m+log 2 p),空間複雑度O(1).
code:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <string>
using namespace std;
typedef long long LL;
LL n,k,p,a,b;
int get()
{
int f=0,v=0;char ch;
while(!isdigit(ch=getchar()))if(ch=='-')break;
if(ch=='-')f=1;else v=ch-48;
while(isdigit(ch=getchar()))v=v*10+ch-48;
if(f==1)return -v;else return v;
}
LL Pow(LL a,LL b,LL p)
{
LL res=1;
for(;b>0;b/=2)
{
if(b%2==1)res=res*a%p;
a=a*a%p;
}
return res;
}
int main()
{
int t=get();
while(t--)
{
n=get(),k=get(),p=get();
a=1,b=1;
for(int i=1;i<=k;i++)a=a*(n-i+1)%p,b=b*i%p;
b=Pow(b,p-2,p);
printf("%d
",a*b%p);
}
return 0;
}
例二:BZOJ 2982
データ範囲1<=m<=n<=2,000,000,000,p=10007.
本題の割合は1 mの範囲を2*10^8に増やした.pは10007しかなく、質量数である.
そこでlucasの定理を直接運用することができる.
Lucasの定理:
where
and
これがあれば、m,nを直接p進法に分割することができます.
C(Nk,Mk)mod pの求め方は,例1のO(P)時間で解くことに変換できる.
時間複雑度O(p),空間複雑度O(p).
code:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <string>
using namespace std;
const int p=10007;
int n,k,inv[p],fac[p],a[10],b[10],len,ans;
int get()
{
int f=0,v=0;char ch;
while(!isdigit(ch=getchar()))if(ch=='-')break;
if(ch=='-')f=1;else v=ch-48;
while(isdigit(ch=getchar()))v=v*10+ch-48;
if(f==1)return -v;else return v;
}
int Pow(int a,int b)
{
int res=1;
for(;b>0;b/=2)
{
if(b%2==1)res=res*a%p;
a=a*a%p;
}
return res;
}
int C(int n,int k)
{
if(k>n)return 0;
if(k==0||n==0||n==k)return 1;
return fac[n]*inv[k]%p*inv[n-k]%p;
}
int main()
{
int T=1;
for(int i=1;i<p;i++)T=T*i%p,inv[i]=Pow(T,p-2),fac[i]=T;
T=get();
while(T--)
{
n=get(),k=get(); ans=1;
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
for(len=0;k>0;k/=p)a[++len]=k%p;
for(len=0;n>0;n/=p)b[++len]=n%p;
for(int i=1;i<=len;i++)ans=ans*C(b[i],a[i])%p;
printf("%d
",ans);
}
return 0;
}
例3:HDU 3439
nの全配列の中でn-k転位列の方案数mod pだけあることを求めます.
データ範囲(0<=k<=n<=10^9,1<=p<=10^5,n!=0)
ターゲットの答えはC(n,k)*f(n-k)mod pで、f(i)はiです!全配列誤りスキーム数.
f(n−k)mod pは直接ループノードを探すことができ,ループノード長はm*2である.
次にC(n,k)mod pを要求する.本題ではpはまだ小さいが,pは素数を保証しないため,例2の方法は使用できない.
どうしようかな、まずp分解素因数p=p 1^k 1*p 2^k 2.*.*pt^kt.そしてc(n,k)mod pi^kiの値をそれぞれ求め,中国の残りの定理を用いて方程式群と同列に統合すればよい.
肝心なのはc(n,k)mod pi^kiを求めることだ.
c(n,k)=n!/(k!(n-k)!).分子分母中のすべての素因数piを蹴り出すと、分母はpi^kiと互いに質を交換し、拡張ユークリッドでmod pi^kiの逆元を求めればよい.
O(logPi)の時間内に分子分母に含まれる素因数piの個数を求め,それらの差をTとすることができる.
そしてすべての問題はnを求めることに転化します!素因数pを蹴ったmod p^cの値にpi^Tを乗じます.
前処理F[i]は、1からiまでpで割り切れない数の積を表す.P=p^cとする.1からnを求めてP個ごとに節を分けます.各グループの前p-1個の数mod Pは同余であるため、素因数pを含まない積はF[n%P]*F[P-1]^(n/P)であり、残りの素因数pの数に対してpを除去した後に積は(n/p)!に変換される.このようにして同じサブ問題に変換します!
1つのインスタンスでシミュレーション:20!Mod 9.
20!=(1*2*4*5*7*8*…*16*17*19)*(3*6*9*12*15*18)=(1*2*4*5*7*8)^2*(19*20)*3^6(1*2*3*4*5*6)=F[9-1]^(20/9)*F[20%9]*((20/3)! Mod 9).
ここで、F[0]からF[9−1]までの値は1,1,2,2,8,4,4,1,8である.
このように問題は完璧に解決された.
時間的複雑度はO(Σpi^ki).空間複雑度O(m).
code:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <string>
using namespace std;
typedef long long LL;
const int maxn=200003;
int ans,n,k,p,T,F[maxn],c[maxn],a[maxn],t[maxn],tot;
int get()
{
int f=0,v=0;char ch;
while(!isdigit(ch=getchar()))if(ch=='-')break;
if(ch=='-')f=1;else v=ch-48;
while(isdigit(ch=getchar()))v=v*10+ch-48;
if(f==1)return -v;else return v;
}
int calc()
{
F[0]=1,F[1]=0;
for(int i=2;i<2*p;i++)F[i]=LL(i-1)*(F[i-1]+F[i-2])%p;
return F[(n-k)%(2*p)];
}
void init(int p)
{
tot=0;
int pp=p;
for(int i=2;i*i<=pp;i++)
{
if(p%i!=0)continue;
a[++tot]=i,c[tot]=1,t[tot]=0;
while(p%i==0)c[tot]*=i,p/=i,t[tot]++;
}
if(p!=1)a[++tot]=p,c[tot]=p,t[tot]=1;
}
void ext_gcd(LL a,LL b,LL &x,LL &y)
{
if(b==0)
{
x=1,y=0;
return;
}
ext_gcd(b,a%b,x,y);
LL tp=x;
x=y;
y=tp-a/b*y;
}
LL Pow(LL a,LL b,int P)
{
LL res=1;
for(;b>0;b/=2)
{
if(b%2==1)res=res*a%p;
a=a*a%p;
}
return res;
}
LL calc(int n,int p,int _p)
{
if(n<_p)return F[n];
return (LL(F[n%p])*calc(n/_p,p,_p)%p*Pow(F[p-1],n/p,p)%p+p)%p;
}
int count(int n,int p)
{
if(n<p)return 0;
return n/p+count(n/p,p);
}
int C(int n,int k,int p,int _p,int t)
{
F[0]=1;
for(int i=1;i<=p;i++)
if(i%_p!=0)F[i]=LL(F[i-1])*i%p;else F[i]=F[i-1];
int t3=count(n,_p)-count(k,_p)-count(n-k,_p);
if(t3>=t)return 0;
LL t1=calc(n,p,_p),t2=calc(k,p,_p)*calc(n-k,p,_p)%p;
LL x,y;
ext_gcd(t2,p,x,y);
return (t1*x%p*Pow(_p,t3,p)+p)%p;
}
int work()
{
int res=0,tp,inv;
LL x,y;
for(int i=1;i<=tot;i++)
{
ext_gcd(p/c[i],c[i],x,y);
tp=C(n,k,c[i],a[i],t[i]);
res+=x*tp*(p/c[i])%p,res%=p;
}
return (res+p)%p;
}
int main()
{
T=get();
for(int i=1;i<=T;i++)
{
n=get(),k=get(),p=get();
ans=calc();
init(p);
ans=LL(ans)*work()%p;
printf("Case %d: %d
",i,ans);
}
return 0;
}