各種友(e)善(xin)數論總集,從入門到絕望5---組合數取模?(擴展)盧卡斯套上直接摁在地上錘

有些只是我個人看着像數論,其實不一定是數論

最近數論越來越短,只是單純因爲這個專題竟然只能選三個,更好的體驗果然還是要上博客園吧。

參考資料

說是資料,就是抄襲吧不過還是會做一些取捨,把一些我當初也不是很懂的東西做一些註解,可以說是加強版吧。

普通盧卡斯推導:https://www.luogu.com.cn/blog/28007/lucas

擴展盧卡斯推導:

普通盧卡斯定理

例題

萬能的洛谷

定義

沒有定義的話這篇證明還是挺難看的。

話說只是爲了更好的抄題解把。。。

一下定義僅適用於普通盧卡斯。

C(n,m,p)C(n,m,p)Cnmmod  pC_{n}^{m}\mod p,如果右邊的定義你都不會的話,你學什麼盧卡斯,先去把組合數學過一過吧,所以本文章幾乎所有內容均在同餘的意義下。

同時注意,C(n,m,p)(n>mC(n,m,p)(n>mn<1)n<1)在這篇證明中都算是00,即無意義,默認定理中的組合數都是有效的,證明中可能出現的無意義組合數請在代碼中特判爲00

做法

許多人也許會說,爲什麼不能爆搞逆元?

注意!有時候答案不一定是00,但是由於p<n,mp<n,m,所以在計算C(n,m,p)C(n,m,p)中分母的m!m!的逆元就會無解。

但是其實有個很有意思的事情,就是許多人都會發現其實是可以把分子分母的pp給全部提取出來的,但是最後都是直接去看題解了,這種其實也是一種做法,就是擴展盧卡斯定理!!!

先說結論吧:

對於C(n,m,p)C(n,m,p)

nn可以拆分爲:aipi+ai1pi1+...+a0a_{i}p^{i}+a_{i-1}p^{i-1}+...+a_{0}
mm也是,不過把aa改成bb。(bib_{i}00也要補上)

那麼C(n,m,p)=C(ai,bi,p)C(ai1,bi1,p)...C(n,m,p)=C(a_{i},b_{i},p)C(a_{i-1},b_{i-1},p)...

但是其實我最好奇的是爲什麼只要有一個上面的大於下面的就全部都爲0了,但又感覺證明沒什麼問題。。。

定理1:C(p,i,p)piCp1i1C(p,i,p)≡\frac{p}{i}C_{p-1}^{i-1}

C(p,i,p)p!i!(p1)!(p1)!(i1)!(pi)!pipiCp1i1C(p,i,p)≡\frac{p!}{i!(p-1)!}≡\frac{(p-1)!}{(i-1)!(p-i)!}\frac{p}{i}≡\frac{p}{i}C_{p-1}^{i-1}

定理2:(1+x)p1+xpmod  p(1+x)^p≡1+x^{p}\mod p

這個在我未來要寫的斐波那契循環節中也是特別重要的。。。

(1+x)p1+C(p,1,p)x+C(p,2,p)x2+...+xpmod  p(1+x)^p≡1+C(p,1,p)x+C(p,2,p)x^2+...+x^p\mod p

那麼關鍵就是:C(p,i,p)(0<i<p)C(p,i,p)(0<i<p)到底能不能被pp整除?

首先我們觀察這個式子:p!i!1(pi)!\frac{p!}{i!}*\frac{1}{(p-i)!}他又大又圓

我們可以發現因爲pp是質數,所以不會被除掉,所以就可以整除啦。

所以

(1+x)p1+xpmod  p(1+x)^p≡1+x^p\mod p

得證。

定理三:證明開頭說的那段話。簡單暴力的偷懶

設:n=ap+b,m=ip+j(0b,j<p)n=ap+b,m=ip+j(0≤b,j<p)

證明:C(n,m,p)C(ap,ip,p)C(b,j,p)C(n,m,p)≡C(ap,ip,p)*C(b,j,p),然後遞歸一下就可以得出結論。

然後模仿抄襲題解的就是:

(1+x)n=(1+x)ap(1+x)b(1+x)^{n}=(1+x)^{ap}(1+x)^{b}

展開(1+x)ap(1+x)^{ap}

(1+x)ap((1+x)p)a(1+xp)amod  p(1+x)^{ap}≡((1+x)^p)^a≡(1+x^p)^a\mod p

