1. 程式人生 > 其它 >890. 能被整除的數

890. 能被整除的數

題目傳送門

一、容斥原理理論知識

韋恩圖(又稱文氏圖)

(1)兩個圓相交的那部分面積。

$S=S_1+S_2-S_1\cap S_2$

(2)三個圓相交的那部分面積。

\(S=S_1+S_2+S_3- S_1\cap S_2 -S_2\cap S_3 - S_1\cap S_3 + S_1\cap S_2 \cap S_3\)

遇事不決,小學數學。

(3)四個圓相交的那部分面積。


$S=S_1+S_2 + S_3 + S_3 \( \)\ \ \ -S_1\cap S_2 -S_1\cap S_3 -S_1\cap S_4 - S_2\cap S_3 -S_2\cap S_4 -S_3\cap S_4\( \)

\ \ + S_1\cap S_2 \cap S_3 + S_1\cap S_2 \cap S_4+ S_2\cap S_3 \cap S_4 ++ S_1\cap S_3 \cap S_4\( \)\ \ - S_1 \cap S_2 \cap S_3 \cap S_4$

上面,我們是用面積來考慮的問題,所以等式左邊寫的是S,也可以用集合來考慮,那就是
\(|S_1 \cup S_2 \cup S_3| =|S_1|+|S_2|+|S_3|- |S_1\cap S_2| -|S_2\cap S_3| - |S_1\cap S_3| + |S_1\cap S_2 \cap S_3|\)

其中\(|\)

代表集合中的元素個數。

上面專案的個數有什麼規律嗎?

上面的求解過程,其實是在求 \(C_n^1 - C_n^2+ ... + {(-1)}^{n-1}C_n^n\)
也就理解為從n個選擇1個,減去從n中選擇2個,加上從n中選擇3個,減去從n中減去4個...,也可以記為奇數個元素的集合是加,偶數個的(指相交)的集合是減。

根據數學組合數定理,有如下的事實定理:$ C_n^0 +C_n^1 + C_n^2+ ... +C_nn=2n $
證明
等式右邊=從n個數中選任意個數的方案數。
等式左邊=從n個數中選擇0個數的方案數量+從n個數中選擇1個數的方案數量+...
它們倆個的實際意義是一樣的,所以是成立的!

根據上面的證明,運算的式子就是沒有 \(C_n^0\) 這一項,
也就是在求:$C_n^1 + C_n^2+ ... +C_nn=2n -C_n^0 =2^n-1 $

所以,一定會有\(2^n-1\)項,所以時間複雜度是\(2^n\),注意:這裡這所以每個組合數之間都用加法,是因為我們想知道到底我們需要計算多少次,是次數的和,所以一起在用加法,真正的容斥原理的計算是加減加減的,一定要區分開!

經典例題1

容斥原理有個經典題目:一個班每個人都有自己喜歡的科目,有20人喜歡數學,10人喜歡語文,11人喜歡英語,其中3人同時喜歡數學語文,3人同時喜歡語文英語,4人同時喜歡數學英語,2人都喜歡,問全班有多少人?

這個肯定不可以20+10+11的簡單計算,因為有人喜歡多個科目,會重複計算,在之前基礎上-3-3-4,這時候會發現全部都喜歡的被多減了,再+2。得到班級人數=20+10+11-3-3-4+2

經典例題2

問題:我們在1-10中,找出能被質數2和3整除的數的個數是幾個?

方法1:雙重迴圈,挨個去算模是不是等於零,是的話,就count++.
如此辦法,時間複雜度為: \(O(n \times m)\) ,其中n是指數字的個數,本題為10,m是指待檢查的質數個數,本題個數是2.
可是,如果題目要求n特別大,比如1e9,那麼n*m我們是不會在1秒內解決問題的,就是,演算法複雜,需要優化。

方法2:容斥原理
\(S_2= \\{2,4,6,8,10 \\} \ S_3=\\{3,6,9\\}\)

原題其實是在求解:
$|S_2 \cup S_3|=|S_2|+|S_3| -|S_2 \cap S_3| $= 5+3-1=7

容斥原理來算的話,時間複雜度是 \(2^n\),本題n其實就是質數的個數,也就是走的m的資料範圍,是1<=m<=16,就是\(2^{16}=65536\),那麼時間確實是降低了。

二、容斥原理的數學表示式

\[\small \sum_{1<=i<=n}|A_i| - \sum_{1<=i<j<=n}|A_i \cap A_j| + \sum_{1<=i<j<k<=n}|A_i \cap A_j \cap A_k| - ...+{(-1)}^{n-1}|A_1 \cap A_2 \cap ... \cap A_n| \]

比如三個質數是2,3,5,那麼:\(S_2\)就代表2的倍數集合,\(S_3\)就代表3的倍數集合,\(S_5\)就代表5的倍數集合,
$ \small |S_2 \cup S_3 \cup S_5| = |S_2| + |S_3| +|S_5| -|S_2 \cap S_3| -|S_3 \cap S_5| -|S_2 \cap S_5|+|S_2 \cap S_3 \cap S_5|$

