1. 程式人生 > 實用技巧 >快速冪+龜速乘+費馬小定理+逆元+矩陣乘法

快速冪+龜速乘+費馬小定理+逆元+矩陣乘法

我是這個機房最菜的

我今天覆習的是:

王者吃雞CF,上分小隊等你來

扯遠了,接下來才是乾貨

快速冪+慢速乘+費馬小定理+逆元+矩陣乘法(講錯了還請笑的收斂點

本來太蒻,所以快速冪,慢速乘,費馬小定理沒有找到合適的例題,逆元和矩陣乘法的例題也不多而且不難

快速冪

說到求幾次方,我們不難想到\(pow()\)這個函式,但是這個函式不僅不快而且還有坑(某位大佬曾因這個在省選翻車了),用\(pow()\)切記要注意\(pow()\)返回\(double\)型別的數

於是我們便要xiao習一下更快而且沒那麼容易鍋的\(ksm\)這不有手就行嘛

基本思路:

1)當\(b\)是奇數時,那麼有 \(a^b = a * a^{b-1}\)

2)當\(b\)是偶數時,那麼有 \(a^b = a^{b/2} * a^{b/2}\)

時間複雜度: \(O(log\ n)\)

程式碼實現:

//遞迴寫法
int ksm(int x, int y, int mod){//求x的y次方(在%m的意義下) 
    if(y == 0) return 1;
    if(y % 2 == 0){
        int num = ksm(x ,y / 2) % mod;//防止計算兩次,節省時間   
        return num * num % mod;
    }
    else return ksm(x, y - 1) * x % mod;
} 
//while寫法
int ksm(int x, int y){
	int res = 1;
	while(y){
		if(y & 1) res =(res * x) % mod ; 
		x = x * x % mod;
		y >>= 1;
	}
	return res % mod;
}

優化:

if(y%2==1)
   等效於
if(y&1)

因為y&1​是按位與,判斷y的末尾是否為1因此當y為奇數時y&1返回為1,if條件成立,這樣執行速度更快

注意:

針對不同的題目,有兩個細節需要注意

1)如果初始值x 大於 m ,那麼需要在進入函式前就讓x對m取模,