觀察(1+x)n(1+x)ap(1+x)b(1+xp)a(1+x)b(1+x)^n≡(1+x)^{ap}(1+x)^{b}≡(1+x^p)^a(1+x)^b

觀察第xmx^m項的係數(其實xmx^m的係數在模意義下就是C(n,m,p)C(n,m,p)):C(n,m,p)xmC(a,i,p)xpiC(b,j,p)xjC(n,m,p)x^m≡C(a,i,p)x^{pi}*C(b,j,p)x^j

C(n,m,p)xmC(a,i,p)C(b,j,p)xmC(n,m,p)x^m≡C(a,i,p)*C(b,j,p)x^m

所以:C(n,m,p)C(n%p,m%p,p)C(n/p,m/p,p)C(n,m,p)≡C(n\%p,m\%p,p)*C(n/p,m/p,p)

得證。

那麼其實代碼裏面也是拿這個去遞歸的。

這個證明我覺得特別優美,爲什麼,因爲在拆分成(1+x)n(1+x)ap(1+x)b(1+xp)a(1+x)b(1+x)^n≡(1+x)^{ap}(1+x)^{b}≡(1+x^p)^a(1+x)^bxmx^m的係數表示是兩個組合數相乘,沒有加號的,也就是利用ip+jip+j的表現形式。

當然,盧卡斯是O(logpn+p)O(log_{p}^{n}+p)級別的時間複雜度,還是很快的

#include<cstdio>
#include<cstring>
#define  N  110000
using  namespace  std;
typedef  long  long  LL;
LL  inv[N],jie[N]/**/;
LL  n,m,p;
inline  LL  C(LL  x,LL  y)//計算組合數
{
	if(x>y)return  0;
	return  (jie[y]*inv[jie[x]]*inv[jie[y-x]])%p;
}
inline  LL  Lucas(LL  x,LL  y)//盧卡斯遞歸過程
{
	if(!y)return  1;//y=0的別忘了特判
	return  (Lucas(x/p,y/p)*C(x%p,y%p))%p;
}
int  main()
{
	int  T;scanf("%d",&T);
	for(int  i=1;i<=T;i++)
	{
		scanf("%lld%lld%lld",&n,&m,&p);
		inv[0]=inv[1]=1;for(int  i=2;i<p;i++)inv[i]=(p-p/i)*inv[p%i]%p;
		jie[1]=1;for(int  i=2;i<=p;i++)jie[i]=jie[i-1]*i%p;//預處理逆元和階乘
		printf("%lld\n",Lucas(m,n+m));
	}
	return  0;
}

擴展盧卡斯

例題

又是萬能的洛谷

做法

這個做法其實也是很暴力的。

首先,pp不是質數,看起來很不友善,那我們就把他們化成質數嗎。

不過其實也講的不是很嚴謹,應該說是質數次冪。(爲了方便後面,其實也有因爲兩個相同的模數無法合併的問題。)

pp拆分成p1a1p2a2...piai(pjp_{1}^{a_{1}}p_{2}^{a_{2}}...p_{i}^{a_{i}}(p_{j}都是質數,1ji)1≤j≤i)

分別求出在模pjajp_{j}^{a_{j}}意義下的值,然後中國剩餘定理合併即可。

我們根據之前在盧卡斯之前提到過的。

在這裏,我們假設我們現在模的是pkp^k,那麼我們就把所有的pp移出來,然後就可以求逆元了。

前面提到的方便就是我們只需要把所有的pp移出來,然後剩下的數字絕對與pkp^k互質。

So,我們把式子化成了這個樣子(注意,後面的除以pp的幾次方表示全部提出來):C(n,m,pk)n!pxm!py(nm)!pzpxyzmod  pkC(n,m,p^k)≡\frac{\frac{n!}{p^x}}{\frac{m!}{p^y}\frac{(n-m)!}{p^z}}*p^{x-y-z}\mod p^k

