容斥原理

转载声明:原作:e-maxx(Russia) 发表于 2011.8.25 翻译:vici
原文地址http://www.cppblog.com/vici/archive/2011/09/05/155103.html
小红字部分是自己的随笔

容斥原理是一种重要的组合数学方法,可以让你求解任意大小的集合,或者计算复合事件的概率。
容斥原理描述如下:

要计算几个集合并集的大小,我们要先将所有单个集合的大小计算出来,求和,然后减去所有两个集合相交的部分,再加回所有三个集合相交的部分,再减去所有四个集合相交的部分,依此类推,一直计算到所有集合相交的部分。

关于集合的原理公式:

 关于集合的原理公式

我们将B作为所有Ai的集合,那么容斥原理就变成了:

这里写图片描述

维恩图的原理:
用维恩图来表示集合A、B和C:
这里写图片描述
那么这里写图片描述的面积就是集合A、B、C各自面积之和减去12这里写图片描述的面积,再加上这里写图片描述的面积。
这里写图片描述
由此,我们也可以解决n个集合求并的问题。

关于概率论的原理:
设事件这里写图片描述这里写图片描述 代表发生某些事件的概率(即发生其中至少一个事件的概率),则:这里写图片描述
这个公式也可以用B代表Ai的集合:
这里写图片描述

对于实际问题的应用:

首先,我们用三个简单的例子来阐释这个理论。

一个简单的排列问题

由0到9的数字组成排列,要求第一个数大于1,最后一个数小于8,一共有多少种排列? 我们可以来计算它的逆问题,即第一个元素<=1或者最后一个元素>=8的情况。 我们设第一个元素<=1时有X组排列,最后一个元素>=8时有Y组排列。

那么通过容斥原理来解决就可以写成:这里写图片描述

经过简单的组合运算,我们得到了结果:这里写图片描述
然后被总的排列数10!减,就是最终的答案了。

(0,1,2)序列问题

长度为n的由数字0,1,2组成的序列,要求每个数字至少出现1次,这样的序列有多少种? 同样的,我们转向它的逆问题。也就是不出现这些数字的序列 不出现其中某些数字的序列。

我们定义Ai(i=0…2)表示不出现数字i的序列数,那么由容斥原理,我们得到该逆问题的结果为:这里写图片描述
可以发现每个Ai的值都为2^n(因为这些序列中只能包含两种数字)。而所有的两两组合这里写图片描述都为1(它们只包含1种数字)。最后,三个集合的交集为0。(因为它不包含数字,所以不存在) 要记得我们解决的是它的逆问题,所以要用总数减掉,得到最终结果:这里写图片描述

(0,1,2)方程整数解问题

给出一个方程:这里写图片描述
其中这里写图片描述
求这个方程的整数解有多少组。

我们先不去理会xi<=8的条件,来考虑所有正整数解的情况。这个很容易用组合数来求解,我们要把20个元素分成6组,也就是添加5块“夹板”,然后在25个位置中找5块“夹板”的位置。
这里写图片描述
然后通过容斥原理来讨论它的逆问题,也就是x>=9时的解。
我们定义Ak为xk>=9并且其他xi>=0时的集合,同样我们用上面的添加“夹板”法来计算Ak的大小,因为有9个位置已经被xk所利用了,所以:
这里写图片描述
然后计算两个这样的集合Ak、Ap的交集:
这里说的是有2个xi>=9的情况
这里写图片描述
因为所有x的和不能超过20,所以三个或三个以上这样的集合时是不能同时出现的,它们的交集都为0。最后我们用总数剪掉用容斥原理所求逆问题的答案,就得到了最终结果:
这里写图片描述

讨论一些复杂问题,试看如何用容斥原理来解决它们。
(0,1,2)求指定区间内与n互素的数的个数:

给出整数n和r。求区间[1;r]中与n互素的数的个数。

公因数只有1的两个非零自然数,叫做互素数
去解决它的逆问题,求不与n互素的数的个数。 考虑n的所有素因子pi(i=1…k) 在[1;r]中有多少数能被pi整除呢?它就是:这里写图片描述
这里的符号是向下取整
然而,如果我们单纯将所有结果相加,会得到错误答案。有些数可能被统计多次(被好几个素因子整除)。所以,我们要运用容斥原理来解决。
我们可以用2^k的算法求出所有的pi组合,然后计算每种组合的pi乘积,通过容斥原理来对结果进行加减处理。
关于此问题的最终实现:

