[數論基礎] 1. 輸出素數(素數篩、線性篩、Miller-Rabbin 素性測試、巧妙解法)

1. 題目來源

鏈接:79. 輸出素數

2. 題目說明

在這裏插入圖片描述

3. 題目解析

方法一:素數篩+巧妙解法

題意很明確,這應該是學習完語言基礎,甚至是循環判斷基礎後就應該會做的題目。但是會做不代表能在這道題目上拿到滿分。

光是這一道題目就能產生 5 個層次的學生,看看你在哪一層呢?

第一層:0 分,不會寫,過不了,沒思路

第二層:40 分,最暴力的方式解決,但忘記考慮邊界 0 1情況,仍採用 cin、cout,但數據不強,在此不受影響

參見代碼如下:

#include <iostream>
#include <cmath>

using namespace std;

bool is_prime(int x) {
	for (int i = 2; i < x; ++i) {
		if (x % i == 0) return false;
	}
	return true;
}

int main() {
	int a, b;
	cin >> a >> b;
	for (int i = a; i <= b; ++i) {
		if (is_prime(i)) cout << i << endl;
	}
	return 0;
}

第三層:50分,算是及格分了,最暴力的方式解決,考慮邊界 0 1情況,仍採用 cin、cout,但數據不強,在此不受影響

參見代碼如下:

#include <iostream>
#include <cmath>

using namespace std;

bool is_prime(int x) {
	// 加一行,多10分
	if (x == 0 or x == 1) return false;
	for (int i = 2; i < x; ++i) {
		if (x % i == 0) return false;
	}
	return true;
}

int main() {
	int a, b;
	cin >> a >> b;
	for (int i = a; i <= b; ++i) {
		if (is_prime(i)) cout << i << endl;
	}
	return 0;
}

第四層:70分,算是及格分了,開根號優化,考慮邊界 0 1情況,仍採用 cin、cout,但數據不強,在此不受影響,在此即便換成 scanf、printf 也僅有 70 分

參見代碼如下:

#include <iostream>
#include <cmath>

using namespace std;

bool is_prime(int x) {
	if (x == 0 or x == 1) return false;
	for (int i = 2; i * i <= x; ++i) {
		if (x % i == 0) return false;
	}
	return true;
}

int main() {
	int a, b;
	cin >> a >> b;
	for (int i = a; i <= b; ++i) {
		if (is_prime(i)) cout << i << endl;
	}
	return 0;
}

第五層:100分,完美的解答,採用素數篩,優化效率

這個也就是本篇博文的意義所在了,素數篩 算法,思想很簡單,就考你知道還是不知道。在此首先來看看 一般篩,也就是 埃氏篩。下面依據紫書的 10.1.2節有關講解進行說明。篩法的思想特別簡單:對於不超過 n 的每個非負整數 p,刪除 2p, 3p, 4p,… 當處理完所
有數之後,還沒有被刪除的就是素數。如果用 vis[i] 表示i已經被刪除,篩法的代碼可以寫成:

memset(vis, 0, sizeof(vis));
for(int i = 2; i <= n; ++i)
  for(int j = i*2; j <= n; j += i) vis[j] = 1;

循環總次數會小於O(nlogn)O(nlogn),時間複雜度爲 O(nlnlnn)O(n*lnlnn) 效率已然很高了,但這樣會重複篩去,2*3=63*2=6會重複篩,但即便這樣,效率仍接近線性。有興趣可自由證明。

在此掛上大佬鏈接:普通篩法時間界的證明

下面來改進這份代碼。首先,在 對於不超過 n 的每個非負整數 p 中,p 可以限定爲素數,只需在第二重循環前加一個判斷 if(!vis[i]) 即可。另外,內層循環也不必從 i*2 開始,它已經在 i=2 時被篩掉了。改進後的代碼如下:

memset(vis, 0, sizeof(vis));
for(int i = 2; i * i <= n; ++i) if(!vis[i])
  for(int j = i*i; j <= n; j += i) vis[j] = 1;