對於爲什麼xyzx-y-z這個玩意沒可能小於00的話,ZFY奆佬告訴我們,前面那個分數的分子與pp互質,後面又全是pp,所以如果呈現的形式是1p\frac{1}{p}的話,就會變成分數,但是這玩意結果是整數,所以就是大於等於00的。(大霧

在這裏插入圖片描述

然後關鍵就是怎麼計算n!px\frac{n!}{p^x}以及快速返回xx。(分母也是這樣幹,然後逆元。)

pp的倍數的個數其實是很容易計算的,就是np\frac{n}{p}個。

爲了快速計算非pp倍數的乘法,這裏我們進行分組:

對於123...n1*2*3*...*n

把他們pkp^k一組一組的分。

然後對於長度爲pkp^k的一組(也就是滿的組),在mod  pk\mod p^k的以一下,其實他們的值是一樣的,都是1...(pk1)1*...*(p^k-1)(解釋一下,模運算裏面有個很神奇的性質,abc...(amod  p)bc...mod  pa*b*c*...≡(a\mod p)*b*c*...\mod p,然後就可以得到這個性質了)。

剩下的那一組就只能單獨計算了。

那麼其實仔細想想就是:

(i=1,(imod  p)>0pki)npk(i=npkpk+1,(imod  p)>0)ni)(\prod\limits_{i=1,(i\mod p)>0}^{p^k}i)^{\frac{n}{p^k}}*(\prod\limits_{i=\left \lfloor \frac{n}{p^k} \right \rfloor * p^k+1,(i\mod p)>0)}^{n}i)

但是其實由於提出了pp,我們前面還跟了一些東西,就是:

pnpnp!p^{\left \lfloor \frac{n}{p} \right \rfloor}\left \lfloor \frac{n}{p} \right \rfloor!

那麼其實我們前面的東西可以直接返回去,但是後面怎麼又有階乘?

那就遞歸求解嗎,但是也要把前面的pp次方傳上去。

So,分析時間複雜度:

首先,對於pkp^k,中間的求階乘和快速冪的部分,時間複雜度爲:O(pklog2npk)O(p^k*log_2\frac{n}{p^k})

由於後面的遞歸總和最多是等於第一次求階乘的,所以最多帶個22的常數。

對於pp而言,時間複雜度爲所有的pjajp_{j}^{a_{j}}的單詞時間複雜度相加,那麼記爲AA

我們採用擴展歐幾里得求逆元,用CRT合併,CRT的時間複雜度爲O(ilog2p)O(ilog_2p)

所以總的時間複雜度爲:O(A+ilogp)O(A+ilogp)。其實可以接近於認爲是O(plogn)O(plogn)

#include<cstdio>
#include<cstring>
using  namespace  std;
typedef  long  long  LL;
inline  LL  ksm(LL  x,LL  k,LL  mod)
{
	LL  now=1;
	while(k)
	{
		if(k&1)now=(now*x)%mod;
		x=(x*x)%mod;
		k>>=1;
	}
	return  now;
}
inline  LL  jc(LL  l,LL  r,LL  x,LL  mod)//求階乘
{
	LL  ans=1;
	for(LL  i=l;i<=r;i++)
	{
		if(i%x!=0)ans=ans*(i%mod)%mod;
	}
	return  ans;
}
inline  void  exgcd(LL  a,LL  b,LL  &x,LL  &y)//求逆元
{
	if(!a)return  (void)(x=0,y=1);
	exgcd(b%a,a,x,y);
	LL  tmp=x;x=y-(b/a)*x;y=tmp;
}
LL  gcd(LL  x,LL  y)//這個函數好像一直沒有用到。
{
	if(!x)return  y;
	return  gcd(y%x,x);
}
inline  LL  inv(LL  x,LL  mod)//求逆元 
{
	LL  a,b;
	exgcd(x,mod,a,b);
	/*b=-b,因爲mod其實是負數形式*/
	a=(a%mod+mod)%mod;
	return  a;
}//返回逆元
LL  calc(LL  n,LL  p,LL  mod,LL  &now)//表示計算(x!/P^now)%P^y
{
	if(!n)return  1;
	now+=n/p;//加上P的次冪 
	LL  ans=calc(n/p,p,mod,now);//先得到階乘;
	return  (ans*ksm(jc(1,mod,p,mod),n/mod,mod)%mod)*jc((n/mod)*mod+1,n,p,mod)%mod;
}
inline  LL  C(LL  x,LL  y,LL  p,LL  mod)//C_x^y
{
	LL  n=0,m=0;LL  ans=calc(x,p,mod,m);n+=m;m=0;
	ans*=inv(calc(y,p,mod,m),mod);ans%=mod;n-=m;m=0;
	ans*=inv(calc(x-y,p,mod,m),mod);ans%=mod;n-=m;m=0;
	return  ans*ksm(p,n,mod)%mod;
}
inline  LL  fen(LL  x,LL  a1,LL  a2)//分解質因數
{
	LL  y=2;
	LL  nmo=1,ans=0;
	while(y<=x)
	{
		if(x%y==0)
		{
			LL  z=1;
			while(x%y==0)z*=y,x/=y;
            //z爲模數,y爲質數,也是ZFY奆佬教給我的一個分解質因數的方法
			LL  now=C(a1,a2,y,z);
			LL  zong=z*nmo;
			ans=(ans*z*inv(z,nmo)+now*nmo*inv(nmo,z))%zong;
			nmo=zong;
            //兩個同餘式合併,因爲彼此互質,所以可以用比較簡單的CRT。
		}
		y++;
	}
	return  ans;
}
int  main()
{
//	freopen("std.in","r",stdin);
//	freopen("vio.out","w",stdout);
	LL  n,m,p;scanf("%lld%lld%lld",&n,&m,&p);
	printf("%lld\n",fen(p,n,m));
	return  0;
}