2)若果m 為 1,可以直接在函式外部特判為 0,不需要進入函式來計算。(因為任何數對1 取模都是0。

例題水題

P3197 越獄

這道題相信許多人都已經

這道題的核心柿子是$$m^{n}-m*(m-1)^{n-1}$$

至於為什麼呢?這似乎不屬於我講的範圍

但我們考慮資料範圍

對於100%的資料,保證\(1<=m<=1e8, 1<=n<=1e12\)

這個資料求個\(m^{n}\)\(m*(m-1)^{n-1}\)就很容易炸了吧

然後我們就可以想到\(ksm\)

沒錯,就是這東西

long long ksm(long long x, long long y){
	long long res = 1;
	while(y){
		if(y & 1) res =(res * x) % mod ; 
		x = x * x % mod;
		y >>= 1;
	}
	return res % mod;
}

慢速乘(俄羅斯農民乘法)

由來:

計算機很笨當然不是了,乘法很慢,還動不動就溢位,於是慢速乘出現了。

在很久很久以前。。。人們是這樣做乘法的。。。這是一種古老的乘法演算法,但是在如今的計算機中卻還能發現它的身影。它,就是以俄羅斯農民命名的乘法演算法!

我們的慢速乘(龜速乘)主要是為了防止乘法溢位

原理聽我口胡

假設我們現在有兩個數\(x\)\(y\),我們要得到\(x*y\),如果給你的資料是

$ 1 <= x,y <= 1000000 $

這完美的炸掉了\(int\)

肯定有聰明的童鞋說道:你開個 $ long\ long$不就完了嗎?

就這?就這,我果然很菜

如果再大一點點的話肯定有孩紙說\(unsigned\ long\ long\)就好了

這不有手就行嘛

我先扶一下自己的假肢(手動滑稽

那如果這樣呢

\(1 <= x,y <= 1000000000000000\)

這種毒瘤咋辦呢???

在這就發揮出了慢速乘的作用了

那慢速乘是怎麼實現的呢?

我們可以把\(x,y\)看成二進位制下的乘

我們把\(y\)拆成好多個2來解決,邊乘邊模

是不是頓悟了許多呢!!!

程式碼實現:

long long mul(long long a, long long b){
	long long res = 0;
	while(b){
		if(b & 1) res += a;
		a <<= 1;
		b >>= 1;
	}
	return res;
}

切記切記切記!!!

這裡是 a <<= 1;
而不是 a *= a;

emmmm至於例題嘛

本人太菜了,沒找到合適的例題(大家都會敲慢速乘就好了

費馬小定理

沒錯,費馬小定理就是這個人提出來的雖然百度百科給ta的職業定位是律師和業餘數學家

雖然費馬小定理還有個哥哥:費馬大定理(又名費馬最後的定理

他斷言當整數n >2時,關於x, y, z的方程 $$x^n + y^n = z^n$$ 沒有正整數解。

當然我們也不需要記住它,因為我們要學的是費馬小定理

主要內容就是:

如果p是一個質數,而整數a不是p的倍數,則有$$a^{p-1}≡1\ (mod\ p)$$

滿足費馬小定理的條件就是: \(p\)為質數, 且\(a\)\(p\)互質

你們想看證明嗎我知道其實你們不想

但還是得講呀~~其實跳過去也闊以

證明:

宣告:
  • 我不是用尤拉函式證明的,我怕講了別人準備的尤拉函式,所以用另一種簡單的方式去證明

(如果你們想看尤拉函式證明的戳這裡

emmmm....有點突發狀況 ,尤拉定理我也講,費馬小定理的尤拉定理的證明在補充的位置

  • 在證明之前我們先給出兩個引理(請抓好扶好,前方有些曲折

  • 還有個很重要的事情就是:切記!!!取模運算對於加法、減法、乘法都封閉(可以參考最後的補充部分)


<一>​

引理<一>:

假設\(p\)\(c\)互質,且存在\(a*c\equiv b*c\ (mod\ p)\)

則$$a\equiv b\ (mod\ p) $$

注:引理<一>和取模運算的第二條定理的區別在於正反推,而且取模運算第二條定理對於\(c\)沒有做要求,而這個引理<一>要保證\(p\)\(c\)互質

證:

\(a*c\equiv b*c\ (mod\ p)\)

∴$$(ac - bc)\equiv 0\ (mod\ p)$$

∴$$(a-b)*c\equiv 0\ (mod\ p)​$$

∴$$(a-b)c = kp\ (k\in Z)​$$

∵$$p$$與\(c\)互質

\(k'*c*p\equiv 0\ (mod\ p)\)

又因為$$(a-b)c = kp\ (k\in Z) $$

即:$$(a-b)*c \equiv 0\ (mod\ p)$$

∴$$k'*p\equiv (a-b)\ (mod\ p)$$

即$$(a-b)\equiv 0\ (mod\ p)$$

∴$$a\equiv b\ (mod\ p)$$

證畢。


<二>

首先引入一個概念叫作剩餘系。。

剩餘類,亦稱同餘類,是一種數學的用語,為數論的基本概念之一。設模為n,則根據餘數可將所有的整數分為n類,把所有與整數a模n同餘的整數構成的集合叫做模n的一個剩餘類,記作[a]。並把a叫作剩餘類[a]的一個代表元。

然後我們在根據這個引入另一個概念叫作完全剩餘系。。。

從模n的每個剩餘類中各取一個數,得到一個由n個數組成的集合,叫做模n的一個完全剩餘系。完全剩餘系常用於數論中存在性證明

如果沒有透徹意思的可以舉個栗子:就是說在%n的意義下,$$0, 1, 2,3,\cdots,n-1$$就是模n的一個完全剩餘系

引理<二>:

若$$a_{1},a_{2},a_{3},\cdots,a_{p}$$ 為\(mod\ p\) 的完全剩餘系,\(p\)\(b\)互質,

則$$a_{1}b, a_{2}b, a_{3}b,\cdots,a_{p}b$$也是\(mod\ p\) 的完全剩餘系

證:

利用反證法:

假設存在\(b*a_{i}\equiv b*a_{j}\ (mod\ p)\),根據引理<一>可以得到\(a_{i}\equiv a_{j}\ (mod\ p)\)

這顯然不成立,

引理<二>成立

證畢。


我們便可以開始證明費馬小定理了

證:

\(0,1,2,\cdots, p-1\)是%\(p\)的完全剩餘系,這很顯然吧

\(a\)\(p\)互質

∴根據引理<二>可以得知:\(0,a*1,a*2,\cdots, a*(p-1)\)也是%\(p\)的完全剩餘系

∵$$\left{ \begin{array}{lrc} 1\equiv ak_{1}\ (mod\ p)\ \vdots \p-1\equiv ak_{p-1}\ (mod\ p)\end{array} \right.$$$$\left{ \begin{array}{lrc} k_{1}\in\ (1,\cdots,p-1)\ \vdots\ \k_{p-1}\in\ (1,\cdots,p-1)\end{array} \right.$$

∴根據取模運算對乘法是封閉的得出:$$12\cdots(p-1)\equiv a1a2\cdotsa*(p-1)\ (mod\ p)$$

即:$$(p-1)!\equiv (p-1)!*a^{p-1}\ (mod\ p)$$

根據引理<一>兩邊同時約去\((p-1)!\)

就可以得到\(1\equiv a^{p-1}\ (mod\ p)\)

即費馬小定理:\(a^{p-1}\equiv 1\ (mod\ p)\)

證畢。

是不是透徹了許多呢???

應用:

求逆元,沒錯,就是我們下面要講的逆元

例題:

乘法逆元

這道題我們用費馬小定理做的話,會T掉三個點,所以我們先不講這道題,我們先講一下逆元

逆元

定義:

逆元素是指一個可以取消另一給定元素運算的元素。 ————某度某科

這個定義似乎不太那麼直觀……

我們來舉個例子吧,先再實數範圍舉例,由小學知識可知,如果一個代數式 \(F\) 乘一個數 \(a\) 後,再乘它的倒數 \(\frac 1{a}\) ,相當於沒有乘 \(a\)(這裡不考慮 \(0\) 的情況),換句話說,我們乘 \(\frac 1{a}\) 後,取消了代數式 \(F\)\(a\) 後值增大的影響。

我們換句話說就是:

如果說 a 在模 p 意義下的乘法逆元是 x,那麼 $$ a*x≡1\ (mod\ p)$$

我們通常把\(a\)的逆元記為\(a^{-1}\)

就像\(0\)沒有倒數一樣,\(0\)也沒有逆元

當且僅當\(gcd(a,p) =1\)時,\(a\)的逆元有唯一解,否則無逆元

不難發現,\(a\)在模 \(p\) 意義下的乘法逆元為 [1,p - 1] 中的整數。

(這不難理解吧,因為如果\(a^{-1} >= p\)的話,在模\(p\)的意義下就會抵消

求法:

今天我要講五種求逆元的演算法(認真聽,萬一我沒睡醒,把你帶進無盡深淵

如果沒聽懂的話我甩了個連結

(1)拓展歐幾里得求逆元

拓展歐幾里得相信大家都會吧!!!

\(a*a^{-1}\equiv 1\ (mod\ p)\)

∴我們設\(x=a^{-1}\),我們就可以得到

我們把這個柿子巧妙地轉化一下就變成了:\(a*x-k*p= 1\)

我們令\(y=-k\),則\(a*x+p*y=1\)

我們發現這個方程有解的條件是 \(gcd(a,p)=1\),即 \(a,p\) 互質,所以得出結論: *\(a\)在模\(p\) 意義下的乘法逆元存在當且僅當 \(a,p\)互質,這也在一定程度上解釋了大多數題目模數為什麼為質數的原因:(設模數為 \(p\))可以保證在 [1,p−1] 中的所有整數都有模 \(p\) 意義下的逆元。

\(gcd(a,p)=1\)

∴我們可以用拓展歐幾里得求方程\(a*x+p*y = gcd(a,p)\),即\(a*x+p*y=1\)

最後求出\(x\)的值,即為\(a\)的逆元

時間複雜度為\(O(log\ p)\)

參考程式碼(本人碼風較醜,勿噴

//luogu P3811 會T掉最後一個點(貌似開long long的話T倆點)
#include<bits/stdc++.h>
using namespace std;
int n, p, x, y, a[30000005];
void exgcd(int a, int b){
	if(b == 0){
		x = 1, y = 0;
		return ;
	}
	exgcd(b, a % b);
	int t = x;
	x = y;
	y = t - a / b * y;
}
int main(){
	scanf("%d %d", &n, &p);
	for(int i = 1; i <= n; i ++){
		exgcd(i, p);
		a[i] = (x + p) % p;
	}
	for(int i = 1; i <= n; i ++){
		printf("%d\n", a[i]);
	}
	return 0;
}

(2)費馬小定理求逆元

前面我們剛複習了一下費馬小定理(你要是現在說不會或者說忘了,我就要抽出我四十米長的大刀來給你削個蘋果,因為你太秀了)

上面我手打了那麼多行的證明,大家應該都透徹費馬小定理的正確性了吧

我們想一下

\[\left\{ \begin{array}{lrc}a*a^{-1}\equiv 1\ (mod\ p)\\ a^{p-1} \equiv 1\ (mod\ p)\end{array} \right. \]

我們把\(a^{p-1}\)轉化為\(a*a^{p-2}\)

我們會驚奇地發現\(a^{p-2}=a^{-1}\)

於是我們找到了費馬小定理求逆元的方法

時間複雜度為\(O(log\ p )\)

參考程式碼

//luogu P3811 會T掉三個點
#include<bits/stdc++.h>
using namespace std;
long long n, mod;
long long ksm(long long x, long long y){
	if(!y) return 1;
	if(y & 1) return (ksm(x, y - 1) * x) % mod;
	long long res = ksm(x, y >> 1) % mod;
	return res * res % mod;
}
int main(){
	scanf("%lld %lld", &n, &mod);
	for(int i = 1; i <= n; i ++){
		printf("%lld\n", (ksm(i, mod - 2) % mod + mod) % mod);
	}
	return 0;
}

細心的小盆友會發現這兩種方法的時間複雜度不優

(3)線性求逆

注:這種方法僅適用於當\(p\)為質數的時候

1在模任何正整數意義下的乘法逆元都是1,即:\(1^{-1}=1\ (mod\ n)\ (n\in Z)\)

我們要求在模\(p\)意義下\(1\)~\(n(1<=n<p)\)的逆元,我們設\(p=k*i + m\)

我們不難想到\(k = \lfloor \frac {p}{i} \rfloor ,\ m = p\ mod\ i\)

∴我們可以列出方程\(k*i+m\equiv 0\ (mod\ p)\)

然後兩邊同乘\(i^{-1},\ m^{-1}\)

我們得到\(k*m^{-1}+i^{-1}\equiv 0\ (mod\ p)\)

\(i^{-1}\equiv -k*m^{-1}\ (mod\ p)\)

又∵$k = \lfloor \frac {p}{i} \rfloor ,\ m = p\ mod\ i $

\(i^{-1}\equiv -\lfloor \frac {p}{i} \rfloor *(p\ mod\ i)^{-1}\ (mod\ p)\)

但是我們考慮一點,\(-\lfloor \frac {p}{i} \rfloor\)是個負數,我們在模\(p\)的意義下,效果是等效於\((p - \lfloor \frac {p}{i} \rfloor)\)

∴我們的柿子又闊以變成了這個鬼亞子\(i^{-1}\equiv (p- \lfloor \frac {p}{i} \rfloor)*(p\ mod\ i)^{-1}\ (mod\ p)\)

這不就得到了一個遞推式了嘛

時間複雜度\(O(n)\)

參考程式碼如下:

//luogu P3811 這樣就可以很愉悅的AC了
#include<bits/stdc++.h>
using namespace std;
const int N = 3e6 + 5;
long long n, p, inv[N];
int main(){
	scanf("%d %d", &n, &p);
	inv[1] = 1;
	for(int i = 2; i <= n; i ++){
		inv[i] = (p - p / i) * inv[p % i] % p;
	}
	for(int i = 1; i <= n; i ++){
		printf("%d\n", inv[i]);
	}
	return 0;
}

注:

(1):別忘了給inv[1]賦初值

(2):別忘了開long long(不開long long見祖宗

(4)尤拉定理求逆元

首先我們要先了解一下尤拉定理

\(p,a\)為正整數,且\(p,a\)互質,則:\(a^{\varphi(p)}\equiv 1\ (mod\ p)\)

尤拉定理不歸我講,所以記住就好了

(如果真的想看證明的話戳這裡,和上面甩的那個費馬小定理的證明是同一篇文章

emmmm......事情發生了點轉變,尤拉定理還是我講吧(我把證明就放在最後的補充裡面吧)

\(a^{\varphi(p)}\equiv 1\ (mod\ p)\)

\(a*a^{\varphi(p)-1}\equiv 1\ (mod\ p)\)

又∵\(a*a^{-1}\equiv 1\ (mod\ p)\)

\(a^{-1}\equiv a^{\varphi(p)-1}\ (mod\ p)\)

引理:對於正整數\(n\)進行唯一分解,得到柿子\(\displaystyle n=p_{1}^{k_{1}}\times p_{2}^{k_{2}}\times \cdots \times p_{m}^{k_{m}}=\prod_{i=1}^{m}p_{i}^{k_{i}}\)

則:\(\displaystyle \varphi(n)=n\times(1-\frac 1{p_{1}})\times(1-\frac 1{p_{2}})\times\cdots\times(1-\frac 1{p_{m}})=n\times \prod_{i=1}^{m}(1-\frac 1 {p_{i}})\)

然後我們轉換一下形態:\(\displaystyle \varphi(n) = n\times \prod_{i = 1}^{m}(\frac {p_{i}-1}{p_{i}})\)

我們可以在\(O(\sqrt {n})\)的時間內列舉每個質因子,因為每個質因子的貢獻只有一次,所以用完之後要記得消去,

這裡有一個很巧妙的防止炸的方法就是首先令最初的\(ans = n\),然後每次累乘\(\frac {p_{i}-1}{p_{i}}\)就好了,我們假設當前進行到了第\(m\)次累乘,所以當前\(\displaystyle ans=n*\prod _{i=1}^{m-1}{\frac{p_{i}-1}{p_{i}}}\),於是我們要得到的新的\(ans\),便可以用\(ans=(\frac {ans}{p_{m}})*( p_{m}-1)\),最後我們用快速冪求一下\(a^{\varphi(p)-1}\)就歐克了

時間複雜度:\(O(log\ \varphi(p)+\sqrt{p})\)

參考程式碼:

#include<bits/stdc++.h>
using namespace std;
int n, p;
int phi(int x){
	int res = x, tmp = x;
	for(int i = 2; i * i <= tmp; i ++){
		if(x % i == 0){
			res = ((res / i) % p * (i - 1) % p) % p;
			while(x % i == 0) x /= i;
		}
	}
	if(x > 1) res = ((res / x) % p * (x - 1) % p) %p;
	return res % p;
}
int ksm(int x, int y){
	if(!y) return 1;
	if(y & 1) return x * ksm(x, y - 1) % p;
	int res = ksm(x, y >> 1) % p;
	return res * res % p;
}
int main(){
	scanf("%d %d", &n, &p);
	int tmp = phi(p) - 1;
	printf("%d\n", ksm(n, tmp));
	return 0;
} 

(5)離線逆元

看我剛才甩的部落格上說這個演算法貌似很難(其實不是很難,認真聽絕對能聽懂,相信我

這個演算法主要的用處在於,給定\(n\)個整數,不一定相鄰,就是隨機的那種,然後讓你求一下每個\(a_{i}\)的逆元

我們看到這個的時候,在沒學這個演算法之前可能會用線性求逆來求,也有可能用費馬小定理或拓展歐幾里得求,但是,今天學了這個演算法之後,你就會愛上這個神奇的演算法

我們先看一道

看到這道題,我們首先考慮線性求逆的複雜度\(O(max(a_{i}))\),在這道題中顯然不是很優,因為時間空間都會被卡(而且出題人也不傻,怎麼可能乘法逆元一的正解就是乘法逆元二的呢)

我們再考慮一下費馬小定理和拓展歐幾里得的\(O(n\ (log\ p))\),貌似更炸.

然後我們講完離線逆元之後我們再考慮一下這個神奇的演算法的可行性

首先我們要知道一個事情就是:字首積的逆元就是逆元的字首積(即逆元是完全積性的)

證:我們設任意兩個整數\(a\)\(b\),我們看一下他們在模\(p\)意義下一定滿足\(a^{-1}*b^{-1}\equiv (a*b)^{-1}\ (mod\ p)\)

我們根據逆元的定義可以得到柿子\(a*a^{-1}*b*b^{-1}\equiv 1\ (mod\ p)\)

然後我們把\(a*b\)看做一個整體,然後這個整體的逆元就是\((a*b)^{-1}\)

我們可以知道\((a*b)*(a*b)^{-1}\equiv 1\ (mod\ p)\)

\((a^{-1}*b^{-1})\equiv (a*b)^{-1}\ (mod\ p)\)

根據這個性質,我們可以用一個\(pre_{n}\)陣列來儲存\(a_{n}\)的字首積(不是字首和!!!

\(\displaystyle pre_{n}=\prod_{i=1}^{n}a_{i}\)

\(\displaystyle pre_{n}^{-1}=\prod_{i=1}^{n}a_{i}^{-1}\)

然後我們就會發現:

\(a_{i}^{-1}=pre_{i}^{-1}*pre_{i-1}\ (1<=i<=n)\)

\(pre_{i}^{-1}=pre_{i+1}^{-1}*a_{i+1}\ (1<=i<n)\)

然後我們是不是闊以根據這兩個柿子來做題了呢

我們考慮一下他的時間複雜度:線性的列舉\(O(n)\)+費馬小定理(或拓展歐幾里得)求$pre_{n}^{-1} \(的\)O(log\ p) $

時間複雜度為:\(O(n+log\ p)\)

但是這道題還會卡常,所以我們還要用快讀

因為這道題最後要求的是\(\displaystyle\sum_{i=1}^{n}{\frac {k^{i}}{a_{i}}}\),所以我們可以轉換為\(\displaystyle \sum_{i=1}^{n}{k^{i}*a_{i}^{-1}}\)

我太菜了,這道題我交了20次才A,還是在\(hjr\)童鞋的幫助下

參考程式碼:

#include<cstdio>
using namespace std;
const int N = 5e6 + 5;
int n, p, k, pre[N], inv_pre[N], a[N];
inline int read(){
	int op = 0, opp = 1; char ch = getchar();
	while(ch > '9' || ch < '0'){ if(ch == '-') opp = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){ op = (op << 1) + (op << 3) + (ch - '0'); ch = getchar();}
	return 1ll * op * opp;
}
inline int ksm(int x, int y, int p){
	int res = 1;
	while(y){
		if(y & 1) res = 1ll * res * x % p;
		x = 1ll * x * x % p;
		y >>= 1;
	}
	return res;
}
int main(){
	n = read(), p = read(), k = read();
	pre[0] = 1;
	for(register int i = 1; i <= n; ++ i){
		a[i] = read();
		pre[i] = 1ll * pre[i - 1] * a[i] % p;
	}
	inv_pre[n] = 1ll * ksm(pre[n], p - 2, p) ;
	for(register int i = n; i >= 1; -- i){
		inv_pre[i - 1] = 1ll * inv_pre[i] * a[i] % p;
	}
	int ans = 0;
	for(register int i = n ; i ;i --){
		ans = (ans + 1ll * inv_pre[i] * pre[i - 1] % p) * k % p;//直接在計算中求的逆元,並且逆序不用擔心k(這很細節操作,我一直被卡在了這)
	} 
	printf("%d\n", ans);
	return 0;
}

關於這道題卡常,我在luogu看到了一篇帖子

  • 換成快讀,用fread而不是getchar
  • i++換成++i,i--同理
  • 儘可能減少%p
  • 快讀中的*10,替換成(sum<<1)+(sum<<3)
  • 減少型別轉換
  • 迴圈變數等任何區域性變數,定義前加入register關鍵字(例如 for (register int i=1;i<=n;++i;))
  • 減少呼叫標頭檔案
  • 吸臭氧(厭氧型程式碼可能負優化)
  • 對於我個人的程式碼,其中register和fread是最高效率的。

還有一個我補充的(因為我那20次提交中,有一次用\(inline\),優化成功了一個點):就情況寫\(inline\)

對於這些優化常數的東西,沒有試過,

因為我太菜了, 不會常數優化,所以我又來甩連結了,相同的連結,不同的視覺效果

這篇部落格較為系統全面地介紹了優化常數的東西

  • 宣告:對於乘法逆元二這道題,我看第一篇題解賊簡單,思想其實和這個離線逆元有那麼一丟丟的相似

啊這!! !你們想聽嗎?反正我在最後的補充那寫了(你們也可以自己去康康)

在弄完這個方法之後,我這道題前前後後一共交了40遍,多多少少有點想吐,咳!!!

階乘逆元:

沒錯,你沒有看錯,就是階乘逆元

其實思想特別~簡單

我們考慮要計算\({(n!)}^{-1}\)的話,如果對每個元素都單獨求一遍的話,是會炸的!!!

然後我們考慮上面講到的第五種求逆元的方法———離線逆元,

我們會發現,我們可以把\((n!)\)看成當時的\(pre_{n}\)陣列,然後根據遞推式求就好了

怎麼樣,是不是很簡單呀!!!

參考程式碼:

#include<cstdio>
#include<iostream>
using namespace std;
const int N = 5e6 + 5;
int n, p, pre[N], inv_pre[N], inv[N];
inline int read(){
	int op = 0, opp = 1; char ch = getchar();
	while(ch > '9' || ch < '0'){ if(ch == '-') opp = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){ op = (op << 1) + (op << 3) + (ch - '0'); ch = getchar();}
	return op * opp;
}
inline int ksm(int x, int y, int p){
	int res = 1;
	while(y){
		if(y & 1) res = res * x % p;
		x = x * x % p;
		y >>= 1;
	}
	return res % p;
}
int main(){
	n = read(), p = read();
	pre[0] = 1;
	for(register int i = 1; i <= n; ++ i){
		pre[i] = pre[i - 1] * i % p;
	}
	inv_pre[n] = ksm(pre[n], p - 2, p) ;
	for(register int i = n; i >= 1; -- i){
		inv[i] = inv_pre[i] * pre[i - 1] % p;
		inv_pre[i - 1] = inv_pre[i] * i % p;
	}
	for(register int i = 1; i <= n; i ++){
		printf("%d\n", (inv[i] % p));
	} 
	return 0;
}

逆元的知識到此結束了,前方到站———矩陣乘法

逆元還有沒有什麼不透徹的地方

矩陣乘法

還是老規矩,先甩一篇部落格,如果我講的實在聽不懂, 可以看看那篇部落格

事先說好,我矩陣乘法不好我似乎啥也不好emmm...容易把人帶到無盡深淵,還有就是本人太菜了,不太會用\(markdown\)這高階玩意,所以矩陣有點醜的話還請見諒吧

定義:

首先我們要先了解一下矩陣這東西

來自某度某科的一段話,

然後,我們現在可以知道什麼是矩陣了吧

矩陣乘法運演算法則及性質:

我們考慮什麼是矩陣乘法呢???

矩陣乘法(蒟陣乘法),顧名思義,就是把兩個矩陣(蒟蒻)乘起來,但是和普通乘法不太一樣

如果還是不能理解矩陣乘法的話,劉汝佳寫的那本藍皮書的\(P151\)頁有和線性方程聯絡起來的解釋

兩個矩陣能相乘的條件:

這個很重要,趕緊拿小本本記下來

當一個矩陣的行數等於另一個矩陣的列數的時候,相乘才有意義

而且假設一個\(p*n\)的矩陣和一個\(n*k\)的矩陣相乘,我們最後得到的新矩陣是\(p*k\)的,\(p,k\)不一定相等

運演算法則:

首先我們需要三個矩陣(蒟蒻)\(A,B,C\)

我們假設矩陣\(C=A*B\)

我們假設\(A\)矩陣是\(n*m\)的,\(B\)矩陣是\(m*p\)的,然後我們可以知道\(C\)矩陣是\(n*p\)的,且\(\forall i \in [1,n], \forall j \in [1,p]\)

然後我們發現$\displaystyle C_{i,j} = \sum {k=1}^{m}A{i,k}*B_{k,j} $

然後我們用矩陣來實現一下(我們就以\(2*3\)的矩陣和\(3*2\)的矩陣為例):

\[\left[ \begin{matrix} a & b & c \\ d & e & f \end{matrix} \right] \times \left[ \begin{matrix} g & h \\ i & j \\ k & l \end{matrix} \right] = \left[ \begin{matrix} a*g+b*i+c*k & a*h+b*j+c*l \\ d*g+e*i+f*k & d*h+e*j+f*l \end{matrix} \right]$$​ 怎麼樣?是不是透徹了許多呢(要是還不透徹都對不起我手打的這個矩陣) ##### 性質: 下面的東西可能會很亂,建議大家自己百度百科一下(有些東西貌似不用掌握,瞭解就好) 運演算法則已經$get$到了吧,那這有啥特殊性質呢 * 矩陣乘法滿足結合律,$A*(B*C)=(A*B)*C$,根據這一性質,我們後面可以通過矩陣快速冪來快速求解,並且還能降低時間複雜度 * 矩陣乘法**並不滿足**交換律,因為矩陣乘法涉及許多的操作,而且矩陣相乘得到的還是矩陣,總之就不滿足交換律 我們記得逆元,其實每個元素$a$都存在唯一的逆元$a^{-1}$,類似地,$n$階矩陣(即$n*n$)的 集合中也存在特殊的矩陣$A$存在唯一的逆元$A^{-1}$使得$A*A^{-1}=A^{-1}*A=I_{n}$,$A^{-1}$就稱為$A$的逆矩陣 其中的$I_{n}$為$n$階的單位矩陣(其實就是對角線上全為1),我們可以形象地將單位矩陣理解為數字$1$,因為用處的話到後面的矩陣快速冪十分顯然 $$I_{n} = \left[ \begin{matrix} 1 & 0 & \cdots & 0 \\ 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & 1 \end{matrix} \right]\]

  • 矩陣可逆的一個常用的充分必要條件為:所有的行向量是線性無關的(線性無關是指任意一個向量都不是其他向量的線性組合)
  • 矩陣\(A\)\(n\)階方陣,若存在\(n\)階矩陣\(B\),使得矩陣\(A、B\)的乘積為單位陣,則稱\(A\)為可逆陣,\(B\)\(A\)的逆矩陣。若方陣的逆陣存在,則稱為可逆矩陣或非奇異矩陣,且其逆矩陣唯一。

A可逆的充要條件:

1、\(|A|\)不等於0。

2、\(r(A)=n\)

3、\(A\)的列(行)向量組i線性無關。

4、\(A\)的特徵值中沒有0。

5、\(A\)可以分解為若干初等矩陣的乘積。

其中\(r(A)\)為矩陣的秩:

用初等行變換將矩陣A化為階梯形矩陣, 則矩陣中非零行的個數就定義為這個矩陣的秩, 記為\(r(A)\)

以下是滿秩矩陣的定義:

若矩陣秩等於行數,稱為行滿秩;若矩陣秩等於列數,稱為列滿秩。既是行滿秩又是列滿秩則為n階矩陣即n階方陣。行滿秩矩陣就是行向量線性無關,列滿秩矩陣就是列向量線性無關;所以如果是方陣,行滿秩矩陣與列滿秩矩陣是等價的。

接下來為階梯矩陣的定義:

矩陣的特徵值:

設 A 是n階方陣,如果存在數m和非零n維列向量x,使得 \(Ax=mx\) 成立,則稱 m 是矩陣A的一個特徵值或本徵值

啊這,是不是吐了,我也是,我們還是愉悅地開始別的吧,這個要是沒懂的話也不要緊,只要把單位矩陣之前的聽懂就好了,感興趣的可以自己下課搜一下百度百科

用途:

話說,講了這麼多,這個有什麼用呢?

前面在說矩陣乘法滿足結合律的時候提到了一個矩陣快速冪,是的,沒錯,就是這玩意,這玩意就是矩陣優化遞推的原理,我們就可以把一個不能用快速冪優化的柿子轉化成快速冪的形式

這樣的話,我們就能把許多題的\(O(n)\)優化到\(O(log\ n)\)

前面我們提到了單位矩陣,我們說可以把單位矩陣形象地理解為數字\(1\),何處此言呢,我們考慮,我們要進行矩陣快速冪的話,肯定最後會找一個矩陣來承接,但這個矩陣長什麼亞子呢?

我們考慮如果不做任何處理,直接用全為\(0\)的矩陣,那我們無論怎麼乘,最後都會是\(0\)

但是單位矩陣就不一樣了,我們來證明一下單位矩陣的合理性:

我們假設一個\(n\)階矩陣\(A\),我們將\(A\)\(I_{n}\)相乘,得到新矩陣\(C\)

\[\left[ \begin{matrix} a & b & c\\ d & e & f\\g & h & i \end{matrix} \right] \times \left[ \begin{matrix} 1 & 0 & 0\\0 &1 & 0\\ 0 & 0 & 1 \end{matrix} \right] = \left[ \begin{matrix} a & b & c \\ d & e & f\\ g & h & i \end{matrix} \right] \]

是不是驚奇地發現\(A\times I_{n} = C =A\)

我們考慮一下why?

因為我們發現當\(A\)\(I_{n}\)相乘的時候,當\(A\)的第\(i\)行與\(I_{n}\)的第\(j\)列相乘得到新矩陣\(C\)\(C_{i,j}\),我們可以想到\(I_{n}\)的第\(j\)列,一定只有第\(j\)個位置是\(1\),而其他位置均為\(0\),所以我們很容易知道\(A\)的第\(i\)行乘\(I_{n}\)的第\(j\)列,最後得到的那個數,一定是\(A_{i,j}\),而得到的這個值恰好是\(C_{i,j}\),所以就很神奇的證明出來了

突然發現矩陣的Markdown寫起來要死人

我們透徹了原理的話,就要來做一道模板題

這就是個矩陣快速冪的模板題,但要記得開長整型,並且寫快速冪的時候要記得寫\(while\)的,遞迴的會炸,你會死的很慘的

本來是想打過載的,但我太笨了,不會

參考程式碼:(這份程式碼寫的有些忐忑,我居然開炸空間了,所以很醜,不要在意)

#include<bits/stdc++.h>
using namespace std;
const int P = 1e9 + 7;
long long n, k;
struct node{
	long long a[105][105];
}ju, ans;
node musl(node op, node opp){
	node res;
	for(int i = 1; i <= n; i ++){
		for(int j = 1; j <= n; j ++){
			res.a[i][j] = 0;
		}
	}
	for(int i = 1; i <= n; i ++){
		for(int j = 1; j <= n; j ++){
			for(int k = 1; k <= n; k ++){
				res.a[i][j] = (res.a[i][j] + op.a[i][k] * opp.a[k][j] % P) % P ;
			}
		}
	}
	return res;
}
int main(){
	scanf("%lld %lld", &n, &k);
	for(int i = 1; i <= n; i ++){
		for(int j = 1; j <= n; j ++){
			scanf("%lld", &ju.a[i][j]);
			if(i == j) ans.a[i][j] = 1;
		}
	}
	while(k){
		if(k & 1) ans = musl(ans, ju);
		ju = musl(ju, ju);
		k = k / 2;
	}
	for(int i = 1; i <= n; i ++){
		for(int j = 1; j <= n; j ++){
			printf("%lld ", ans.a[i][j]);
		}
		printf("\n");
	}
	return 0;
}

例題:

由於本人能力有限,找到的題不是很多,而且\(so\ easy\),所以,請各位\(dalao\)合理\(AC\)

斐波那契數列

你在$$luogu$$搜斐波那契數列,你將會看到有好多顏色的,其中有道紫色的資料範圍是$$n \leq 10^{30000000}, p<2^{31}$$

咳,當然我們不講這個,因為我太菜了,這心有餘而力不足呀!!!

我們考慮斐波那契數列的公式:

\[\left \{ \begin{array}{lrc} f_{1}= f_{2} =1 \\ f_{n}=f_{n-1} +f_{n-2}\ (n \geq 2 且 n\in Z)\end{array} \right. \]

我們考慮如何用矩陣得到呢

我們會發現\(f_{n}\)\(f_{n-1},f_{n-2}\)有關

所以我們就設一個矩陣\(A=\left[ \begin{matrix} f_{n-1} & f_{n-2} \end{matrix} \right]\)

根據這個矩陣的話,我們最後肯定是要得到一個結構相似的,才能進行矩陣快速冪呢

我們不難發現我們可以得到矩陣\(C=\left[ \begin{matrix} f_{n} & f_{n-1} \end{matrix} \right]\)

那我們考慮\(A*B=C\)的話,那麼這個\(B\)是啥呢

我們經過推理柿子:

\[\left[ \begin{matrix} f_{n-1} & f_{n-2} \end{matrix} \right] \times B = \left[ \begin{matrix} f_{n} & f_{n-1} \end{matrix} \right] \]

我們根據相乘的規律可以得到\(B\)一定是一個\(2*2\)的矩陣吧!

所以我們就要開始填數了

\(B=\left[ \begin{matrix} x_{1} & x_{2} \\ x_{3} & x_{4} \end{matrix} \right]\)

\(f_{n}= x_{1}*f_{n-1}+x_{3}*f_{n-2}=f_{n-1}+f_{n-2}​\),我們不難得出\(\left \{ \begin{array}{lrc} x_{1}=1 \\x_{3}=1 \end{array} \right.​\)

又因為\(f_{n-1}=x_{2}*f_{n-1}+x_{4}*f_{n-2}​\),我們可以得到\(\left \{ \begin{array}{lrc} x_{2}=1\\x_{4}=0 \end{array} \right.​\)

所以我們就完美地求出了矩陣\(B=\left[ \begin{matrix} 1 & 1\\1 & 0 \end{matrix} \right]​\)

參考程式碼:(太醜了,我自己都嫌棄這份程式碼)

#include<bits/stdc++.h>
using namespace std;
const int p = 1e9 + 7;
struct node{
	long long a[3][3];
}js, s;
struct nndd{
	long long a[3];
}ans, st;
node mul(node op, node opp){
	node ss;
	for(int i = 1; i <= 2; i ++){
		for(int j = 1; j <= 2; j ++){
			ss.a[i][j] = 0;
		}
	}//清空一下下
	for(int i = 1; i <= 2; i ++){
		for(int j = 1; j <= 2; j ++){
			for(int k = 1; k <= 2; k ++){
				ss.a[i][j] = (ss.a[i][j] + op.a[i][k] * opp.a[k][j] % p) % p;
			}
		}
	}//矩陣乘法基本操作
	return ss;
}
long long n;
int main(){
	scanf("%lld", &n);
	if(n == 1 || n == 2){//特判
		printf("1\n");
	}else{
		st.a[1] = st.a[2] = 1;//st存的是f[2],f[1]
		js.a[1][1] = js.a[1][2] = js.a[2][1] = 1;//要把那個迴圈的東西搞出來
		s.a[1][1] = s.a[2][2] = 1;//這個就是單位矩陣,最後承接js的辣麼多次方
		n = n - 2;//我們要算js的(n-2)次方,所以直接(n=n-2)就okkk了
		while(n){
			if(n & 1) s = mul(js, s);
			js = mul(js, js);
			n >>= 1;
		}//這就是矩陣快速冪
		for(int i = 1; i <= 2; i ++){
			for(int j = 1; j <= 2; j ++){
				ans.a[i] = (ans.a[i] + st.a[i] * s.a[j][i] % p) % p;
            }//將答案轉移到ans陣列
		}
		printf("%lld\n", ans.a[1]);
	}
	return 0;
}
刷題比賽

是的呢,你沒有看錯,就是上次\(ZQH\)學長講的那個\(11*11\)的噁心的矩陣

咳!又到了推柿子的環節了(又要敲矩陣了)

首先這三個人\(nodgd,Ciocio,Nicole\)

他們第\(k+2\)天的刷題量分別為

\(a_{k+2}=p*a_{k+1}+q*a_{k}+b_{k+1}+c_{k+1}+r*k^{2}+t*k+1\\ b_{k+2}=u*b_{k+1}+v*b_{k}+a_{k+1}+c_{k+1}+w^{k}\\ c_{k+2}=x*c_{k+1}+y*c_{k}+a_{k+1}+b_{k+1}+z^{k}+k+2\)

這其中我們不難發現\(k\)是一個變數,那肯定就會和矩陣有點關係,我們先將含\(k\)的元素列舉出來

$a_{k+1},a_{k},b_{k+1},b_{k},c_{k+1},c_{k},k^{2},k,w^{k},z^{k} $

但是我們考慮一下當\(k^{2}\)變為\((k+1)^{2}=k^{2}+2*k+1\)的時候,我們還會需要到\(1\),所以我們把\(1\)加入到這個矩陣中,我們就得到了這個\(1*11\)的矩陣:

\(\left[ \begin{matrix} a_{k+1} & a_{k} & b_{k+1} & b_{k} & c_{k+1} & c_{k} &k^{2}& k & w^{k} & z^{k} & 1\end{matrix} \right]\)

我們考慮要得到的矩陣是什麼亞子的呢

\(\left[ \begin{matrix} a_{k+2} & a_{k+1} & b_{k+2} & b_{k+1} & c_{k+2} & c_{k+1} & (k+1)^{2} & k+1 & w^{k+1} & z^{k+1} & 1\end{matrix} \right]\)

既然\(A*B=C\)\(A,C\)都已經明確了,那我們就求一下中間的轉移矩陣吧

我們通過計算可以得知矩陣\(B\)一定是一個\(11*11\)的矩陣

那我們根據一堆亂七八糟的東西可以推出來這個轉移矩陣(敲得我手疼)

\(\left[ \begin{matrix} p & 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ q & 0 & 0& 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & u & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & v & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 1 & 0 & x & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & y & 0 & 0 & 0 & 0 & 0 & 0 \\ r & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ t & 0 & 0 & 0 & 1 & 0 & 2 & 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & w & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & z & 0 \\ 1 & 0 & 0 & 0 & 2 & 0 & 1 & 1 & 0 & 0 & 1 \end{matrix} \right]\)

但這道題還是個要注意的地方:這東西算出來特別~~大,所以要用到我們上面講到的龜速乘

這道題反正就覺得挺噁心的,但是把矩陣寫出來就好多了

還有就是提醒廣大同胞們,龜速乘那裡是乘\(2\),不是乘它本身,不要像sb的我一樣在這卡了一個小時

參考程式碼:

#include<bits/stdc++.h>
using namespace std;
long long n, m;
int p, q, r, t, u, v, w, x, y, z;
struct node{
	long long a[12][12];
}s, ss, sss, ans;
long long lower_mul(long long x, long long y){
	long long res = 0;
	while(y){
		if(y & 1) res = (res + x) % m;
		x = (x * 2) % m;
		y >>= 1; 
	}
	return res % m;
}
node musl(node op, node opp){
	node res;
	for(int i = 1; i <= 11; i ++){
		for(int j = 1; j <= 11; j ++){
			res.a[i][j] = 0;
		}
	}
	for(int i = 1; i <= 11; i ++){
		for(int j = 1; j <= 11; j ++){
			for(int k = 1; k <= 11; k ++){
				res.a[i][j] = (res.a[i][j] + lower_mul(op.a[i][k], opp.a[k][j])) % m;
			}
		}
	}
	return res;
}
int main(){
	scanf("%lld %lld", &n, &m);
	scanf("%d %d %d %d %d %d %d %d %d %d", &p, &q, &r, &t, &u, &v, &w, &x, &y, &z);
	s.a[1][1] = 3, s.a[1][2] = 1, s.a[1][3] = 3;
	s.a[1][4] = 1, s.a[1][5] = 3, s.a[1][6] = 1;
	s.a[1][7] = 1, s.a[1][8] = 1, s.a[1][9] = w;
	s.a[1][10] = z, s.a[1][11] = 1;
	for(int i = 1; i <= 11; i ++) sss.a[i][i] = 1;
	ss.a[1][1] = p, ss.a[2][1] = q, ss.a[3][1] = 1;
	ss.a[5][1] = 1, ss.a[7][1] = r, ss.a[8][1] = t;
	ss.a[11][1] = 1, ss.a[1][2] = 1, ss.a[1][3] = 1;
	ss.a[1][5] = 1, ss.a[3][3] = u, ss.a[3][4] = 1;
	ss.a[3][5] = 1, ss.a[4][3] = v, ss.a[5][3] = 1;
	ss.a[5][5] = x, ss.a[5][6] = 1, ss.a[6][5] = y;
	ss.a[7][7] = 1, ss.a[8][5] = 1, ss.a[8][7] = 2;
	ss.a[8][8] = 1, ss.a[9][3] = 1, ss.a[9][9] = w;
	ss.a[10][5] = 1, ss.a[10][10] = z, ss.a[11][5] = 2;
	ss.a[11][7] = 1, ss.a[11][8] = 1, ss.a[11][11] = 1;
	n = n - 2;
	while(n){
		if(n & 1) sss = musl(sss, ss);
		ss = musl(ss, ss);
		n = n/2;
	}
	for(int j = 1; j <= 11; j ++){
		for(int k = 1; k <= 11; k ++){
			ans.a[1][j] = (ans.a[1][j] + lower_mul(s.a[1][k], sss.a[k][j])) % m;			
		}
	}
	printf("nodgd %lld\nCiocio %lld\nNicole %lld\n", ans.a[1][1], ans.a[1][3], ans.a[1][5]);
	return 0;
}

我今天要講的就這些,應該不難,那個矩陣乘法還有一些優化啥的在我甩的那個連結裡

emmmmm....就這樣吧

補充:

取模運算重要性質:

模運算與基本四則運算有些相似,但是除法例外。其規則如下:

  1. $(a + b)% p = (a% p + b% p)% p $
  2. $(a - b)% p=(a% p - b% p)% p $
  3. \((a * b)\% p=(a\% p * b\% p)\% p\)
  4. \(a^b\% p =((a\% p)^b)\% p\)
  • 結合律:
  1. \(((a+b)\% p + c)\% p = (a + (b+c)\% p)\% p\)

  2. \(((a*b)\% p * c)\% p = (a * (b*c)\%p)\% p\)

  • 交換律:
  1. \((a + b)\% p = (b+a)\% p\)

  2. \((a * b)\% p = (b * a)\% p\)

  • 分配律:
  1. \((a+b)\% p = ( a\% p + b\% p )\% p\)

  2. \(((a +b)\% p * c)\% p = ((a * c)\% p + (b * c)\% p)\% p\)

【重要定理】

  • \(a\equiv b\ (mod\ p)\),則對於任意\(c\),都有$$(a+c)\equiv (b+c)\ (mod\ p)$$
  • 若$$a\equiv b\ (mod\ p)$$,則對於任意\(c\),都有\((a*c)\equiv (b*c)\ (mod\ p)\)
  • 若$$a\equiv b\ (mod\ p),c\equiv d\ (mod\ p)$$,則$$(a+c)\equiv (b+d)\ (mod\ p),(a-c)\equiv (b-d)\ (mod\ p),(ac)\equiv (bd)\ (mod\ p)$$;

尤拉定理證明:

我的數論不是特別的好,所以我儘量讓大家透徹尤拉定理

首先我們應該還記得上面我講費馬小定理的時候用到的引理一和引理二吧

我們考慮一下尤拉定理的結論是啥吧

\(p,a\)為正整數,且\(p,a\)互質,則:\(a^{\varphi(p)}\equiv 1\ (mod\ p)\)

首先,我們取出\(a\)在模\(p\)意義下,一個完全剩餘系中所有與\(p\)互質的數,

記為\(k_{1},k_{2},\cdots,k_{\varphi(p)}\),並且\(k_1,k_{2},\cdots, k_{\varphi(p)}\)在同一個完全剩餘系中,這樣就構成了模\(p\)的簡化剩餘系,簡稱縮系

我們可以知道\(k_{i}\)\(p\)互質

因為\(p,a\)互質

所以根據引理二,我們可以知道\(a*k_{1},a*k_{2},\cdots, a*k_{\varphi(p)}\)也是模\(p\)意義下的一個剩餘系

根據取模運算的定理,我們又不難發現這個剩餘系是一個縮剩餘系

至於為什麼呢?聽我細細道來,我們考慮\(k_{i}\)是一個縮系,裡面的每個元素都與\(p\)互質,

互質的話,說明\(k_{i}\)的每個元素的唯一分解中一定不會出現\(p\)的唯一分解中出現的質數,否則,這兩個數的\(gcd\)肯定不為\(1\),即這兩個數一定不互質,那我們把\(k_{i}\)的每個元素都乘一個新的\(a\),這個\(a\)也是與\(p\)互質的一個元素,我們可以把\(k_{i}*a\)想象成為在\(k_{i}\)的每個元素的唯一分解中都加入\(a\)的唯一分解的質數,那麼我們能夠知道,\(k_{i}*a\)依舊是與\(p\)互質的,而且一定還是一個完整的縮系,雖然可能不與原來的\(k_{i}\)相對應,那為什麼一定會是一個完整的新的縮系呢?

我們用反證法,假設存在\(a*k_{i} \equiv a*k_{j}\ (mod\ p)\ (i!=j)\),那我們根據推理一可以得到\(k_{i} \equiv k_{j}\ (mod\ p)\),這顯然是不成立的,所以,我們可以得知\(a*k_{i}\)在模\(p\)的意義下肯定是不同餘的,所以會構成一個完整的新的縮系

\(k_1*k_{2}*\cdots*k_{\varphi(p)} \equiv a*k_{1}*a*k_{2}*\cdots \ a*k_{\varphi(p)}\ (mod\ p)\)

\(k_{1}*k_{2}*\cdots *k_{\varphi(p)} \equiv a^{\varphi(p)} * k_{1}*k_{2} *\cdots * k_{\varphi(p)}\ (mod\ p)\)

然後根據引理一我們兩邊同時消去\(k_{1}*k_{2}*\cdots *k_{\varphi(p)}\),我們會得到\(a^{\varphi(p)} \equiv 1\ (mod\ p)\)

證畢。

是不是透徹了許多呢,那我們就來了解一下拓展尤拉定理吧

費馬小定理的尤拉定理證明:

首先上面我們已經知道了尤拉定理的正確性

然後我們考慮當\(p\)為質數時,\(\varphi(p)=p-1\),所以我們可以知道當\(p\)為質數的時候\(a^{\varphi(p)}=a^{p-1} \equiv 1\ (mod\ p)\)

所以我們的費馬小定理就得到了證明

拓展尤拉定理

首先我們要知道拓展尤拉定理是個啥玩意吧!!!

\(\left \{ \begin{array}{lrc} a^{c}\equiv a^{c\ mod\ \varphi(m)}\ (mod\ m)\ (gcd(a,m)=1)\\ a^{c}\equiv a^{c}\ (mod\ m)\ (gcd(a,m)\neq 1 且 c < \varphi(m))\\a^{c}\equiv a^{c\ mod\ \varphi(m) + \varphi(m)}\ (mod\ m)\ (gcd(a,m)\neq1且c\geq \varphi(m)) \end{array} \right.​\)

(1).當\(gcd(a,m)=1\)的時候,我們可以設\(c=k*\varphi(m)+d(d=c\ mod\ \varphi(m))\),我們可以得到\(a^{k*\varphi(m)+d}\equiv a^{d}\ (mod\ m)\),即\(a^{k*\varphi(m)}\times a^{d}\equiv a^{d}\ (mod\ m)\)

又因為\(a^{\varphi(m)}\equiv 1\ (mod\ m )\),所以\(a^{k*\varphi(m)}\equiv 1\ (mod\ m)\)

我們可以把\(a^{k* \varphi(m)}\),看成\(k\)\(a^{\varphi(m)}\)相乘,那最後還是會\(a^{k*\varphi(m)}\equiv 1\ (mod\ m)\)

然後我們兩邊同時乘上\(a^{m}\),得到\(a^{k*\varphi(m)}\times a^{d}\equiv a^{d}\ (mod\ m)\)

即我們要證的\(a^{c}\equiv a^{c\ mod\ \varphi(m)}\ (mod\ m)\)

(2).這很顯然的事情\(a^{c}\ mod\ m=a^{c}\ mod\ m\)

(3).首先我們知道\(gcd(a,m)\neq1\)\(a,m\)的 唯一分解中存在相同的質因數

我們首先取出\(m\)的一個質因數\(p\),令\(m = p^{r}*s\)

因為\(s\)中不含$p \(,所以\)gcd(p,s)=1$

根據尤拉定理我們可以知道\(p^{\varphi(s)}\equiv 1\ (mod\ s)\)

根據尤拉函式的性質可以得到\(\varphi(m)=\varphi(p^{r})*\varphi(s)\)(前提是\(gcd(p^{r},s)=1\))

所以\(p^{\varphi(p^r)\times \varphi(s)}\equiv 1\ (mod\ s)\)

我們可以得到\(p^{\varphi(m)}\equiv 1\ (mod\ s)\)

我們還是可以想象成\(\varphi(p^ r)\)\(\varphi(s)\)相乘,那麼最後還是\(p^{\varphi(p^r)\times \varphi(s)}\equiv 1\ (mod\ s)\)

\(p^{\varphi(m)}\equiv 1\ (mod\ s )\)

我們可以設\(p^{\varphi(m)} =k*s+1\),所以\(p^{\varphi(m)+r}=p^{\varphi(m)}*p^r=(k*s+1)*p^r=k*m+p^r\)

所以我們可以知道\(p^{\varphi(m)+r}\equiv p^r\ (mod\ m)\)

所以我們可以發現\(p^{c-r}*p^{\varphi(m)+r}\equiv p^r *p^{c-r}\ (mod\ m)(c \geq r)\)

沒有寫完呢,就(甩個連結)[https://blog.bill.moe/euler-theorem-notes/]吧

https://www.seotest.cn/jishu/43916.html

乘法逆元二(第一篇題解的方法)

來看這道我前前後後一共交了40遍的\(sb\)

這道題引起了我的極度不適,願你們能平安AC,我太菜了

我們首先考慮這道題最後要得到的答案是\(\displaystyle \sum_{i = 1}^{n}{\frac {k^{i}}{a_{i}}}\)

我們設\(\displaystyle s=\prod _{i = 1}^{n}{a_{i}}\),然後我們會發現\(\frac {1}{a_{i}}=\frac {s/{a_{i}}}{s}\)

然後柿子就變成了\(\displaystyle \sum _{i = 1}^{n}{\frac {k^{i}*(s/a_{i})}{s}}\)

∴我們只要維護一下當前這個位置的\(s/a_{i}\),即維護一個字首積和一個字尾積就歐克了

我們考慮每次都要乘\(\frac 1{s}\),我們這個提出來就變成了\(\displaystyle \frac 1{s}*\sum_{i=1}^{n}{k^{i}*(s/a_{i})}\)

我們考慮\(\frac 1{s}=s^{-1}\),所以我們最後把\(\displaystyle \sum _{i = 1}^{n}{k^i*(s/a_i)}\)求出來之後,然後乘上\(s\)的逆元就歐克了

整體的複雜度是\(O(n + log\ p)\) 題解給的是\(O(n)\)

經過我\(n+n\)次的試驗,第一篇題解的線性求單個逆元的時間居然優於費馬小定理

(我又交了好多次,其中有兩次是求單個逆元的方法不同,線性的方法能AC,而費馬小定理的T了好幾個點)

或許是因為\(ksm\)要申請新的空間吧

應該透徹了吧

下面的程式碼不一定能AC,因為\(luogu\)的測評機有點雙標,相同的程式碼我交了兩次,第一遍AC,第二遍T了兩個點

參考程式碼:

#include<cstdio>
#include<iostream>
using namespace std;
const int N = 5e6 + 5;
int n, p;
long long k;
int  a[N];
long long pre[N], nxt[N], ans;
inline long long inv(int x){
	if(x == 1) return 1;
	else return (p - p / x) * inv(p % x) % p;
}
inline int read(){
	int op = 0, opp = 1; char ch = getchar();
	while(ch > '9' || ch < '0'){ if(ch == '-') opp = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){ op = (op << 1) + (op << 3) + (ch - '0'); ch = getchar();}
	return 1ll * op * opp;
}
int main(){
	n = read(), p = read(), k = read();
	pre[0] = nxt[n + 1] = 1;
	for(int i = 1; i <= n; i ++){
		a[i] = read();
		pre[i] = 1ll * pre[i - 1] * a[i] % p;
	}
	for(int i = n; i >= 1; i --){
		nxt[i] = 1ll * nxt[i + 1] * a[i] % p;
	}
	int t = k;
	for(int i = 1; i <= n; i ++){
		ans = (ans + k * pre[i - 1] % p * nxt[i + 1] % p )% p;
		k = 1ll * k * t % p;
	}
	printf("%d\n", ans * inv(pre[n]) % p);
	return 0;
}