AcWing 196. 質數距離
一、二次篩法
這裡的區間範圍給定的最大值是\(2^{31} - 1\),而用線性篩法求的是\([1,n]\)中的所有質數,因此直接用線性篩法求肯定會直接\(gg\),因此需要通過挖掘某些性質,才能有技巧性的完成。
二、挖掘性質
-
性質\(1\):
若一個數\(n\)是一個合數,必然存在\(2\)個因子\(d\),\(\frac{n}{d}\),假設\(d <=\frac{n}{d}\),則\(d <= \sqrt{n}\),因此必然存在一個小於等於 \(\sqrt{n}\)的因子 -
性質2
若\(x∈[L,R]\),且\(x\)是合數,則一定存在\(P <= \sqrt{2^{31}-1} (< 50000)\)
\(\sqrt{2147483647}=46340\),這個值是小於\(50000\)的,所以,\(yxc\)一般喜歡使用直接寫上上限值\(50000\),就是因為這個數字夠用,而且夠大,好記。
三、實現步驟
- 找出\(1 \sim \sqrt{2^{31}-1}\) \((< 50000)\)中的所有質因子
- 對於\(1 \sim 50000\) 中每個質數\(P\),將\([L,R]\)中所有\(P\)的倍數篩掉(至少\(2\)倍,\(1\)倍的就是自己,是質數不是合數)
找到大於等於\(L\)的最小的\(P\)的倍數\(P_0\),找下一個倍數時只需要\(+= P\)
四、引理
在尋找大於\(L\)的第一個\(P\)的倍數時,採用了下面的引理:
引理(分數的上取整轉換下取整)
五、細節
\(Q\): 每個質數是\(2\sim 50000\)中的數,為啥 LL p = primes[i]
; 這裡的p
的LL
啊, int
不就夠了嗎?
\(A\): LL p = primes[i]
,這裡p
用LL
是因為如果p
也是用int
型別,本身l
也是用int
型別,如果l
取得足夠大,下面的l + p - 1
會有可能直接爆int
變成負數
\(Q\): for (LL j = max(p * 2, (l + p - 1) / p * p); j <= r; j += p)
這裡的 j
r
的,而 r
的取值範圍是小於\(2 ^ {31}\),也是在int
範圍內啊, 這裡為啥用LL
啊?\(A\): 這裡的
j
是小於等於 r
的,而 r
的取值範圍是小於\(2 ^ {31}\),這裡確實是這樣,可是這個迴圈跳出的條件是j <= r
,也就是說如果r
是最大的int
,那麼當j += p
,要超過最大的int
的時候需要比它還大才能跳出迴圈,因此直接爆int
變成負數,然後j <= r
依然成立,會一直死迴圈下去。其實本質上這個問題與問題\(1\)是一回事,在做數論時一次要小心\(LL\)和\(int\)的加法、乘法,小心爆\(int\)是一條死規則,儘量多想想是不是應該用\(LL\)來設定變數~
\(Q\): 我知道這裡是複用st
陣列,但是在用之前都初始化為\(0\)了啊,為什麼init(50000);
放在while (cin >> l >> r)
的外面(也就是最前面)程式碼不行啊?
\(A\):放在最前面也是可以的,\(y\)總的程式碼中判斷從\([1,50000]\)中誰是質數和在區間\([L,R]\)中誰是質數直接複用\(st[]\)陣列,就不用再開一個數組去存了,也可以把init()
放在前面,用一個專門的陣列去記錄區間\([L,R]\)中誰是質數
\(Q\): 為什麼要寫上常數\(50000\)呢?
\(A\):
- 寫法\(1\):
get_primes(50000)
- 寫法\(2\):
get_primes(sqrt(r))
- 寫法\(3\):
get_primes(sqrt(INT32_MAX))
其實都行,但有了經驗後,發現,直接寫\(50000\)最簡單粗暴效果好~
\(Q\): 為什麼要使用偏移量st[j - l] = true
?
\(A\): 因為資料範圍太大,直接用陣列進行桶計數,會超空間,需要離散化一下,就是把\(1e6\)範圍內的資料對映到\(0\sim 1e6\)
\(Q\):為什麼for (int i = 0; i < cnt - 1; i++)
這裡要用cnt-1
呢?為什麼不是cnt
呢?
\(A\):\(cnt\)個質數,下標是\([0,cnt-1]\),因為現在列舉的是前一個質數,要保證還有後一個,所以取不到\(cnt-1\)
六、時間複雜度
\(O(n)\)
七、實現程式碼
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
//尤拉篩
const int N = 1e6 + 10;
int primes[N], cnt; // primes[]儲存所有素數
bool st[N]; // st[x]儲存x是否被篩掉
void get_primes(int n) {
memset(st, 0, sizeof st);
cnt = 0;
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int l, r; //起始值,終止值
while (~scanf("%d%d", &l, &r)) { //注意,這裡在while迴圈中使有scanf,必須要使用~
get_primes(50000);
memset(st, 0, sizeof st); //給st陣列以新的定義,用於判斷[l,r]之間某個數字是不是可以被列舉的小質數因子篩掉
for (int i = 0; i < cnt; i++) { //列舉篩出的所有小質數因子
LL p = primes[i];
//將[l,r]之內,找出P的第一個整數倍(>=2)的數字,
for (LL j = max(p * 2, (l + p - 1) / p * p); j <= r; j += p)
st[j - l] = true; //防止陣列越界,採用了一個偏移l的辦法,可以理解為離散化
}
cnt = 0; //清空了primes陣列,廢物利用啊
for (int i = 0; i <= r - l; i++) //這裡也使用了偏移l的辦法
if (!st[i] && i + l >= 2) //如果i+l沒有被篩掉,表示是質數,並且>=2,表示是真的質數
primes[cnt++] = i + l; //將數字i+l記錄到質數陣列中
if (cnt < 2)
puts("There are no adjacent primes."); //如果質數數量不足2個
else {
int minp = 0, maxp = 0;
// cnt個質數,下標是[0,cnt-1],因為現在列舉的是前一個質數,要保證還有後一個,所以取不到cnt-1
for (int i = 0; i < cnt - 1; i++) {
int d = primes[i + 1] - primes[i]; //計算相鄰兩個質數的差值
if (d < primes[minp + 1] - primes[minp]) minp = i; //對比記錄最小質數差位置
if (d > primes[maxp + 1] - primes[maxp]) maxp = i; //對比記錄最大質數差位置
}
printf("%d,%d are closest, %d,%d are most distant.\n",
primes[minp], primes[minp + 1],
primes[maxp], primes[maxp + 1]);
}
}
return 0;
}