一種新式的奇技淫巧

最近剛剛學習的一種神仙的奇技淫巧。

就是叫你計算Cxymod  paC_{x}^y\mod p^a

pp一般比較小,但是pap^a可以在long long範圍內,x,yx,y也是。

這種奇技淫巧放在擴展盧卡斯定理中就可以解決質因數比較小的long long範圍的質數了。

那麼到底是個什麼神仙東西呢?

例題

沒有鏈接,就是叫你求Cxymod  523C_{x}^{y}\mod 5^{23}

這個是某個噁心集訓隊互測的一種噁心的子問題。那道題DP很好,非要搞一個組合數取模

思路

這種神奇的思想依舊是按照算出除55以外的階乘與55的個數。

但是特別神奇的是他在算階乘的時候採用的是樹套樹套樹套樹拆括號。

沒錯就是小學的拆括號,但是這個拆括號極其有技術含量。

我們選擇一個252*5(後面就知道爲什麼要乘22了)的倍數作爲基數。

題解裏使用的是1010(其實是10的倍數也可以,但是取最小的一般常數是最小的),我們現在要計算n!n!,那麼我們設k=n1010k=\left \lfloor \frac{n}{10} \right \rfloor *10k+1k+1~nn的我們暴力算,也就在1010以內。但是關鍵是k!k!呢?

一種也同樣明顯隱藏的思路就是把kk分成兩份。

1(k/2)1-(k/2)(k/2+1)k(k/2+1)-k,然後類似分治的思想。

這也就解釋了爲什麼基數要選擇偶數了。

假設我們知道了1(k/2)1-(k/2),但是怎麼求出(k/2+1)k(k/2+1)-k,我們列出括號(設o=k2o=\frac{k}{2},容易知道:5o5|ooo55整除)):(1+o)(2+o)(3+o)...(o+o)(1+o)*(2+o)*(3+o)*...*(o+o),由於是5235^{23}次冪,在選的時候最多隻能選2222oo

仔細觀察,發現o!o!我們是知道的,但是oio^i的係數我們是不知道的。

注意到我說了oio^i的係數,明白人都發現那不是生成函數(自行學習吧)嗎!!!

也就是說對於oio^i的係數我們就需要知道在1o1-o中選oio-i個不等的數字的乘積,而0i220≤i≤22

所以我們只要接着構造出1o1-o的生成函數就行了,也就是(1+x)(2+x)(3+x)...(o+x)(1+x)*(2+x)*(3+x)*...*(o+x),所以問題就成了通過1o1-o的生成函數的值求(o+1)k(o+1)-k的生成函數的值,而(o+1)k(o+1)-k的生成函數爲(o+1+x)(o+2+x)...(k+x)(o+1+x)*(o+2+x)*...*(k+x)

等會,生成函數?難道是FFT,不不不,由於0i220≤i≤22,所以我們只要記錄第0220-22項,所以直接暴力n2n^2多項式乘法,不就可以了嗎。

所以我們只需要對於xix^i的係數乘以oio^i加到(o+1)k(o+1)-k的常數項中(即階乘值),等會,那其他的(o+1)k(o+1)-k其他的項你不理他了?還是要理會的,對於xix^i的係數我們要轉化到(o+1)k(o+1)-kxj(ij)x^j(i≥j)的係數,我們就需要在ii11中挑選iji-j個變成oo加入到xjx^j的係數中
,所以不僅要乘oijo^{i-j},還要乘CiijC_{i}^{i-j}

