1.简单容斥原理
引例:
假设班里有10个学生喜欢数学,15个学生喜欢语文, 21个学生喜欢编程,班里至少喜欢一门学科的有多少个学生呢?
答案显然不会是10+15+21,因为有些学生可能同时喜欢数学和语文,或者语文和编程,甚至还有可能三者都喜欢。为了叙述方便,我们把喜欢语文、数学、编程的学生集合分别用集合A,B,C表示。答案就是下面这个韦恩图中三个集合取交集的部分。
把上述问题推广到一般情况,就是我们熟知的容斥原理。
容斥原理是在计数时,必须注意没有重复,没有遗漏。于是人们想出来的一种计数方法:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复。用两个集合来讲就是
∣
A
∪
B
∣
=
∣
A
∣
+
∣
B
∣
−
∣
A
∩
B
∣
|A∪B| = |A|+|B| - |A∩B |
∣A∪B∣=∣A∣+∣B∣−∣A∩B∣,如果是三个集合的话就是
∣
A
∪
B
∪
C
∣
=
∣
A
∣
+
∣
B
∣
+
∣
C
∣
−
∣
A
∩
B
∣
−
∣
B
∩
C
∣
−
∣
C
∩
A
∣
+
∣
A
∩
B
∩
C
∣
|A∪B∪C| = |A|+|B|+|C| - |A∩B| - |B∩C| - |C∩A| + |A∩B∩C|
∣A∪B∪C∣=∣A∣+∣B∣+∣C∣−∣A∩B∣−∣B∩C∣−∣C∩A∣+∣A∩B∩C∣,不难发现,出现的元素是奇数就加,偶数就减,这个原理应该也比较好懂,
n
n
n个元素就总共有
2
n
−
1
2^n-1
2n−1种取法,只要把这
2
n
−
1
2^n-1
2n−1种东西列出来,奇加偶减就可以解决。也是由于与2的次幂有关,推荐使用位运算来枚举每个集合的选和不选。
例题
题目描述
给定一个整数
n
n
n和
m
m
m个不同的质数
p
1
,
p
2
,
…
,
p
m
p_1,p_2,…,p_m
p1,p2,…,pm
请求出
1
∼
n
1∼n
1∼n中能被
p
1
,
p
2
,
…
,
p
m
p_1,p_2,…,p_m
p1,p2,…,pm中的至少一个数整除的整数有多少个。
数据范围
1
≤
m
≤
16
,
1
≤
n
,
p
i
≤
1
0
9
1≤m≤16,1≤n,p_i≤10^9
1≤m≤16,1≤n,pi≤109
idea:
为了使用容斥原理,我们要分别解决三个问题:
① 求出每个集合中元素的个数
② 求出集合和集合之间交集的个数
③ 用二进制表示选择了哪个集合与否
三个问题的解决:
- 题目给出了m个质数,我们可以根据 1 ∼ n 1∼n 1∼n中能被 p i p_i pi整除这个属性分出m个集合来。第 i i i个集合表示这个集合中的数都可以被 p i p_i pi整除,这个集合的大小就是 ⌊ n p i ⌋ \lfloor \frac{n}{p_i} \rfloor ⌊pin⌋。
- 假设我们现在要求集合 i i i和集合 j j j的交集。我们知道当一个数 x x x既可以被 p i p_i pi整除又可以 p j p_j pj整除的时候,一定可以被 p i × p j p_i \times p_j pi×pj整除。所有集合1和集合2的交集的大小就是 ⌊ n p i × p j ⌋ \lfloor \frac{n}{p_i \times p_j} \rfloor ⌊pi×pjn⌋。
- 每个集合的选和不选一共 2 m − 1 2^m-1 2m−1种情况,我们可以令 i i i 从 1 1 1到 2 m − 1 2^m-1 2m−1 循环一遍,每一个 i i i就代表了一种情况,此时对于第 j j j个集合选没选,可以使用位运算查看 i i i二进制下的第 j j j位是否为 1 1 1即可。
到此这题的思路已经缕清,下面是code
#include<bits/stdc++.h>
#define LL long long
#define INF 0x3f3f3f3f
using namespace std;
LL p[20],n,m,ans;
void solve()
{
ans = 0;
cin>>n>>m;
for(int i=0;i<m;i++)
{
cin>>p[i];
}
for(int i=1;i<(1<<m);i++)
{
LL temp = 1,sum = 0;
for(int j=0;j<m;j++)
{
if( (i>>j)&1 )
{
if( temp*p[j]>n )//可能过大会溢出,需要特判
{
temp = n+1;
break;
}
temp *= p[j];
sum++;
}
}
if( sum%2==1 )
{
ans += n/temp;
}
else ans -= n/temp;
}
cout<<ans;
}
2.错位排列计数
概念
对于 1 ∼ n 1∼n 1∼n中的排列 P P P,如果满足 p i ≠ i p_i \neq i pi=i,则称 P P P是 n n n 的错位排列.。
引例
问题:有一天,有五个人各自收到了一封信,每个人的家门前都有一个自己的信箱。可是送信员在送信的时候恰好把每个人的信都送到了别人家的信箱里,问:满足这样送信方案的方案数一共有多少种?
分析:为了更简便的说明,用大写字母 A B C D E ABCDE ABCDE来表示五个人的信箱,对应的小写字母 a b c d e abcde abcde表示五个人的信。用 f ( n ) f(n) f(n)来表示 n n n个人 n n n封信的错排问题的方案数。
既然所有的信都被投到了别人的信箱里,那我们不妨先假设信 a a a被送到了信箱 B B B。此时我们可以画出一个现在的情况图:
好,那我们来进行下一步,b信件去哪里呢?
-
b
b
b信件可以去
A
A
A信箱。
那么现在的情况变成了这样:
我们会发现在这种情况下,少了两封信两个信箱,问题变为3个人3封信,方案数显然为 f ( 5 − 2 ) = f ( 3 ) f(5-2) = f(3) f(5−2)=f(3)
2) b信件可以去C信箱。
情况变成了
这回好像我们没有办法一眼看出当前的方案数究竟是什么了。但是我们可以来看一下在a信件到B信箱的过程中都发生些了什么!
首先我们需要回忆一下这个问题的规则是什么?
每一封信都有一个对应的信箱,而每一封信不能投到对应的信箱中去。我们不妨把这种信封与信箱对应的关系暂且称为互为一个错排对,意为错排对中的信不能投到同一个错排对中的信箱中。
这样我们可以把原问题描述为:现在有信箱 A B C D E ABCDE ABCDE与信件 a b c d e abcde abcde,对应的大写字母与小写字母之间互为错排对。问满足条件的情况下,要把每封信不重复不遗漏地投到所有信箱中,一共有多少种不同的方案数?我们可以想到其实这里我们非常直观地把某一个 a 和 A a和A a和A, b 和 B b和B b和B…当成了错排対,但是如果我们换一个思路,我们其实只是需要指定一个不重不漏的对应关系,求满足这样关系的排列依然是错排问题。(比如我们指定a不能放到B中,b不能放到C中,c不能放到D中,d不能放到E中,e不能放到A中,满足这样关系的排列的数目的本质和原题目其实是一样的)
现在,信箱剩下 A C D E ACDE ACDE,信件剩下 b c d e bcde bcde。
而我们已经讨论了信件b投到信箱A的情况。那么我们现在就该讨论b不投到A中的情况了。
你可能已经发现了,在我们讨论完b投到A的情况后,为了使讨论不重复,b与A之间就建立了一个错排对的关系!那么我们就可以写出第二个分类,既b信件不去A信箱。我们不管刚刚的b到C去这个分类,在重新分类下情况应该是这样的:
- b信件不去A信箱。
这个时候因为所有的4封信件都与唯一一个信箱有唯一的错排对关系,那么这种情况下,方案数就转化为: f ( 5 − 1 ) = f ( 4 ) f(5-1) = f(4) f(5−1)=f(4)
现在讨论无重复无遗漏,于是我们就可以得到信件a投到信箱B为前提所有满足条件的方案数: f ( 4 ) + f ( 3 ) f(4)+f(3) f(4)+f(3) 。
但是a不但可以去B,还可以去CDE,也就是有5-1种选择,那么总结一下原问题答案就是 f ( 5 ) = ( 5 − 1 ) × ( f ( 3 ) + f ( 4 ) ) f(5) = (5-1) \times ( f(3)+f(4) ) f(5)=(5−1)×(f(3)+f(4))
同理,我们可以按照这种思路,将5变为n,其实就得到了错排问题的递推式:
f
(
n
)
=
(
n
−
1
)
×
(
f
(
n
−
1
)
+
f
(
n
−
2
)
)
f(n) = (n-1) \times ( f(n-1) +f(n-2) )
f(n)=(n−1)×(f(n−1)+f(n−2))
最后我们考虑一下递推的边界,当只有一封信一个信箱时答案是0,两个信封两个信箱时答案是1,即:
f
(
1
)
=
0
,
f
(
2
)
=
1
f(1) = 0,f(2) = 1
f(1)=0,f(2)=1。
例题
题目描述
大家常常感慨,要做好一件事情真的不容易,确实,失败比成功容易多了!
做好“一件”事情尚且不易,若想永远成功而总从不失败,那更是难上加难了,就像花钱总是比挣钱容易的道理一样。
话虽这样说,我还是要告诉大家,要想失败到一定程度也是不容易的。比如,我高中的时候,就有一个神奇的女生,在英语考试的时候,竟然把40个单项选择题全部做错了!大家都学过概率论,应该知道出现这种情况的概率,所以至今我都觉得这是一件神奇的事情。如果套用一句经典的评语,我们可以这样总结:一个人做错一道选择题并不难,难的是全部做错,一个不对。
不幸的是,这种小概率事件又发生了,而且就在我们身边:
事情是这样的——HDU有个网名叫做8006的男性同学,结交网友无数,最近该同学玩起了浪漫,同时给n个网友每人写了一封信,这都没什么,要命的是,他竟然把所有的信都装错了信封!注意了,是全部装错哟!
现在的问题是:请大家帮可怜的8006同学计算一下,一共有多少种可能的错误方式呢?
Input
输入数据包含多个多个测试实例,每个测试实例占用一行,每行包含一个正整数n(1<n<=20),n表示8006的网友的人数。
Output
对于每行输入请输出可能的错误方式的数量,每个实例的输出占用一行。
Sample Input
2
3
Sample Output
1
2
套用我们推出的递推式就可以直接写了
code:
#include<bits/stdc++.h>
using namespace std;
int n,f[22];
void solve()
{
f[1] = 0;
f[2] = 1;
for(int i=3;i<=20;i++) f[i] = (i-1)*( f[i-1]+f[i-2] );
while( scanf("%d",&n)!=EOF ) cout<<f[n]<<endl;
}