1. 程式人生 > 其它 >容斥原理初探

容斥原理初探

技術標籤:數學 - 容斥原理

整理的演算法模板合集: ACM模板

點我看演算法全家桶系列!!!

實際上是一個全新的精煉模板整合計劃


以下內容摘自 我的文章:演算法競賽中的數論問題 - 數論全家桶(信奧 / 數競 / ACM)作者孟繁宇四萬字十三萬字元競賽數論完全總結,將會擇機發布,敬請期待 ~

0x15 容斥原理初探

容斥原理是一種應用在集合上的較常用的計數方法,其基本思想是:先不考慮重疊的情況,把包含於某內容中的所有物件的數目先計算出來(),然後再把計數時重複計算的數目排斥出去(),使得計算的結果既無遺漏又無重複。

容斥原理核心的計數規則可以歸為一句話:奇加偶減

假設被計數的有 A 、 B 、 C A、B、C

ABC 三類,那麼, A 、 B 、 C A、B、C ABC 類元素個數總和 = = = A A A 類元素個數 + B +\ B +B 類元素個數 + C +\ C +C 類元素個數 − - 既是 A A A 又是 B B B 的元素個數 − - 既是 A A A 又是 C C C 的元素個數 − - 既是 B B B 又是 C C C 的元素個數 + + + 既是 A A A 又是 B B B 且是 C C C 的元素個數。

即: A ∪ B ∪ C = A + B + C − A B − B C − A C + A B C A∪B∪C = A+B+C - AB - BC - AC + ABC

ABC=A+B+CABBCAC+ABC

img

當被計數的種類被推到 n n n 類時,其統計規則遵循奇加偶減

  • 競賽例題選講

Problem A Co-prime(HDU 4135)

[ a , b ] [a,b] [a,b] 區間與 n n n 互質的數的個數,其中 1 ≤ a , b , n ≤ 2 31 1\le a,b,n\le2^{31} 1a,b,n231

Solution

容斥定理最常用於求解 [ a , b ] [a,b] [a,b] 區間與 n n n 互質的數的個數問題,該問題可以視為求 [ 1 , b ] [1,b] [1,b] 區間與 n n

n 互質的個數減去 [ 1 , a − 1 ] [1,a-1] [1,a1] 區間內與 n n n 互質的個數。

那麼我們這裡只需要考慮它的一個子問題:

求小於等於 m m m n n n 互素的數的個數。

我們知道當 m m m 等於 n n n ,就是一個簡單的尤拉函式問題,但是一般 m m m 都不等於 n n n ,我們考慮將 n n n 質因數分解。

我們首先分析最簡單的情況:當 n n n 為素數的冪,即 n = p k n = p^k n=pk 時,那麼顯然答案就等於 m − m p m - \cfrac{m}{p} mpm (其中 m p \cfrac{m}{p} pm 表示的是 p p p 的倍數, 1 1 1 ~ m m m 中去掉 p p p 的倍數,剩下的就都是與 n n n 互素的數了)

然後再來討論 n n n 是兩個素數的冪的乘積,即 n = p 1 k 1 × p 2 k 2 n = p_1^{k_1} \times p_2^{k_2} n=p1k1×p2k2,那麼我們需要做的就是找到 p 1 p_1 p1 的倍數和 p 2 p_2 p2 的倍數,並且要減去 p 1 p_1 p1 p 2 p_2 p2 的公倍數,我們發現這實際上就是容斥原理,所以這種情況下答案為: m − ( m p 1 + m p 2 − m p 1 × p 2 ) m - ( \cfrac{m}{p_1} + \cfrac{m}{p_2} - \cfrac{m}{p_1\times p_2} ) m(p1m+p2mp1×p2m)

這裡的 + + + 就是 − - 就是 ,並且 總是交替進行的(一個的加上,兩個的減去,三個的加上,四個的減去),而且可以推廣到 n n n 個元素的情況,如果 n n n 分解成 s s s 個素因子,也同樣可以用容斥原理求解。

容斥原理其實是列舉子集的過程,常見的列舉方法為 d f s dfs dfs ,也可以採用二進位制法( 0 0 0 表示取, 1 1 1 表示不取)。

例如我們求 [ 1 , 9 ] [1, 9] [1,9] 中和 6 6 6 互素的數的個數,這時 6 6 6 分解的素因子為 2 2 2 3 3 3

a n s = 9 − ( 9 2 + 9 3 ) + 9 6 = 3 ans = 9 - (\cfrac{9}{2} + \cfrac{9}{3}) + \cfrac{9}{6} = 3 ans=9(29+39)+69=3,其中, a n s ans ans 分為三部分, 0 0 0 個數的組合, 1 1 1 個數的組合, 2 2 2 個數的組合。

答案 = ( 1 1 1 ~ b b b 的元素個數) − - 1 1 1 ~ a − 1 a-1 a1 的元素個數) − - 1 1 1 ~ b b b 中與 n n n 不互質的數的個數) + + + 1 1 1 ~ a − 1 a-1 a1 中與 n n n 互質的數的個數)

Code

const int N = 10005;
ll n, primes[N], cnt, factor[N], num;
bool vis[N];
inline void get_primes(int n)
{
	for(register int i = 2;i <= n;i ++) {
    	if(!vis[i]) primes[ ++ cnt] = i;
    	for(register int j = 1;j <= cnt && i * primes[j] <= n; ++ j) {
        	vis[i * primes[j]] = 1;
            if(i % primes[j] == 0) break;
    	}
    }
}
void get_factor(int n) {
    num = 0;
    for (ll i = 1; primes[i] * primes[i] <= n && i <= cnt; i ++ ) {
        if (n % primes[i] == 0) { //記錄n的因子
            factor[num ++ ] = primes[i];
            while (n % primes[i] == 0)
                n /= primes[i];
        }
    }
    if (n != 1) //1既不是素數也不是合數
        factor[num ++ ] = n;
}
ll solve(ll m, ll num) {
    ll res = 0;
    for (ll i = 1; i < (1 << num); i++) {
        ll sum = 0;
        ll temp = 1;
        for (ll j = 0; j < num; j++) {
            if (i & (1 << j)) {
                sum ++ ;
                temp *= factor[j];
            }
        }
        if (sum % 2) res += m / temp;
        else res -= m / temp;
    }
    return res;
}
int kcase, t;
int main() {
    get_primes(N - 1);
    scanf("%d", &t);
    while(t -- ) {
        ll a, b, n;
        scanf("%lld%lld%lld", &a, &b, &n);
        get_factor(n);
        //容斥定理,奇加偶減,
        ll res = (b - (a - 1) - solve(b, num)) + solve(a - 1, num);
        printf("Case #%d: %lld\n", ++ kcase, res);
    }
    return 0;
}

Problem B Helping Cicada(LightOJ - 1117)

[ 1 , n ] [1,n] [1,n] 中不能被 m m m 個數整除的個數。

Solution

我們知道對於任意一個數 k k k ,在 [ 1 , n ] [1,n] [1,n] 中有 ⌊ n k ⌋ \lfloor\cfrac{n}{k}\rfloor kn 個數是 k k k 的倍數,也就是能被 k k k 整除,故 a n s = n − ∑ i = 1 n ⌊ n a [ i ] ⌋ ans=n-\sum_{i=1}^n\lfloor\cfrac{n}{a[i]}\rfloor ans=ni=1na[i]n

我們再來考慮兩個數 a , b a,b a,b 的情況,因為 a , b a,b a,b 的公倍數 l c m ( a , b ) lcm(a,b) lcm(a,b),即被 a a a 整除,又被 b b b 整除,所以減了兩次。所以最後的答案要加上 ⌊ n l c m ( a , b ) ⌋ \lfloor\cfrac{n}{lcm(a,b)}\rfloor lcm(a,b)n 。對於三個數 a , b , c a,b,c a,b,c 來說,答案就要減去 ⌊ n l c m ( a , b , c ) ⌋ \lfloor\cfrac{n}{lcm(a,b,c)}\rfloor lcm(a,b,c)n ,以此類推

最後拓展到 m m m 個數的情況,我們發現需要使用容斥定理。

根據容斥定理的奇加偶減,對於 m m m 個數來說,其中的任意 2 、 4 、 ⋯ 、 2 k 2、4、\cdots、2k 242k 個數就要減去他們最小公倍數能組成的數, 1 、 3 、 ⋯ 、 2 k + 1 1、3、\cdots、2k+1 132k+1 個數就要加上他們的最小公倍數,對於每一個數來說,我們都有選或不選兩種情況,因此 m m m 個數就有 2 m 2^m 2m 種情況,也就是從 0 0 0(啥也不選)到 2 m − 1 2^m-1 2m1,對應二進位制就是 m m m 1 1 1,也就意味著 我們選擇了 m m m 個數。這種方法叫做狀態壓縮,我們接用狀態壓縮來列舉所有的可能狀態,依次使用位運算判斷當前的狀態選擇了多少個數,然後再進行奇加偶減即可。

s u m sum sum = = = (從 m m m 中選 1 1 1 個數得到的倍數的個數) − - (從 m m m 中選 2 2 2 個數得到的倍數的個數) + + + (從 m m m 中選 3 3 3 個數得到的倍數的個數) − - (從 m m m 中選 4 4 4 個數得到的倍數的個數)……

那麼能被整除的數的個數就是 s u m sum sum,不能被整除的數的個數就是 n − s u m n-sum nsum

Code

const int N = 10005;
ll LCM(ll a, ll b) {
    return a / __gcd(a, b) *b;
}
int m, kcase;
ll n, a[N];
int main()
{
    int t;
    scanf("%d", &t);
    while(t -- ) {
        scanf("%lld%d", &n, &m);
        for(int i = 0; i < m; ++ i)
            scanf("%lld", &a[i]);
        ll sum = 0;
        for(int i = 0; i < (1 << m); ++ i) {
            ll lcm = 1;
            ll cnt = 0;
            for(int j = 0; j < m; ++ j) {
                if(i >> j & 1) {//如當前的狀態i選擇了第j個數
                    lcm = LCM(lcm, a[j]);//那就選第j個數
                    cnt ++ ;
                }
            }
            if(cnt != 0) {
                if(cnt & 1) sum += n / lcm;//奇加
                else sum -= n / lcm;//偶減
            }
        }
        printf("Case %d: %lld\n", ++ kcase, n - sum);
    }
    return 0;
}