有些只是我個人看着像數論,其實不一定是數論
最近數論越來越短,只是單純因爲這個專題竟然只能選三個,更好的體驗果然還是要上博客園吧。
參考資料
說是資料,就是抄襲吧不過還是會做一些取捨,把一些我當初也不是很懂的東西做一些註解,可以說是加強版吧。
普通盧卡斯推導:https://www.luogu.com.cn/blog/28007/lucas
擴展盧卡斯推導:
普通盧卡斯定理
例題
定義
沒有定義的話這篇證明還是挺難看的。
話說只是爲了更好的抄題解把。。。
一下定義僅適用於普通盧卡斯。
設爲,如果右邊的定義你都不會的話,你學什麼盧卡斯,先去把組合數學過一過吧,所以本文章幾乎所有內容均在同餘的意義下。
同時注意,和在這篇證明中都算是,即無意義,默認定理中的組合數都是有效的,證明中可能出現的無意義組合數請在代碼中特判爲。
做法
許多人也許會說,爲什麼不能爆搞逆元?
注意!有時候答案不一定是,但是由於,所以在計算中分母的的逆元就會無解。
但是其實有個很有意思的事情,就是許多人都會發現其實是可以把分子分母的給全部提取出來的,但是最後都是直接去看題解了,這種其實也是一種做法,就是擴展盧卡斯定理!!!
先說結論吧:
對於
設可以拆分爲:
也是,不過把改成。(爲也要補上)
那麼
但是其實我最好奇的是爲什麼只要有一個上面的大於下面的就全部都爲0了,但又感覺證明沒什麼問題。。。
定理1:
定理2:
這個在我未來要寫的斐波那契循環節中也是特別重要的。。。
那麼關鍵就是:到底能不能被整除?
首先我們觀察這個式子:他又大又圓
我們可以發現因爲是質數,所以不會被除掉,所以就可以整除啦。
所以
得證。
定理三:證明開頭說的那段話。簡單暴力的偷懶
設:
證明:,然後遞歸一下就可以得出結論。
然後模仿抄襲題解的就是:
展開
觀察
觀察第項的係數(其實的係數在模意義下就是):
所以:
得證。
那麼其實代碼裏面也是拿這個去遞歸的。
這個證明我覺得特別優美,爲什麼,因爲在拆分成,的係數表示是兩個組合數相乘,沒有加號的,也就是利用的表現形式。
當然,盧卡斯是級別的時間複雜度,還是很快的
#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;
}
擴展盧卡斯
例題
做法
這個做法其實也是很暴力的。
首先,不是質數,看起來很不友善,那我們就把他們化成質數嗎。
不過其實也講的不是很嚴謹,應該說是質數次冪。(爲了方便後面,其實也有因爲兩個相同的模數無法合併的問題。)
把拆分成都是質數,
分別求出在模意義下的值,然後中國剩餘定理合併即可。
我們根據之前在盧卡斯之前提到過的。
在這裏,我們假設我們現在模的是,那麼我們就把所有的移出來,然後就可以求逆元了。
前面提到的方便就是我們只需要把所有的移出來,然後剩下的數字絕對與互質。
So,我們把式子化成了這個樣子(注意,後面的除以的幾次方表示全部提出來):
對於爲什麼這個玩意沒可能小於的話,ZFY奆佬告訴我們,前面那個分數的分子與互質,後面又全是,所以如果呈現的形式是的話,就會變成分數,但是這玩意結果是整數,所以就是大於等於的。(大霧
然後關鍵就是怎麼計算以及快速返回。(分母也是這樣幹,然後逆元。)
是的倍數的個數其實是很容易計算的,就是個。
爲了快速計算非倍數的乘法,這裏我們進行分組:
對於
把他們一組一組的分。
然後對於長度爲的一組(也就是滿的組),在的以一下,其實他們的值是一樣的,都是(解釋一下,模運算裏面有個很神奇的性質,,然後就可以得到這個性質了)。
剩下的那一組就只能單獨計算了。
那麼其實仔細想想就是:
但是其實由於提出了,我們前面還跟了一些東西,就是:
那麼其實我們前面的東西可以直接返回去,但是後面怎麼又有階乘?
那就遞歸求解嗎,但是也要把前面的次方傳上去。
So,分析時間複雜度:
首先,對於,中間的求階乘和快速冪的部分,時間複雜度爲:。
由於後面的遞歸總和最多是等於第一次求階乘的,所以最多帶個的常數。
對於而言,時間複雜度爲所有的的單詞時間複雜度相加,那麼記爲。
我們採用擴展歐幾里得求逆元,用CRT合併,CRT的時間複雜度爲。
所以總的時間複雜度爲:。其實可以接近於認爲是。
#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;
}
一種新式的奇技淫巧
最近剛剛學習的一種神仙的奇技淫巧。
就是叫你計算
一般比較小,但是可以在long long範圍內,也是。
這種奇技淫巧放在擴展盧卡斯定理中就可以解決質因數比較小的long long範圍的質數了。
那麼到底是個什麼神仙東西呢?
例題
沒有鏈接,就是叫你求
這個是某個噁心集訓隊互測的一種噁心的子問題。那道題DP很好,非要搞一個組合數取模
思路
這種神奇的思想依舊是按照算出除以外的階乘與的個數。
但是特別神奇的是他在算階乘的時候採用的是樹套樹套樹套樹拆括號。
沒錯就是小學的拆括號,但是這個拆括號極其有技術含量。
我們選擇一個(後面就知道爲什麼要乘了)的倍數作爲基數。
題解裏使用的是(其實是10的倍數也可以,但是取最小的一般常數是最小的),我們現在要計算,那麼我們設,~的我們暴力算,也就在以內。但是關鍵是呢?
一種也同樣明顯隱藏的思路就是把分成兩份。
和,然後類似分治的思想。
這也就解釋了爲什麼基數要選擇偶數了。
假設我們知道了,但是怎麼求出,我們列出括號(設,容易知道:(被整除)):,由於是次冪,在選的時候最多隻能選個
仔細觀察,發現我們是知道的,但是的係數我們是不知道的。
注意到我說了的係數,明白人都發現那不是生成函數(自行學習吧)嗎!!!
也就是說對於的係數我們就需要知道在中選個不等的數字的乘積,而。
所以我們只要接着構造出的生成函數就行了,也就是,所以問題就成了通過的生成函數的值求的生成函數的值,而的生成函數爲。
等會,生成函數?難道是FFT,不不不,由於,所以我們只要記錄第項,所以直接暴力多項式乘法,不就可以了嗎。
所以我們只需要對於的係數乘以加到的常數項中(即階乘值),等會,那其他的其他的項你不理他了?還是要理會的,對於的係數我們要轉化到的的係數,我們就需要在個中挑選個變成加入到的係數中
,所以不僅要乘,還要乘。
關於爲什麼挑選了就是非項?
把式子展開不難發現,其實是在括號內的常數項的,只不過把的係數變成了常數項的係數的一部分(這個過程可以說是把多個值融合成結果)。
所以就成了。
題解中可能爲了減少特判,還直接把的生成函數的值全部求了!!!
當然的個數就在遞歸中慢慢算了,注意,上面的全部過程都是把的倍數拿了出來,所以請自行遞歸算。
代碼在這裏:
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];
}
}
};
然後這個玩意再拿擴展中國剩餘定理合併,就可以處理在long long範圍的情況了,不過最大的質因子要小一點。
但對於而言(是質數),且基數選擇爲,時間複雜度爲:,這個複雜度大了受不了的(雖然大了其中一個幾乎就是常數了)。
小結
好像沒什麼好說的。