完成版如下,但仍只能獲取 70 分,但是這並不是算法的問題,而是 cin、cout 太拉胯了,得改成 scanf、printf,就好了。

參見代碼如下:

#include <iostream>
#include <cmath>

using namespace std;

const int MAXN = 1e7 + 50;
int prime[MAXN];

void init_prime() {
	prime[0] = prime[1] = 1;
	for (int i = 2; i * i <= MAXN; ++i) {
		if (prime[i]) continue;
		for (int j = 2 * i; j <= MAXN; j += i) {
			prime[j] = 1;
		}
	}
	return;
}

int main() {
	init_prime();
	int a, b;
	cin >> a >> b;
	for (int i = a; i <= b; ++i) {
		if (prime[i] == 0) cout << i << endl;
	}
	return 0;
}

cin、cout,改成 scanf、printf,終於獲取滿分。

參見代碼如下:

#include <iostream>
#include <cmath>

using namespace std;

const int MAXN = 1e7 + 50;
int prime[MAXN];

void init_prime() {
	prime[0] = prime[1] = 1;
	for (int i = 2; i * i <= MAXN; ++i) {
		if (prime[i]) continue;
		for (int j = 2 * i; j <= MAXN; j += i) {
			prime[j] = 1;
		}
	}
	return;
}

int main() {
	init_prime();
	int a, b;
	scanf("%d%d", &a, &b);
	for (int i = a; i <= b; ++i) {
		if (prime[i] == 0) printf("%d\n", i);
	}
	return 0;
}

方法二:線性篩+巧妙解法

線性篩即 歐拉篩,將素數判斷的時間複雜度優化至 O(n)O(n),其思想仍繼承自上述的埃氏篩,埃氏篩的效率已經很高了,但其不足之處在於仍存在很多數被重複判斷,做了這些不必要的操作必然影響效率。歐拉篩針對這點進行改善。

參見代碼如下:

#include <iostream>
#include <string.h>
#include <cmath>

using namespace std;

const int MAXN = 1e7 + 50;
int prime[MAXN], c = 0;
bool number[MAXN];

void init_prime(int n) {
    for (int i = 2; i <= n; i++) {
        if (!number[i]) prime[c++] = i;
        for (int j = 0; j < c and prime[j] * i <= MAXN; j++) {
            number[prime[j] * i] = true;
            //保證每個合數只會被它的最小質因數篩去,因此每個數只會被標記一次
            if (i % prime[j] == 0)
                break;
        }
    }
}

int main() {
	int a, b;
	scanf("%d%d", &a, &b);
    init_prime(b);
	for (int i = 0; i < c; ++i) {
        if (prime[i] < a) continue;
		else printf("%d\n", prime[i]);
	}
	return 0;
}

prime 數組存放小於 n 的所有素數,其個數爲 c 個。prime 數組中的素數是遞增的,當 i 能整除 prime[j],那麼 i * prime[j + 1] 這個合數肯定被 prime[j] 乘以某個數篩掉。因爲 i 中含有 prime[j]prime[j]prime[j+1] 小,即 i=k*prime[j],那麼 i*prime[j+1]=(k*prime[j])*prime [j+1]=k’*prime[j],接下去的素數同理。所以不用篩下去了。因此,在滿足 i%prime[j]==0 這個條件之前以及第一次滿足改條件時,prime[j] 必定是 prime[j]*i 的最小因子。故不會對一個元素進行重複判斷,將效率提升到 O(n)O(n)

方法三:Miller-Rabbin 素數測試+巧妙解法

這個方法是在前段時間做一個關於 RSA 加密項目瞭解到的,適合大數的素性測試,500 位以上的大數速度也是很快的,在 boost 庫中自帶有大數及 Miller-Rabbin 素數測試函數接口,配合起來使用實在是太方便了。它基於隨機算法,可以在 O(logn)O(logn) 內判斷一個數是否是素數,但存在一定的誤差。其中包含有一部分基礎數論、概率相關知識,就不在這裏展開詳解了,點到此地,有興趣可自行了解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章