數論算法 Plus


好像有不少更新:)
本文主要記錄一些不是那么熟悉的高級數論算法的推導與應用。

exBSGS算法

解決模數、底數不互質的離散對數問題。

(1)為何\(BSGS\)算法不再適用:\(A\)不一定存在逆元,而且無法保證解的循環性。
(2)無解的結論:
設方程為\(A^x=B \pmod{P}\)
\((A,P) \nmid B\)\(B\ne 1\) 時,原方程無自然數解。
還有就是\(A=0,B≠0\)這種。
(3)算法流程:
先判無解。
然后若\(B=1\),顯然\(x=0\),特判之。
否則 \(G=(A,P)\)
\(G>1\)
兩邊同除以\(G\),得\(A^{x-1}=\frac{B}{G}*(\frac{A}{G})^{-1} \pmod{\frac{P}{G}}\)
迭代至\((A,P)==1\)時即可套用\(BSGS\)算法了。

int BSGS(int a,int b,int P)
{
    int s,o;
    S.clear();
    int m=(int)ceil(sqrt(P));
    o=a;
    s=b;
    for(int i=1; i<=m; i++)
    {
        s=(ll)s*o%P;
        S[s]=i;
    }
    o=fpow(a,m,P);
    s=1;
    for(int i=1; i<=m; i++)
    {
        s=(ll)s*o%P;
        if((it=S.find(s))!=S.end())
            return i*m-it->second;
    }
    return -1;
}

int exbsgs(int a,int b,int c)
{
    a%=c; b%=c;
    if(b==1||c==1)return 0;
    if(!a)return b?-1:1;
    int d=gcd(a,c);
    if(d==1)return BSGS(a,b,c);
    if(b%d!=0)return -1;
    int o=exbsgs(a,(ll)b/d*Inv(a/d,c/d)%(c/d),c/d);
    return o==-1?-1:o+1;
}

快速計算階乘

即快速計算:
對於質數\(p\),把\(N!\)去掉所有\(p\)因子的部分對\(p^e\)取模,\(p^e\le 10^5\)
(1)為了便於統計出現了多少個p的次冪,先將階乘中所有p的倍數提出來。可以簡單算出:共有
$\displaystyle \lfloor \frac{n}{p} \rfloor $ 個。
這中間每一項都除去p,
可以得到 \(\displaystyle \lfloor \frac{n}{p} \rfloor !\) 。該部分可以選擇遞歸求解。
(2)那么接下來只剩下非p的倍數的幾項了。通過簡單觀察可以知道,剩余幾項具有循環節,循環節長度小於\(p^e\)
原因是剩余項關於p具有循環節,而
\(x+p^e \equiv x \pmod{p^e}\)
所以可以一起計算。
結果會剩下幾項湊不齊一個循環節,但是這幾項長度已經小於\(p^e\)了,可以選擇暴力乘法求解。
有必要舉個例子模擬一下:

\(N=22,p=3,e=2,\)
\(22!=1*2*3*......*22\)
\(=(1*2*4*5*7*8*10*11*13*14*16*17*19*20*22)*3^7*7!\)
\(7!\)遞歸處理;

\((1*2*4*5*7*8*10*11*13*14*16*17*19*20*22)\)
\(\equiv (1*2*4*5*7*8)^2*(19*20*22) \pmod {3^2}\)
兩個括號里的暴力計算;
考慮對 \(p^e\) 以內做預處理,查詢復雜度可以做到 \(O(\log_p n)\) 左右。
代碼在跟下面一起貼

組合數取模(擴展盧卡斯)

即快速計算\(\dbinom{N}{M} \pmod{P}\)\(N,M\le 10^9,P不一定是質數,P\le 10^9\),但\(P\)中任何一個質因子的總積不超過\(10^5\)

考慮中國剩余定理:
\(Ans=\dbinom{N}{M}\)

對質因子分開計算有:
\(Ans \equiv a_1 \pmod{p_1^{q_1}}\)
\(Ans \equiv a_2 \pmod{p_2^{q_2}}\)
\(......\)
對於一個式子:
不含\(p_i\)的部分使用上述快速階乘方法求解。
含有\(p_i\)的重數使用\(\displaystyle \sum_{i=1} \lfloor \frac{n}{p^i} \rfloor\)計算。
然后兩部分分開算,乘起來就是對應的\(a_i\)

最后CRT合並起來就可以了。

\[Ans\equiv \sum a_i \times (\prod_{j\neq i}b_j^{-1} \bmod {b_i}) \times \prod_{j\neq i}b_j \pmod {\prod b_i} \]