關於爲什麼挑選了oo就是非xx項?

把式子展開不難發現,oo其實是在括號內的常數項的,只不過把xx11係數變成了常數項的係數的一部分(這個過程可以說是把多個值融合成結果)。

所以就成了。

題解中可能爲了減少特判,還直接把1100001-10000的生成函數的值全部求了!!!

當然55的個數就在遞歸中慢慢算了,注意,上面的全部過程都是把55的倍數拿了出來,所以請自行遞歸算n5!\left \lfloor \frac{n}{5} \right \rfloor!

代碼在這裏:

LL  mod=(LL)11920928955078125;
inline  LL  mul(LL  x,LL  y){return  (x*y-(LL)((long  double)x*y/mod+1e-10)*mod);}
inline  LL  pow(LL  x,LL  k)//求逆元專用 
{
	LL  ans=1;
	while(k)
	{
		if(k&1)ans=mul(x,ans);
		x=mul(x,x);k>>=1;
	}
	return  ans;
}
namespace  Big_num//大整數組合數 
{
	LL  pw[S]={1},C[S][S],K/*表示選出幾個集合*/;
	struct  poly//沒錯,這個向量,他又來了
	{
		LL  a[S];
		poly(LL  x=0,LL  y=0){memset(a,0,sizeof(a));a[0]=x;a[1]=y;}
		void  init(LL  k)
		{
			static  LL  ret[S];memset(ret,0,sizeof(ret)); 
			for(int  i=1;i<S;i++)pw[i]=mul(pw[i-1],k);
			for(int  i=0;i<S;i++)
			{
				for(int  j=0;j<=i;j++)ret[j]=(ret[j]+mul(a[i],mul(pw[i-j],C[i][j/*從不選的裏面挑出幾個*/])))%mod;
			}
			memcpy(a,ret,sizeof(ret));
		}
		poly  operator*(poly  x)
		{
			poly  z;
			for(int  i=0;i<S;i++)
			{
				if(x.a[i])//打不打無所謂
				{
					for(int  k=i;k<S;k++)z.a[k]=(z.a[k]+mul(x.a[i],a[k-i]))%mod;//就是這裏的推導
				}
			}
			return  z;
		}
	}P[10005];
	//-----------poly
	poly  facpoly(LL  n)//求階乘 
	{
		if(n<=10000)return  P[n];
		LL  k=n/10*10;
		poly  t1=facpoly(k>>1),t2=t1;
		t2.init(k>>1);//用一個生成函數求另外一個生成函數的過程
		t1=t1*t2;
		for(LL  i=k+1;i<=n;i++)
		{
			if(i%5!=0)t1=t1*poly(i,1);
		}
		return  t1;
	}
	LLp  solve(LL  n)//遞歸
	{
		LLp  ret=make_pair(facpoly(n).a[0],n/5);
		if(n>=5)
		{
			LLp  tmp=solve(n/5);
			ret.first=mul(ret.first,tmp.first);
			ret.second+=tmp.second;
		}
		return  ret;
	}
	LL  Combk(LL  n)
	{
		if(n<K)return  0;
		LLp  f1=solve(n),f2=solve(K),f3=solve(n-K);
		f1.second-=f2.second+f3.second;
		return  mul(mul(f1.first,pow(mul(f2.first,f3.first),mod/5*4-1)),pow(5,f1.second));
	}
	void  Init()
	{
		C[0][0]=1;
		for(int  i=1;i<S;i++)
		{
			C[i][0]=1;
			for(int  j=1;j<=i;j++)
			{
				C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
			}
		}
		P[0]=poly(1,0);
		for(int  i=1;i<=10000;i++)
		{
			if(i%5!=0)P[i]=P[i-1]*poly(i,1);
			else  P[i]=P[i-1];
		}
	}
};

然後這個玩意再拿擴展中國剩餘定理合併,就可以處理pp在long long範圍的情況了,不過最大的質因子要小一點。

但對於pap^a而言(pp是質數),且基數選擇爲2p2p,時間複雜度爲:O((a2+p)log2n)O((a^2+p)\log^2{n}),這個複雜度pp大了受不了的(雖然pp大了其中一個loglog幾乎就是常數了)。

小結

好像沒什麼好說的。

發佈了75 篇原創文章 · 獲贊 17 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章