1. 程式人生 > 其它 >AcWing 196. 質數距離

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)\)

    ,使得\(P\)能整除\(x\),其中\(P < x\)
    \(\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]; 這裡的pLL啊, int 不就夠了嗎?
\(A\): LL p = primes[i],這裡pLL是因為如果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;
}