信奧日記——數論(快速冪、埃氏篩、尤拉篩)

star發表於2021-07-11

前情提要

本人蒟蒻,第一次寫博,如有寫錯的地方請指出,輕噴,神犇可以繞行

正文

1.快速冪

快速冪,顧名思義,快速冪就是快速算底數的n次冪。

//樸素的求冪方法
for(int i = 1; i <= n; i++){
    ans *= x;
}

顯而易見,如此樸素的求法的時間複雜度為O(N)

於是OIer們便想出了一個更為省時的方法並取名快速冪

思路

根據我們小學二年級學過的知識可以知道

如果(a = b + c)

則 xa = ab + ac

通過這種方法可以把每一步的指數都分成兩半,而相應的底數做平方運算。這樣不僅能把大的指數變小,而且可以減少迴圈次數,達到減小時間複雜度的目的。

所以我們要想求 x^n 時只要 n 能轉化為 2^i 次方即可得到 x^n = ((x2)2)^2......。

那麼我們只要把 n 轉換為二進位制即可達成這一條件

舉個例子
求3的14次方
14 的二進位制為 1110
則 3^14 = 3^8 * 3^4 * 3^2

程式碼

int powd(int a,int k){//傳值 a 為底數 k 為指數
	int ans = 1; //定義一個 ans 變數並將初值賦為 1 用來記錄答案
	while(k){//k 不等於 0 
		if(k & 1) ans = ans * a; //計算
		k >>= 1; 
		a = a * a; 
 	}
 	return ans;//返回結果
}

如此一來快速冪的時間複雜度就能由O(N)變為O(log2N),與樸素的求法相比極大的節省了時間。

完整程式碼(包含取模)

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<ctime>

using namespace std;

inline int read(){//快讀
   int s=0,w=1;
   char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
   while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
   return s*w;
}

typedef long long ll;//重定義 long long 為 ll 以方便後面使用

int powd(int a,int k,int mod){
	int res = 1;
	while(k){
		if(k & 1) res = (ll) res * a % mod;
        //運算時強制轉換為 long long ,防止res 和 a 均為一個臨近 int 邊緣的值,導致 res * a 超過 int 上限,出現錯誤
		k >>= 1;
		a = (ll)a * a % mod; 
 	}
 	return res % mod;
}

int main(){
	int a,k,mod;
	a = read();
	k = read();
	mod = read();
	int ans;
	ans = powd(a,k,mod);
	printf("%d^%d mod %d=%d",a,k,mod,ans);
	return 0;
}

2.埃拉託斯特尼篩法(埃氏篩)

埃氏篩是一種由希臘數學家埃拉託斯特尼所提出的一種簡單檢定素數的演算法。

原理

要得到自然數n以內的全部素數,必須把不大於 √n 的所有素數的倍數剔除,剩下的就是素數。

思路

求 2 到 25 之間的素數

step1 : 列出 2 以後所有的數

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

step2 : 標出目前的第一個素數 2

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

step3 : 劃掉 2 的倍數

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

step4 : 如果這個序列中最大數小於最後一個劃掉的素數的平方,那麼剩下的序列中所有的數都是素數,否則回到step2

25 大於 2 的平方,所以返回step2

step5 : 標出目前的第一個素數 3

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

step6 : 劃掉 3 的倍數

……

最終我們在劃掉 5 的倍數後

23 小於 5 的平方,跳出迴圈

目前序列中的每個元素為

2 3 5 7 11 13 17 19 23

全部是素數

程式碼

void get_primes(int n){//傳值 n
	for(int i = 2; i <= n; i++){//從 2 開始迴圈
		if(!prime[i]){//prime[i] 非 0
			cout << i << ",";//輸出目前的第一個素數
			for(int j = i; j <= n/i; j++){//迴圈 √n 次, n/i等於√n,但不用呼叫sqrt函式可以節約資源
				prime[j * i] = 1;//打標記
			}
		}
	}
}

完整程式碼(求 2 到 n 之間的素數)

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<ctime>

using namespace std;

inline int read(){//快讀
   int s=0,w=1;
   char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
   while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
   return s*w;
}

const int N = 1e7;

bool prime[N];

void get_primes(int n){
	for(int i = 2; i <= n; i++){
		if(!prime[i]){
			cout << i << ",";
			for(int j = i; j <= n/i; j++){
				prime[j * i] = 1;
			}
		}
	}
}

int main(){
	int n;
	n = read();
	get_primes(n);

	return 0;
}

時間複雜度為
$$
O(N * long(longn))
$$
證明見此 (反正我是沒看懂 逃……

3.線性篩

思路

顧名思義,一種針對埃氏篩的優化演算法,可以達到O(N)的複雜度

看剛才的埃氏篩不難發現,這裡面似乎會對某些數標記了很多次其為合數,那麼有沒有方法能夠省掉這些無意義的步驟呢?

追求時間有限的OIer當然要回答你:有!

只要能讓每個合數都只被標記一次,那麼時間複雜度就可以降到O(N)了。

所以我們只需要在 i % primes[j] == 0 時跳出迴圈即可

程式碼

void get_primes(int n){
	for(int i = 2; i <= n; i++){
		if(!prime[i])
			primes[cnt++] = i;
		for(int j = 0; primes[j] <= n/i; j++){
			prime[i * primes[j]] = 1;
			if(i % primes[j] == 0) break;//關鍵語句
		}
	}
}

完整程式碼

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<ctime>

using namespace std;

inline int read(){//快讀
   int s=0,w=1;
   char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
   while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
   return s*w;
}

const int N = 1e8+5;

bool prime[N];
int primes[N],cnt = 0;
int ans[N]; 

void get_primes(int n){
	for(int i = 2; i <= n; i++){
		if(!prime[i])
			primes[cnt++] = i;
		for(int j = 0; primes[j] <= n/i; j++){
			prime[i * primes[j]] = 1;
			if(i % primes[j] == 0)
				break;
		}
	}
}

int main(){
	int n = read();
	int q = read(); 
	get_primes(n);
	for(int i = 1;i <= q; i++){
		cin >> n;
		ans[i] = primes[n-1];
	}
	for(int i = 1;i <= q; i++){
		printf("%d\n",ans[i]);
	}
	return 0;
}

$$
如此一來埃氏篩的時間複雜度就可以降到O(N)了
$$

相關文章