int solve (int n, int r) {
        vector<int> p;
        for (int i=2; i*i<=n; ++i)
               if (n % i == 0) {
                       p.push_back (i);
                       while (n % i == 0)
                               n /= i;
               }
        if (n > 1)
               p.push_back (n);

        int sum = 0;
        for (int msk=1; msk<(1<<p.size()); ++msk) {
               int mult = 1,
                       bits = 0;
               for (int i=0; i<(int)p.size(); ++i)
                       if (msk & (1<<i)) {
                               ++bits;
                               mult *= p[i];
                       }

               int cur = r / mult;
               if (bits % 2 == 1)
                       sum += cur;
               else
                       sum -= cur;
        }

        return r - sum;
}

算法的复杂度为 这里写图片描述

求在给定区间内,能被给定集合至少一个数整除的数个数

给出n个整数ai和整数r。求在区间[1;r]中,至少能被一个ai整除的数有多少。

决此题的思路和上题差不多,计算ai所能组成的各种集合(这里将集合中ai的最小公倍数作为除数)在区间中满足的数的个数,然后利用容斥原理实现加减。
此题中实现所有集合的枚举,需要2^n的复杂度,求解lcm需要O(nlogr)的复杂度。

能满足一定数目匹配的字符串的个数问题

给出n个匹配串,它们长度相同,其中有一些’?’表示待匹配的字母。然后给出一个整数k,求能正好匹配k个匹配串的字符串的个数。更进一步,求至少匹配k个匹配串的字符串的个数。

首先我们会发现,我们很容易找到能匹配所有匹配串的字符串。只需要对比所有匹配串,去在每一列中找出现的字母(或者这一列全是’?’,或者这一列出现了唯一的字母,否则这样的字符串就存在),最后所有字母组成的单词即为所求。
现在我们来学习如何解决第一个问题:能正好匹配k个匹配串的字符串。
我们在n个匹配串中选出k个,作为集合X,统计满足集合X中匹配的字符串数。求解这个问题时应用容斥原理,对X的所有超集进行运算,得到每个X集合的结果:
这里写图片描述
此处f(Y)代表满足匹配集合Y的字符串数。
如果我们将所有的ans(X)相加,就可以得到最终结果:
这里写图片描述
这样,就得到了一个复杂度这里写图片描述的解法。
这个算法可以作一些改进,因为在求解ans(X)时有些Y集合是重复的。
优化如下:
这里写图片描述

路径的数目问题

在一个的方格阵中,有k个格子是不可穿越的墙。一开始在格子(1,1)(最左下角的格子)中有一个机器人。这个机器人只能向上或向右行进,最后它将到达位于格子(n,m)的笼子里,其间不能经过障碍物格子。求一共有多少种路线可以到达终点。

为了方便区分所有障碍物格子,我们建立坐标系,用(x,y)表示格子的坐标。
首先我们考虑没有障碍物的时候:也就是如何求从一个点到另一个点的路径数。如果从一个点在一个方向要走x个格子,在另一个方向要走y个格子,那么通过简单的组合原理可以得知结果为:
这里写图片描述
它是一个非多项式的解法,复杂度这里写图片描述

给出n个数,从其中选出4个数,使它们的最大公约数为1,问总共有多少中取法。
我们解决它的逆问题:求最大公约数d>1的四元组的个数。
运用容斥原理,将求得的对于每个d的四元组个数的结果进行加减。
这里写图片描述
其中deg(d)代表d的质因子个数,f(d)代表四个数都能被d整除的四元组的个数。
求解f(d)时,只需要利用组合方法,求从所有满足被d整除的ai中选4个的方法数。
然后利用容斥原理,统计出所有能被一个素数整除的四元组个数,然后减掉所有能被两个素数整除的四元组个数,再加上被三个素数整除的四元组个数…

和睦数三元组的个数问题

这里写图片描述
首先,我们考虑它的逆问题:也就是不和睦三元组的个数。

然后,我们可以发现,在每个不和睦三元组的三个元素中,我们都能找到正好两个元素满足:它与一个元素互素,并且与另一个元素不互素。

所以,我们只需枚举2到n的所有数,将每个数的与其互素的数的个数和与其不互素的数的个数相乘,最后求和并除以2,就是要求的逆问题的答案。

现在我们要考虑这个问题,如何求与2到n这些数互素(不互素)的数的个数。虽然求解与一个数互素数的个数的解法在前面已经提到过了,但在此并不合适,因为现在要求2到n所有数的结果,分别求解显然效率太低。

所以,我们需要一个更快的算法,可以一次算出2到n所有数的结果。

在这里,我们可以使用改进的埃拉托色尼筛法。

· 首先,对于2到n的所有数,我们要知道构成它的素数中是否有次数大于1的,为了应用容斥原理,我们还有知道它们由多少种不同的素数构成。

对于这个问题,我们定义数组deg[i]:表示i由多少种不同素数构成,以及good[i]:取值true或false,表示i包含素数的次数小于等于1是否成立。

再利用埃拉托色尼筛法,在遍历到某个素数i时,枚举它在2到n范围内的所有倍数,更新这些倍数的deg[]值,如果有倍数包含了多个i,那么就把这个倍数的good[]值赋为false。

· 然后,利用容斥原理,求出2到n每个数的cnt[i]:在2到n中不与i互素的数的个数。

回想容斥原理的公式,它所求的集合是不会包含重复元素的。也就是如果这个集合包含的某个素数多于一次,它们不应再被考虑。

所以只有当一个数i满足good[i]=true时,它才会被用于容斥原理。枚举i的所有倍数i*j,那么对于i*j,就有N/i个与i*j同样包含i(素数集合)的数。将这些结果进行加减,符号由deg[i](素数集合的大小)决定。如果deg[i]为奇数,那么我们要用加号,否则用减号。
程序实现:

int n;
bool good[MAXN];
int deg[MAXN], cnt[MAXN];

long long solve() {
         memset (good, 1, sizeof good);
         memset (deg, 0, sizeof deg);
         memset (cnt, 0, sizeof cnt);

         long long ans_bad = 0;
         for (int i=2; i<=n; ++i) {
                 if (good[i]) {
                          if (deg[i] == 0) deg[i] = 1;
                          for (int j=1; i*j<=n; ++j) {
                                   if (j > 1 && deg[i] == 1)
                                            if (j % i == 0)
                                                    good[i*j] = false;
                                            else
                                                    ++deg[i*j];
                                   cnt[i*j] += (n / i) * (deg[i]%2==1 ? +1 : -1);
                          }
                 }
                 ans_bad += (cnt[i] - 1) * 1ll * (n - cnt[i] - 1);
         }
         return (n-1) * 1ll * (n-2) * (n-3) / 6 - ans_bad / 2;
}

最终算法的复杂度为这里写图片描述 ,因为对于大部分i都要进行n/i次枚举。

错排问题
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 JavaScript 编写的记忆游戏(附源代码)   项目:JavaScript 记忆游戏(附源代码) 记忆检查游戏是一个使用 HTML5、CSS 和 JavaScript 开发的简单项目。这个游戏是关于测试你的短期 记忆技能。玩这个游戏 时,一系列图像会出现在一个盒子形状的区域中 。玩家必须找到两个相同的图像并单击它们以使它们消失。 如何运行游戏? 记忆游戏项目仅包含 HTML、CSS 和 JavaScript。谈到此游戏的功能,用户必须单击两个相同的图像才能使它们消失。 点击卡片或按下键盘键,通过 2 乘 2 旋转来重建鸟儿对,并发现隐藏在下面的图像! 如果翻开的牌面相同(一对),您就赢了,并且该对牌将从游戏中消失! 否则,卡片会自动翻面朝下,您需要重新尝试! 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox, 以获得更好、更优化的游戏体验。要玩游戏,首先,通过单击 memorygame-index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
使用 JavaScript 编写的 Squareshooter 游戏及其源代码   项目:使用 JavaScript 编写的 Squareshooter 游戏(附源代码) 这款游戏是双人游戏。这是一款使用 JavaScript 编写的射击游戏,带有门户和强化道具。在这里,每个玩家都必须控制方形盒子(作为射手)。这款射击游戏的主要目标是射击对手玩家以求生存。当它射击对手时,它会获得一分。 游戏制作 该游戏仅使用 HTML 和 JavaScript 开发。该游戏的 PC 控制也很简单。 对于玩家 1: T:朝你上次动作的方向射击 A:向左移动 D:向右移动 W:向上移动 S:向下移动 对于玩家2: L:朝你上次移动的方向射击 左箭头:向左移动 右箭头:向右移动 向上箭头:向上移动 向下箭头:向下移动 游戏会一直进行,直到您成功射击对手或对手射击您为止。游戏得分显示在顶部。所有游戏功能均由 JavaScript 设置,而布局和其他次要功能则由 HTML 设置。 如何运行该项目? 要运行此项目,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox。要运行此游戏,首先,通过单击 index.html 文件在浏览器中打开项目。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值