(1)如何計算\(|S_2|\)這樣的表示式數值呢?

\(|S_p|\)就是計算1-n中p的倍數集合中的元素個數。它就是等於\(\lfloor \frac{n}{p} \rfloor\),也就是想知道在n中有多少個p,比如,1p,2p,3p,...,kp個。這樣上面式子中的單獨專案我們就會算了,就是算一個\(\lfloor \frac{n}{p}\rfloor\)就可以了。而C++在整數運算除法時,預設就是下取整,這點就不用再特意處理了。

(2)如何計算\(|S_2 \cap S_3|\)這樣的的表示式值呢?

題目給定的\(p_i\)都是質數,所以能被2整除,也能被3整除的數,肯定是能被6整除的數,所以就是 \(|S_6|=|S_2 \cap S_3|\),這樣,問題就轉化成了問題1,也就會求解了!

(3)泛化找到通用公式【順便討論一下它的時間複雜度】

\(|S_{p1} \cap S_{p2} \cap S_{p3} \cap ... \cap S_{pm}| = \lfloor \frac{n}{p_1 \times p_2 \times p_3 \times ... \times p_m} \rfloor\)
上式子的時間複雜度是\(O(m)\)的。整體的時間複雜度就是\(O(2^m \times m)\),本題m=16,就是
\(O(2^{16} \times 2^4)\)=65536*16=100W左右。約等於1e7,就是C++一秒可回!這裡為什麼是\(2^m\),是因為按2,3,5的倍數進行劃分的集合,其實數量是很少的,只有區區3個,也就是說,按容斥原理,運算的時間複雜度是\(O(2^3)\)

(4)怎麼樣把這些子專案都羅列出來

走到了這一步,容斥原理基本上沒有障礙了,因為我們知道公式,還知道公式的第一個子專案怎麼求解,是不是可以編碼了?嘿嘿,還有最後一個攔路虎:怎麼樣把這些子專案都羅列出來?而且是要有前面的正負號的,都羅列出來(帶著正負號),然後加在一起就是答案了。
我們先來明確:我們要羅列的是什麼東西?

就是p[i]的所有組合方式!舉個栗子: {2},{3},{5},{2,3},{3,5},{2,5},{2,3,5}共7種,也就是\(2^3-1=7\)種。
如何不重不漏的把這七種可能都遍歷到呢?這裡使用的是演算法競賽中非常常用的遍歷所有可能的方法:二進位制遍歷!

這裡需要注意的是,迴圈是從1開始的,至 \(2^3-1\)止,為什麼不是從零開始呢?因為如果是從零開始,表示的是三個質數一個也不要!這與本題的要求不符,所以排除了零。

那我們如何知道一個數的每一位是不是1呢?
So Easy,我們可以利用位運算知道第k位是不是1呢? i>> k&1 就是了!

C++ 程式碼

#include <iostream>
using namespace std;

typedef long long LL;
const int N = 20;
int p[N]; //p是質數集合,最多有m項,1≤m≤16,我們開了20個,肯定夠用了。

int main() {
    int n, m; //n個整數,m是質數個數
    cin >> n >> m;
    
    //讀入了m個質數
    for (int i = 0; i < m; i++) cin >> p[i]; 
    //最後的結果
    int res = 0;
    //不重不漏的遍歷所有質數的組合情況,但要排除掉所有質數都不選擇的情況,即i>0
    for (int i = 1; i < 1 << m; i++) { 
        int t = 1, cnt = 0;             //t代表當前所有質數的乘積,cnt當前選法裡面包含幾個1,奇數個是相加的,偶數個是相減的   
        for (int j = 0; j < m; j++)     // 遍歷pj陣列,注意:這裡的下標是從0開始的!
            if (i >> j & 1) {           //這一位是不是1,是1,表示選擇了當前位置的質數  
                cnt++;                  //多選擇了一個集合進行交集運算
                //如果質數乘積大於n了,就不用再繼續算了。換句話說,這是這種選擇法是不符合要求的,這種組合不合法
                //舉個簡單的栗子: p={2,3,5},而n={1,2,3,4,5,6,7,8,9,10},如果三個質數都選擇了,就是30,這對於最大是10的陣列來講是無效的選擇法
                if ((LL) t * p[j] > n) {
                    t = -1; //標識為此次質數組合選擇無效,不用處理,找下一個組合就行了。
                    break;
                }
                t *= p[j];   //質數相乘
            }
        //這裡是在拼容斥原理的公式
        if (t != -1) {
            if (cnt % 2) res += n / t;      //奇數項加,C++的整數除法預設是下取整,不用再關心取整問題
            else res -= n / t;              //偶數項減
        }
    }
    cout << res << endl;
    return 0;
}