struct bnm {
	int p, Mod, fac[1 << 20];
	void init(int x, int y)
	{
		p = x;
		Mod = y;
		fac[0] = 1;
		for(int s = 1; s != Mod; ++s)
			if(s % p)
				fac[s] = 1LL * fac[s - 1] * s % Mod;
			else
				fac[s] = fac[s - 1];
	}
	int Fac(ll x)
	{
		if(!x)
			return 1;
		return 1LL * Fac(x / p) * fexp(fac[Mod - 1], x / Mod, Mod) % Mod * fac[x % Mod] % Mod;
	}
	int operator()(ll n, ll m) {
		if(n < m || m < 0) return 0;
		int res = Fac(n) * Inv(Fac(m), Mod) % Mod * Inv(Fac(n - m), Mod) % Mod;
		int z = 0;
		for(ll d = n; d; z += d /= p);
		for(ll d = m; d; z -= d /= p);
		for(ll d = n - m; d; z -= d /= p);
		res = 1LL * res * fexp(p, z, Mod) % Mod;
		return res;
	}
}Solve;

普通二次剩余

即解方程\(x^2=a \pmod{P}\),P是質數。

(1)勒讓德符號的定義
\((\frac{a}{p})=1\):a在模p意義下有二次剩余。
\((\frac{a}{p})=-1\):a在模p意義下無二次剩余。
計算公式:\((\frac{a}{p}) = a^{\frac{p-1}{2}} \pmod p\)
(2)求解二次剩余
隨機一個\(a\)使得\(b=\sqrt{a^2-n}\)(此時 \(a^2-n\) 無二次剩余,就直接寫成根號形式),然后把\(b\)作為二次域的單位元。
那么\(x=(a+b)^{\frac{p+1}{2}}\)(證明需要二項式定理展開,具體參考這里 !ACdreamer
寫一個擴域乘法類就好了。
注意這里\(p\)得是奇質數,但\(p=2\)也是很容易特判的。
二次剩余有一正一負兩解,下面代碼隨便取了一個。

namespace Cipolla
{
	inline int Le(int x){return fpow(x,(P-1)/2);}
	
	int w;
	struct Cp
	{
		int x,y;
		Cp(int a=0,int b=0){x=a;y=b;}
	};
	Cp operator+(Cp a,Cp b){return Cp((a.x+b.x)%P,(a.y+b.y)%P);}
	Cp operator-(Cp a,Cp b){return Cp((a.x-b.x+P)%P,(a.y-b.y+P)%P);}
	Cp operator*(Cp a,Cp b){return Cp(((ll)a.x*b.x+(ll)w*a.y%P*b.y)%P,((ll)a.x*b.y+(ll)a.y*b.x)%P);}
	Cp operator^(Cp a,int b){
		Cp o(1,0);
		for(;b;b>>=1) {
			if(b&1)o=o*a;
			a=a*a;
		}
		return o;
	}
	
	int calc(int x)
	{
		x%=P;
		int a;
		while(1) {
			a=rand();
			w=((ll)a*a-x+P)%P;
			if(Le(w)==P-1) break;
		}
		Cp s=Cp(a,1)^((P+1)/2);
		return min(s.x,P-s.x);
	}
}

原根和階

奇素數與奇素數的方冪有原根,原根 \(g\) 是階恰好等於 \(p-1\) 的數,也就是 \(g^x(x\in [0,p-2])\) 與整數 \(y\in [1,p-1]\) 一一對應。
我們一般用暴力枚舉的方法求原根,原理類似試除法,判斷是否存在 \(d|p-1\) 使得 \(g^d\equiv 1 \pmod p\) 即可。

bool judge(int x)
{
	for(int j = 0; j < tot; j ++) //只需對質因子做
		if(fexp(x, (P - 1) / c[j], P) == 1)
			return false;
	return true;
}

對一個任意模數求階也不是很難,只要對 \(\varphi (m)\) 不斷試除保證 \(x^k\equiv 1 \pmod m\)即可。

原根可以轉換為單位根,即若 \(n|(\varphi(p)-1)\)\(\omega_n=g^{\frac{\varphi(p)-1}{n}}\)
常用於單位根反演 \([n|i]=\dfrac{1}{n}\sum_{k=0}^{n-1} \omega_n^{ki}\)
單位根反演可以應用於 \(\text{NTT}\) 以及結合“二項式定理”優化復雜度。

快速乘法取模

某些情況下機器不支持int128,或者毒瘤出題人卡int128常數,就需要寫這種 \(O(1)\) 快速乘。

inline ll mul(ll a,ll b,ll p){
	ll res=a*b-((ll)((long double)a*b/p+0.5))*p;
	return res<0?res+p:res;
}

一定要保證 \(a,b\lt p\) 的時候計算,因為它是由這個保證精度的。

合並同余方程組

\(x \equiv now \pmod m\)
\(x \equiv a \pmod b\)

由第一式得
\(x=now+k*m\)
代入二式
\(now+km \equiv a \pmod b\)
\(mk \equiv a-now \pmod b\)
拿個擴歐就能解出來最小的\(k\)
然后\(now=now+k\times m\)

有一個坑點就是你的\(k\)不要對\(m\)取模,
歸納易證你這個\(now\)一定是最小的正整數解,只要題目保證了啥啥,\(now\)就肯定不會溢出。

但是好多題目中\(m\)都會溢出,變成負數,再取模就會出bug。

當然還是不能排除求出假答案的情況,最后可以 check 一遍。

void excrt(ll a,ll b)
{
    ll c=((a-now)%b+b)%b;
    ll x,y;
    ll d=exgcd(M,b,x,y);
    if(c%d!=0) err();
    c/=d;
    b/=d;
    x=(x%b+b)%b;
    x=(__int128)x*c%b;
    now+=x*M;
    M*=b;
}

Miller-Rabin素數測試

二次探測原理:
當p是質數時,則一定有

\(x^2 \equiv 1 \pmod p\),則\(x=1 \pmod p\)\(x=-1 \pmod p\)

這樣我們就考慮反過來想辦法驗證\(n\)是質數。

\(n=2^k \times m + 1\)(m是奇數),

拿個小質數來進行二次探測,進行\(k\)次迭代即可。
取10-12個質數就足夠穩了。

代碼21行是二次探測,25行是費馬小定理。

int ispr(ll n)
{
    if(n<2)
        return 0;
    for(int i=0; i<12; i++)
        if(n%Pr[i]==0)
            return n==Pr[i];
    ll m=n-1,x,y;
    int k=0;
    while(~m&1)
    {
        m>>=1;
        k++;
    }
    for(int i=0; i<12; i++)
    {
        x=fpow(Pr[i],m,n);
        for(int j=0; j<k; j++)
        {
            y=Mul(x,x,n);
            if(y==1&&x!=1&&x!=n-1)
                return 0;
            x=y;
        }
        if(x!=1)
            return 0;
    }
    return 1;
}

質因數分解 Pollard-rho

先說一下為大整數 \(N(N\le 10^{18})\) 分解質因子的流程:

(1)弄出來一個 \(N\) 的非平凡因子 \(x\)
(2)遞歸分解 \(x\)\(N/x\)

而Pollard-rho就是高效地尋找非平凡因子的算法。

Pollard-rho基於隨機化實現,然后算法的效率保證是“生日悖論”,這里的含義如下:

如果隨機一個數 \(x\),那么 \(x\)\(N\) 約數的概率極其微小。
但是我們如果隨機出 \(x,y\),那么 \(|x-y|\)\(N\) 約數的概率將顯著提高。

(1)不斷隨機數字\(x\),假如\(gcd(x,n)>1\)直接返回這個\(gcd\)

額樣例都跑不出來,沒有任何前途。

(2)按照上面講的這么寫,並按照倍增的形式不斷調整:

x=y=0;
for(int k=2; ; k<<=1)
{
    for(int i=1; i<=k; i++)
    {
        x=(x*x+sd)%n;
        v=gcd(abs(x-y),n);
        if(v!=1 && v!=n)
            return v;
        if(x==y)
            return n;
    }
    y=x;
}

效率玄學地大幅提升,但是依然TLE飛起。。。
(3)然而你發現這個gcd常數巨大,我們把改成用一個模 \(N\) 意義下的變量記一下,這樣每隔一會查一次就行!

static int Size(1<<7);
x=y=0;
for(int k=2; ;k<<=1)
{
    v=1;
    y=x;
    for(int i=1; i<=k; i++)
    {
        x=((u128)x*x+sd)%n;
        v=((u128)v*_abs(x-y))%n;
        if(i%Size==0)
        {
            z=gcd(v,n);
            if(z>1) return z;
        }
    }
    z=gcd(v,n);
    if(z>1) return z;
}

就能過了,常數極小。
Pollard-rho時間復雜度是\(O(n^{0.25})\)
實測每次找質因子內層乘法能穩定在\(10^5\)之內。
其實感覺特別暴力。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM