zoukankan      html  css  js  c++  java
  • ACM数论之旅13---容斥原理(一切都是命运石之门的选择(=゚ω゚)ノ)

     容斥原理我初中就听老师说过了,不知道你们有没有听过(/≧▽≦)/

    百度百科说:

    在计数时,必须注意没有重复,没有遗漏。

    为了使重叠部分不被重复计算,人们研究出一种新的计数方法。

    这种方法的基本思想是:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复。

    这种计数的方法称为容斥原理。

    好标准的说法(#-.-)

    那我举个简单的例子

    两个集合的容斥原理: 设A, B是两个有限集合

    容斥原理1

    那么

    |A + B| = |A| + |B| - |AB|

    |A|表示A集合中的元素个数

    三个集合的容斥原理: 设A, B, C是三个有限集合

    容斥原理2

    那么

    |A + B + C| = |A| + |B| + |C| - |AB| - |AC| - |BC| + |ABC|

    这就叫容斥原理

    接下来直接做例题了

    全错排(装错信封问题)

    hdu 1465

    http://acm.hdu.edu.cn/showproblem.php?pid=1465

    n封信对应n个信封

    求恰好全部装错了信封的方案数

    本来全错排是有自己的一个公式的,叫全错排公式(跟容斥没关系)

    那我顺便来讲讲全错排( >ω<)

    要装第i封信的时候,先把前i-1个信全装错信封,然后随便选其中一个与第i封信交换,有i-1种选法

    那么dp[i] = (i-1) * dp[i-1]

    但是还有一种情况

    要装第i封信的时候,先从i-1封信中任选i-2个信把他们全装错信封,然后把剩下的那个信与第i个交换,从i-1封信中任选i-2个信有i-1种选法

    那么dp[i] = (i-1) * dp[i-2]

    两个式子联合起来

    就是那么dp[i] = (i-1) * (dp[i-1] + dp[i-2])

    这就是全错排公式,递推,递归都可以做

    全错排递推AC代码:

     1 #include<cstdio>
     2 typedef long long LL;
     3 int n;
     4 LL dp[25];
     5 void init(){
     6     dp[1] = 0;
     7     dp[2] = 1;
     8     for(int i = 3; i <= 20; i ++){
     9         dp[i] = (i-1) * (dp[i-1] + dp[i-2]);
    10     }
    11 }
    12 int main(){
    13     init();
    14     while(~scanf("%d", &n)){
    15         printf("%I64d
    ", dp[n]);
    16     }
    17 }
    View Code

    那么这题容斥怎么做呢?

    首先,所有装信的总数是n!

    (在n中任选一个信封放进一封信,然后在剩下的n-1中任选一个信封放进一封信,以此类推,所以是n*(n-1)*(n-2)... = n!)

     假设

    A1表示1封信装对信封,数量是(n-1)! (只有n-1个位置可以乱放)

    A2表示2封信装对信封,数量是(n-2)! (只有n-2个位置可以乱放)

    ...

    An表示n封信装对信封,数量是1 

    那么这题的答案就是

    n! - C(n, 1)*|A1| + C(n, 2)*|A2| - C(n, 3)*|A3| + ... + (-1)^n * C(n, n)*|A4|

    把C(n, m)用

    组合数1代入式子

    化简

    n! - n! / 1! + n! / 2! - n! / 3! + ... + (-1)^n * n! / n!

    提取n!

    n!(1 - 1/1! + 1/2! - 1/3! + ... + (-1)^n * 1/n!)

    附上容斥AC代码:

     1 #include<cstdio>
     2 typedef long long LL;
     3 int n, flag;
     4 LL fac[25];
     5 LL ans;
     6 void init(){
     7     fac[0] = 1;
     8     for(int i = 1; i <= 20; i ++) fac[i] = fac[i-1] * i;    
     9 }
    10 int main(){
    11     init();
    12     while(~scanf("%d", &n)){
    13         ans = fac[n];
    14         flag = -1;//容斥的符号变化
    15         for(int i = 1; i <= n; i ++){
    16             ans += flag * fac[n] / fac[i];
    17             flag = -flag;  
    18         }
    19         printf("%I64d
    ", ans);
    20     }
    21 }
    View Code

    第二例题:

    UVALive 7040

    https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=5052

    题意:给n盆花涂色,从m种颜色中选取k种颜色涂,保证正好用上k种颜色,你必须用上这k种颜色去涂满n个相邻的花,并且要求相邻花的颜色不同,求方案数。

     (1 ≤ n, m ≤ 1e9 , 1 ≤ k ≤ 1e6 , k ≤ n, m)

    首先,用k种颜色涂花,假如不考虑全部用上,那么总的方案数是多少

    第一盆花有k种颜色选择,之后的花因为不能跟前一盆花的颜色相同,所以有k-1种选择

    于是总方案数为k*(k-1)^(n-1)

    因为题目问必须用上k种颜色

    这里面包含了只用k-1种颜色的情况,应该减掉所有用k-1种的情况

    减掉的东西里面,这里面包含了只用k-2种颜色的情况,应该加回来

    ...

    反反复复,最后就得出答案了(这算是解释吗。。。)

    最后答案就是

    C(m,k) * ( k * (k-1)^(n-1) + [∑((-1)^i * C(k, k - i) * (k-i) * (k-i-1)^(n-1)) ] )    (1 <= i <= k-1)    红色表示容斥部分

    (这里m有1e9,C(m, k)直接用for循环算,直接for循环从m*(m-1)*...*(m-k+1)再乘k的阶乘的逆元)

     AC代码:

     1 #include<cstdio>
     2 typedef long long LL;
     3 const int N = 1000000 + 5;
     4 const int MOD = (int)1e9 + 7;
     5 int F[N], Finv[N], inv[N];
     6 LL pow_mod(LL a, LL b, LL p){ 
     7     LL ret = 1;
     8     while(b){
     9         if(b & 1) ret = (ret * a) % p;
    10         a = (a * a) % p;
    11         b >>= 1;
    12     }
    13     return ret;
    14 }
    15 void init(){
    16     inv[1] = 1;
    17     for(int i = 2; i < N; i ++){
    18         inv[i] = (MOD - MOD / i) * 1ll * inv[MOD % i] % MOD;
    19     }
    20     F[0] = Finv[0] = 1;
    21     for(int i = 1; i < N; i ++){
    22         F[i] = F[i-1] * 1ll * i % MOD;
    23         Finv[i] = Finv[i-1] * 1ll * inv[i] % MOD;
    24     }
    25 }
    26 int comb(int n, int m){
    27     if(m < 0 || m > n) return 0;
    28     return F[n] * 1ll * Finv[n - m] % MOD * Finv[m] % MOD;
    29 }
    30 int main(){
    31     init();
    32     int T, n, m, k, ans, flag, temp;
    33     scanf("%d", &T);
    34     for(int cas = 1; cas <= T; cas ++){
    35         scanf("%d%d%d", &n, &m, &k);
    36         ans = k * pow_mod(k-1, n-1, MOD) % MOD;
    37         flag = -1;
    38         //计算容斥 
    39         for(int i = 1; i <= k-1; i ++){
    40             ans = (ans + 1ll * flag * comb(k, k-i) * (k-i) % MOD * pow_mod((k-i-1), n-1, MOD) % MOD) % MOD;
    41             flag = -flag;
    42         }
    43         //接下来计算C(m, k) 
    44         temp = Finv[k];
    45         for(int i = 1; i <= k; i ++){
    46             temp = 1ll * temp * (m-k+i) % MOD;
    47         }
    48         ans = ((1ll * ans * temp) % MOD + MOD) % MOD;
    49         printf("Case #%d: %d
    ", cas, ans);
    50     }
    51 }
    View Code

    第三例题:(容斥这章的例题我可能会写很多(o^∇^o)ノ预祝玩的开心have fun)

    hdu 4135

    http://acm.hdu.edu.cn/showproblem.php?pid=4135

    题意:就是让你求(a,b)区间与n互质的数的个数.

    我们可以先求(1~b)区间的答案,然后减去(1~a-1)区间的答案

    这样问题就转换为(1~m)区间与n互质的数的个数

    互质的不好求,我们可以求不互质的个数,然后减一下

    所有我们先求出n的所有质因数,然后用容斥做

    AC代码:

     1 #include<cstdio>
     2 #include<vector>
     3 using namespace std;
     4 typedef long long LL;
     5 vector <LL > prime_factor;
     6 vector <LL > vec;
     7 void init(LL x){
     8     //预处理质因子 
     9     prime_factor.clear();
    10     for(LL i = 2; i*i <= x; i++){
    11         if(x % i == 0){
    12             prime_factor.push_back(i);
    13             while(x % i == 0) x /= i;
    14         }
    15     }
    16     if(x > 1) prime_factor.push_back(x);
    17     //预处理容斥中的倍数项,符号正好是一个减一个加    
    18     int vec_size;
    19     vec.clear();
    20     for(int i = 0; i < prime_factor.size(); i ++){
    21         vec_size = vec.size();//因为vec.size()在接下来的运算中会改变 
    22         for(int j = 0; j < vec_size; j ++){
    23             vec.push_back(vec[j] * prime_factor[i]);
    24         }
    25         vec.push_back(prime_factor[i]);
    26     }
    27 }
    28 LL work(LL x){
    29     //接下来容斥
    30     LL ans = x, flag = -1;
    31     for(int i = 0; i < vec.size(); i ++){
    32         ans += flag * x / vec[i];
    33         flag = -flag;
    34     }
    35     return ans;
    36 }
    37 int main(){
    38     int T;    
    39     LL l, r, n;
    40     scanf("%d", &T);
    41     for(int cas = 1; cas <= T; cas ++){
    42         scanf("%I64d%I64d%I64d", &l, &r, &n);
    43         init(n); 
    44         printf("Case #%d: %I64d
    ", cas, work(r) - work(l-1));
    45     }
    46 }
    View Code

    容斥中的那些倍数我是这么处理的

    比如30 = 2 * 3 * 5

    一开始数组里面什么都没有

    然后变成

    2

    然后把3挨个乘过去的值放在数组后面,同时将自己也放进数组

    2 6 3

    然后5也是一样

    2 6 3 10 30 15 5

    最后答案n就是等于

    n - n / 2 + n / 6 - n / 3 + n / 10 - n / 30 + n / 15 - n / 5

    当然,除了数组形式,还可以用位运算来实现容斥

    AC代码:

     1 #include<cstdio>
     2 #include<vector>
     3 using namespace std;
     4 typedef long long LL;
     5 vector <LL > prime_factor;
     6 void init(LL x){
     7     //预处理质因子 
     8     prime_factor.clear();
     9     for(LL i = 2; i*i <= x; i++){
    10         if(x % i == 0){
    11             prime_factor.push_back(i);
    12             while(x % i == 0) x /= i;
    13         }
    14     }
    15     if(x > 1) prime_factor.push_back(x);
    16 }
    17 LL work(LL x){
    18     //接下来容斥
    19     LL ans = x, cnt, temp;
    20     for(int i = 1; i < (1 << prime_factor.size()); i ++){
    21         cnt = 0;
    22         temp = 1;
    23         for(int j = 0; j < prime_factor.size(); j ++){
    24             if(i & (1 << j)){
    25                 temp *= prime_factor[j];
    26                 cnt ++;
    27             }
    28         }
    29         if(cnt & 1) ans -= x / temp;
    30         else ans += x / temp;
    31     }
    32     return ans;
    33 }
    34 int main(){
    35     int T;    
    36     LL l, r, n;
    37     scanf("%d", &T);
    38     for(int cas = 1; cas <= T; cas ++){
    39         scanf("%I64d%I64d%I64d", &l, &r, &n);
    40         init(n); 
    41         printf("Case #%d: %I64d
    ", cas, work(r) - work(l-1));
    42     }
    43 }
    View Code

    第四例题:

    hdu 1695

    http://acm.hdu.edu.cn/showproblem.php?pid=1695

    题意:给你5个数a,b,c,d,k

    在a~b中选一个x, c~d中选一个y,满足gcd(x,y) = k , 求(x,y) 的对数 

    a, b, c, d, k, 0 < a <= b <= 100,000, 0 < c <= d <= 100,000, 0 <= k <= 100,000

    在题目描述的最后一行有一句话,多组里面所有的a和c都是1(这题目不是坑爹吗(╯‵□′)╯︵┻━┻那输入a和c有什么用)

    然后题目变成

    在1~b中选一个x, 1~d中选一个y,满足gcd(x,y) = k , 求(x,y) 的对数 。。。(无语中。。。)

    gcd(x, y) == k 说明x,y都能被k整除, 但是能被k整除的未必gcd=k  , 必须还要满足互质关系

    那么问题就转化为

    求1~b/k 和 1~d/k间,gcd(x,y) = 1对数的问题

    假设b/k小于d/k

    那么当y <= b/k时,就是求1到b/k的欧拉函数的和

    y > b/k时,只好枚举y从b/k到d/k,用第3例题的求法

    这样问题就解决了(注意:k可以等于0,要特判)

    AC代码:

     1 #include<cstdio>
     2 #include<algorithm>
     3 #include<vector>
     4 using namespace std;
     5 typedef long long LL;
     6 const int N = 1e5+10 ;
     7 vector <LL > prime_factor;
     8 int phi[N], prime[N];
     9 int tot;//tot计数,表示prime[N]中有多少质数 
    10 void Euler(){
    11     phi[1] = 1;
    12     for(int i = 2; i < N; i ++){
    13         if(!phi[i]){
    14             phi[i] = i-1;
    15             prime[tot ++] = i;
    16         }
    17         for(int j = 0; j < tot && 1ll*i*prime[j] < N; j ++){
    18             if(i % prime[j]) phi[i * prime[j]] = phi[i] * (prime[j]-1);
    19             else{
    20                 phi[i * prime[j] ] = phi[i] * prime[j];
    21                 break;
    22             }
    23         }
    24     }
    25 }
    26 void getFactors(int x){
    27     prime_factor.clear();
    28     for(int i = 0; prime[i] <= x / prime[i]; i ++){
    29         if(x % prime[i] == 0){
    30             prime_factor.push_back(prime[i]);
    31             while(x % prime[i] == 0) x /= prime[i];
    32         }
    33     }
    34     if(x > 1) prime_factor.push_back(x);
    35 }
    36 LL work(int n, int m){
    37     LL ans = n, cnt, temp;
    38     getFactors(m);
    39     for(int i = 1; i < (1 << prime_factor.size()); i ++){
    40         cnt = 0;
    41         temp = 1;
    42         for(int j = 0; j < prime_factor.size(); j ++){
    43             if(i & (1 << j)){
    44                 temp *= prime_factor[j];
    45                 cnt ++;
    46             }
    47         }
    48         if(cnt & 1) ans -= n / temp;
    49         else ans += n / temp;
    50     }
    51     return ans;
    52 }
    53 int main(){
    54     Euler();
    55     int T, a, b, c, d, k;
    56     LL ans;
    57     scanf("%d", &T);
    58     for(int cas = 1; cas <= T; cas ++){
    59         scanf("%d%d%d%d%d", &a, &b, &c, &d, &k);
    60         if(k == 0){
    61             printf("Case %d: 0
    ", cas);
    62             continue;
    63         }
    64         if(b > d) swap(b, d);//假设b<=d
    65         b /= k; d /= k; 
    66         ans = 0;
    67         for(int i = 1; i <= b; i ++) ans += phi[i];
    68         for(int i = b + 1; i <= d; i ++) ans += work(b, i);
    69         printf("Case %d: %I64d
    ", cas, ans);
    70     }
    71 }
    View Code

    这题时间只能算卡过去的,因为正常计算下来,这样的代码会超时,只是数据水

    这题正确的做法应该是莫比乌斯反演,我们以后会讲到

    容来容去,脑子都乱了。。。。

     >﹏<

  • 相关阅读:
    js数组
    关于编程,程序员的一些语录
    css心得
    js函数
    一些电脑基础知识
    gnome3安装
    C学习小记
    ubuntu重装系统后
    elinks文字浏览器
    快捷方式
  • 原文地址:https://www.cnblogs.com/linyujun/p/5210410.html
Copyright © 2011-2022 走看看