整理的算法模板合集: ACM模板
实际上是一个全新的精炼模板整合计划
注:为了减少篇幅,我删去了大部分的性质证明和一些性质概念定义的解释,详细内容请参见各章节开始给出的文章链接。
内容过多,质量过硬,建议点赞收藏加关注(●ˇ∀ˇ●)
累死我了
目录
- 0x00 基本计数原理
- 0x10 排列
- 0x20 组合
- 0x30 多重集
- 0x40 二项式定理
- 0x50 特殊的数列
- 0x60 生成函数
- 0x70 群论
- 0x80 递推方程
- 0x90 康托展开
0x00 基本计数原理
0x01 加法、乘法、减法、除法原理
▲ 加法原理(分类)
定义: 若完成一件事有 n n n 类方式,第 i i i 类有 a i a_i ai 种方法,那么完成这件事共有 a 1 + a 2 + ⋯ + a n a_1+a_2+⋯+a_n a1+a2+⋯+an 种方法。
经典应用: 从北京到上海有火车、飞机、轮船 3 3 3 种方式,火车、飞机、轮船分别有 k 1 k1 k1, k 2 k2 k2, k 3 k3 k3 个班次,那么从北京到上海有 k 1 + k 2 + k 3 k1+k2+k3 k1+k2+k3 种方 式可以到达。
▲ 乘法原理(分步)
定义: 若完成一件事 n n n个步骤,第 i i i个步骤有 a i a_i ai种方法,那么完成这件事共有 a 1 × a 2 × ⋯ × a n a_1×a_2×⋯×a_n a1×a2×⋯×an种方法。
经典应用: 从北京乘坐火车到上海,需要转 3 3 3 次车,每次专车分别有 k 1 k1 k1, k 2 k2 k2, k 3 k3 k3 个班次,那么从北京到上海有 k 1 × k 2 × k 3 k1×k2×k3 k1×k2×k3 种方式可以到达。
▲ 减法原理(正难则反)
定义: 记 S S S 为全集,则 ∣ A ∣ = ∣ S ∣ − ∣ S |A| = |S|−|S ∣A∣=∣S∣−∣S \ A ∣ A| A∣ ,常用于当直接统计具有一种性质的事物个数较为困难时,考虑统计不具有这种性质的事物个数,再用总数减去这个值。
经典应用: 班上一共有 n n n 个同学,老师点名,到了 m m m 个,则有 n − m n−m n−m 个同学没来。
▲ 除法原理(划分)
定义: 令 S S S 是一个有限集合,把它划分乘 k k k 个部分使得每一部分包含的对象数目相同,于是。次划分中的部分的数目由下述公式给出: k = ∣ S ∣ 在 一 个 部 分 中 的 对 象 数 目 k=\cfrac{|S|}{在一个部分中的对象数目} k=在一个部分中的对象数目∣S∣
因此,如果我们知道 S S S 中的对象数目,以及各部分所含对象数目的共同之, 就可以确定部分的数目。
Hint
加法原理和乘法原理是两个计数基本原理,回答的都是有关做一件事的不同方法种数的问题。
二者区别在于:
分类加法原理针对的是分类问题,其中各种方法相互独立,用任何一种方法都可以完成这件事。
分步乘法原理针对的是分步问题,各步骤中的方法相互依存,只有各个步骤都完成才算完成任务。
分类要依据同一标准划分,既必须包括所有情况,又不要交错在一起产生重复。
分步则应使各步依次完成,保证整个任务得以完成,既不得多余重复,也不缺少某一步骤。
减法原理则是一个常用的计数技巧,正难则反。
▲ 竞赛例题选讲
Problem A Lining Up(AtCoder 2271)
有 n n n 个人站一行,对于第 i i i 个人,给出第 i i i 个人站在其两端的人数的差值 a[i]
,问一共有多少种可能的站法。若不存在这样的排列方法,输出 0
。
Solution
由于我们仅有的信息是 n n n 的值,和 a i a_i ai 所以我们考虑对 n n n 进行讨论。
对 n n n 进行奇偶讨论:
-
当 n n n 为奇数时:中间的人的左右的人的个数一定相同,因此 a i a_i ai 为 0 0 0 且只有 1 1 1 个,从其向两边延伸,每延伸 1 1 1 个人,差的人数 a i + 2 a_i+2 ai+2 ,因此在奇数情况下,只有 0 、 2 、 4 、 6 、 8 、 . . . 0、2、4、6、8、... 0、2、4、6、8、... 。也就是,除 0 0 0 有 1 1 1 个外,其余均为 2 2 2 个人。
-
当 n n n 为偶数时:一定没有两边差值 a i a_i ai 为 0 0 0 的情况,其两边的差最小为 1 1 1,从其两边延伸,每延伸 1 1 1 个人,差的人数 a i + 2 a_i+2 ai+2 ,因此在偶数的情况下,只有 1 、 3 、 5 、 7... 1、3、5、7... 1、3、5、7...,均为 2 2 2 个。
综上所述,除了奇数情况时差值 a i a_i ai 为 0 0 0 的在中间外,其余的每个位置有两个人可选(就是 a i a_i ai 相同的两个人位置可以交换),因此根据乘法原理,答案为 2 ⌊ n 2 ⌋ 2^{\lfloor\frac{n}{2}\rfloor} 2⌊2n⌋ 。
至于无解的情况,根据上述分析,我们可以开一个桶,记录每个数的个数,如果不满足上面分析的 奇数情况的 0 、 2 、 4 、 6 、 8 、 . . . 0、2、4、6、8、... 0、2、4、6、8、... 除 0 0 0 有 1 1 1 个外,其余均为 2 2 2 个人,或者偶数情况的 1 、 3 、 5 、 7... 1、3、5、7... 1、3、5、7...,且均为 2 2 2 个即为无解输出 0
。
Code
typedef long long ll;
typedef int itn;
const int N = 500007, M = 5000007, INF = 0x3f3f3f3f, mod = 1e9 + 7;
const ll LINF = 4e18 +7;
int n, m;
ll a[N];
int vis[N];
int qpow(int a, int b);
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; ++ i) {
scanf("%d", &a[i]);
vis[a[i]] ++ ;
}
bool flag = true;
if(n & 1) {
if(vis[0] == 0)
flag = false;
for(int i = 2; i <= n; i += 2) //奇数一定是 2,4,6,8... 且一定都是两个才对称
if(vis[i] != 2) {
flag = false;
break;
}
}
else {
for(int i = 1; i <= n - 1; i += 2) //偶数一定是 1,3,5,7... 且一定都是两个才对称
if(vis[i] != 2) {
flag = 0;
break;
}
}
ll res = qpow(2, n / 2);
if(flag)
printf("%lld\n", res);
else puts("0");
return 0;
}
Problem B Triangle Counting(UVA11401 )
给定 n n n 条边,长度分别为 1 , 2 , 3 , . . . , n 1,2,3,...,n 1,2,3,...,n 。用其中三条边构成一个三角形,有多少种不同的方案?注意,一条边只能使用一次。
n ≤ 1 0 6 n\le 10^6 n≤106
Solution
计算能组成三角形的选择方案数,为了补充不漏的计数,我们可以考虑固定三角形中的最长边 x x x,讨论另外两条边 y , z y, z y,z 的情况。
发现我们有且只有一个能用的性质:三角形中,两边之和大于第三边。
假定我们已知 x x x ,则有:
y + z > x y+z>x y+z>x
对于当前的 x x x,考虑合法的 y y y 的范围,显然有:
x − z < y < x x-z<y<x x−z<y<x
尝试讨论:当 y = 1 y = 1 y=1 时,显然 z z z 无解。当 y = 2 y=2 y=2 时, z z z 只有 1 1 1 个解: x − 1 ≤ z < x ⇒ z = x - 1 x-1\le z< x\Rightarrow z = x-1 x−1≤z<x⇒z=x-1;当 y = 3 y=3 y=3 时 z z z 有 2 2 2 个解: x - 2 , x - 1 x-2,x-1 x-2,x-1, ⋯ \cdots ⋯,当 y = x - 1 y=x-1 y=x-1, z z z 有 x - 2 x-2 x-2 个解: 2 , 3 , ⋯ , x - 1 2,3,\cdots,x-1 2,3,⋯,x-1。显然方案数呈等差数列,故总的解数为:
( x − 1 ) ( x − 2 ) 2 \dfrac{(x-1)(x-2)}{2} 2(x−1)(x−2)
现在是满足 y + z > x y+z>x y+z>x 的方案数,但是题目中规定每条边只能只用一次,所以要去掉 y = z y=z y=z 的情况:
y = z , x − z < y < x ⇒ x − y < y ⇒ y > x 2 ⇒ x 2 < y < x ⇒ x 2 + 1 ≤ y ≤ x − 1 \begin{aligned}&y=z,x-z<y<x &\\&\Rightarrow x-y<y \Rightarrow y>\frac{x}{2}&\\& \Rightarrow\frac{x}{2}<y<x&\\&\Rightarrow \frac{x}{2}+1\le y\le x-1\end{aligned} y=z,x−z<y<x⇒x−y<y⇒y>2x⇒2x<y<x⇒2x+1≤y≤x−1
则共有 x − 1 − ( x 2 + 1 ) + 1 = x + 2 2 x-1-(\dfrac{x}{2}+1)+1=\dfrac{x+2}{2} x−1−(2x+1)+1=2x+2 种情况。
题目规定必须是不同的三角形,即 y = 10 , z = 5 y=10,z=5 y=10,z=5 与 z = 5 , y = 10 z=5,y=10 z=5,y=10 是同种方案,所以最后答案需要再除以二。
综上所述,答案 f ( x ) f(x) f(x) 表示最长边为 x x x 时,可以组成的不同的三角形的方案数:
f ( x ) = ( x − 1 ) ( x − 2 ) 2 − x + 2 2 2 = ( x − 2 ) 2 4 f(x)=\dfrac{\dfrac{(x-1)(x-2)}{2}-\dfrac{x+2}{2}}{2}=\dfrac{(x-2)^2}{4} f(x)=22(x−1)(x−2)−2x+2=4(x−2)2
那么对于数列 1 , 2 , ⋯ n {1,2,\cdots n} 1,2,⋯n,总方案数显然为 ∑ i = 3 n f ( i ) \sum\limits_{i=3}^{n}f(i) i=3∑nf(i) 。前缀和预处理即可。
Code
// Problem: UVA11401 Triangle Counting
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/UVA11401
// Memory Limit: 0 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e6 + 7;
int n, m, s, t, k, ans, a[N];
ll f[N];
void prework(int x)
{
f[1] = f[2] = 0;
for (int i = 3; i <= x; ++ i) {
f[i] = f[i - 1] + 1ll * (i - 2) * (i - 2) / 4;
}
}
int main()
{
prework(N - 5);
while(scanf("%d", &n) != EOF && n) {
if(n < 3) return 0;
cout << f[n] << endl;
}
return 0;
}
0x02 抽屉原理(鸽巢原理)
▲ 第一抽屉原理
定理2.1: 把 n + 1 n+1 n+1 件东西放入 n n n 个抽屉,则至少有一个抽屉里会放入两件或两件以上的东西。
定理2.2: 从另一个角度考虑,把 n − 1 n-1 n−1 件东西放入 n n n 个抽屉,则至少一个抽屉是空的。
定理2.3: 把多于 m × n + 1 m\times n+1 m×n+1( n n n 不为 0 0 0)个的物体放到 n n n 个抽屉里,则至少有一个抽屉里有不少于 m + 1 m+1 m+1 的物体。
定理2.4: 把比无数多还多的这么多件物体放入 n n n 个抽屉,则至少有一个抽屉里有无数个物体。
▲ 第二抽屉原理
定理2.5: 把 m × n - 1 m\times n-1 m×n-1 个物体放入 n n n 个抽屉中,其中必有一个抽屉中至多有 m − 1 m-1 m−1 个物体。
定理2.6: 把多于 m × n m\times n m×n 个的物体放到 n n n 个抽屉里,则至少有一个抽屉里有不少于 m + 1 m+1 m+1 的物体。
例如:将 3 × 5 − 1 = 14 3×5-1=14 3×5−1=14 个物体放入 5 5 5 个抽屉中,则必定有一个抽屉中的物体数少于等于 3 − 1 = 2 3-1=2 3−1=2 (平均 3 3 3 个都不够,令一个变多,另一个一定变少)。
推论2.7: 已知 n + 1 n+ 1 n+1 个互不相同的正整数,它们全都小于或等于 2 n 2n 2n ,证明当中一定有两个数是互质的。
考虑证明:
取 n n n 个盒子,在第一个盒子我们放 1 1 1 和 2 2 2,在第二个盒子我们放 3 3 3 和 4 4 4 ,第三个盒子是放 5 5 5 和 6 6 6 ,依此类推直到第 n n n 个盒子放 2 n − 1 2n-1 2n−1 和 2 n 2n 2n 这两个数。
如果我们在 n n n 个盒子里随意抽出 n + 1 n+1 n+1 个数。我们马上看到一定有一个盒子是被抽空的。而被抽空的盒子里的两个数一定是连续的,因此在我们任意选择的这 n + 1 n+1 n+1 个数中必有两个数是连续数,很明显的连续数是互质的。
推论2.7 得证 □
▲ 构造抽屉的方法
运用抽屉原理的核心是分析清楚问题中,哪个是物件,哪个是抽屉。例如,属相是有 12 12 12 个,那么任意 37 37 37 个人中,至少有一个属相是不少于 4 4 4 个人。这时将属相看成 12 12 12 个抽屉,则一个抽屉中有 37 12 \cfrac{37}{12} 1237,即 3 3 3 余 1 1 1 ,余数不考虑,而向上考虑取整数,所以这里是 3 + 1 = 4 3+1=4 3+1=4 个人,但这里需要注意的是,前面的余数 1 1 1 和这里加上的 1 1 1 是不一样的 。
因此,在问题中,较多的一方就是物件,较少的一方就是抽屉,比如上述问题中的属相 12 12 12 个,就是对应抽屉, 37 37 37 个人就是对应物件,因为 37 37 37 相对 12 12 12 多。这样我们设抽屉个数为 m m m,物件个数为 n n n ,也就意味着一定有 ⌈ m n ⌉ \lceil\cfrac{m}{n}\rceil ⌈nm⌉ 个元素满足同样的性质。我们通常利用这一点性质来证明一定有解。
▲ 最差原则
即考虑所有可能情况中,最不利于某件事情发生的情况。也就是最差的情况都能被满足,那么其余所有的情况都比最差的要好,连最差的都能满足,则说明其余所有情况也都能满足,也就是任意情况都能满足
Example A 求职
有 300 300 300 人到招聘会求职,其中软件设计有 100 100 100 人,市场营销有 80 80 80 人,财务管理有 70 70 70 人,人力资源管理有50人。那么至少有多少人找到工作才能保证一定有 70 70 70 人找的工作专业相同呢?
此时我们考虑的最差情况为:软件设计、市场营销和财务管理各录取 69 69 69 人,人力资源管理的 50 50 50 人全部录取,则此时再录取 1 1 1 人就能保证有 70 70 70 人找到的工作专业相同。因此至少需要 69 × 3 + 50 + 1 = 258 69\times 3+50+1=258 69×3+50+1=258 人。
根据 定理2.3 推导: m × n + 1 m\times n+1 m×n+1 个人的时候必有 m + 1 m+1 m+1 个人找到的工作专业相同,所以是要求出 m × n + 1 m\times n+1 m×n+1 的人数,已知 n = 3 n=3 n=3, m + 1 = 70 m+1=70 m+1=70 。考虑到人力资源专业只有 50 50 50 人,得出 m × n + 1 = ( 69 × 3 + 50 ) + 1 = 258 m\times n+1=(69\times 3+50)+1=258 m×n+1=(69×3+50)+1=258 人。
Example B 强迫症
一个抽屉里有 20 20 20 件衬衫,其中 4 4 4 件是蓝的, 7 7 7 件是灰的, 9 9 9 件是红的,则应从中随意取出多少件才能保证有 5 5 5 件是同颜色的?
根据抽屉原理, n n n 个抽屉, k × n + 1 k\times n + 1 k×n+1 件物件,则至少有一个抽屉中有 k + 1 k + 1 k+1 件物件。若根据抽屉原理的推论直接求解,此时 k=4,n=3,则应抽取 3 × 4 + 1 = 13 3 \times 4 + 1 = 13 3×4+1=13 件才能保证有 5 5 5 件同色。其实不能这么考虑,因为原本的意思是差的那一个物件放到那个抽屉里都行,但是蓝色抽屉实际上是不能再放入的,所以本问题的模型实际上和鸽巢原理有些不同。在解决该问题时,应该考虑最差原则,连续抽取过程中抽取出 4 4 4 件蓝色的衬衣,取走后,问题变成有灰色和红色构成相同颜色的情况,这时, n = 2 n=2 n=2, k + 1 = 5 k + 1 = 5 k+1=5, k = 4 k = 4 k=4 。故应取 4 + 4 × 2 + 1 = 13 4 + 4\times 2 + 1 = 13 4+4×2+1=13 件。
问题分析:该情况下鸽巢原理的推论不再适用,由于蓝色的衬衫只有 4 4 4 件,而题目中要求有 5 5 5 件是同色的,导致 4 4 4 件蓝色衬衫都被抽取出这一最差情况的存在,所以应该先考虑最差情况,然后在此基础上再运用鸽巢原理。
▲ 竞赛例题选讲
Problem A Halloween treats(POJ 3370)
给出一个含有 n n n 个数字的序列,要找一个连续的子序列,使他们的和一定是 c c c 的倍数,若不存在,则输出 no sweetws
。 1 ≤ c ≤ n ≤ 1 0 5 1 ≤ c ≤ n ≤ 10^5 1≤c≤n≤105
Solution
首先预处理序列的前缀和 sum[i]
。
根据抽屉原理,以 s u m sum sum 数组构造一个抽屉 d r a w e r drawer drawer 数组,其保存的是最先出现的 sum[i] % c
的下标,当 s u m sum sum 的一个元素第二次放入重复的抽屉时,说明 出现了一个和第一次放入抽屉的元素性质相同的元素 。我们假设第一个 被放入该抽屉的元素的下标为 i i i ,第二次被放入重复的抽屉元素的下标是 j j j 。
此时:
-
若
sum[i]
是 c c c 的倍数,即sum[i] % c == 0
,我们直接输出前 i i i 项就是答案,或者前j
项也是答案,它们都有相同的性质:是 c c c 的倍数。 -
若
sum[i]
不是 c c c 的倍数,则此时,sum[i] % c == sum[j] % c
且 i ! = j i!=j i!=j ,则说明前 i i i 项的个与前 j j j 项的和模 c c c 同余,也就意味着 s u m [ j ] − s u m [ i ] ≡ 0 ( m o d c ) sum[j] - sum[i]\equiv 0\pmod c sum[j]−sum[i]≡0(modc),也就是 从第 i + 1 i+1 i+1 项到第 j j j 项的和是 c c c 的倍数。
如何证明一定存在答案呢?
因为我们求的是 % c \% c %c 相同的两个点, % c \%c %c 的值一共只有 0 ⋯ c − 1 0\cdots c-1 0⋯c−1 , c c c 种答案,但是我们一共有 n ≥ c n\ge c n≥c 个数,根据抽屉原理,如果 n = c n=c n=c ,则一定存在一个数 x % c = 0 x\%c=0 x%c=0 是满足题意的答案,直接输出即可(这种情况下 n n n 个数构成模 c c c 的简化剩余系) ,或者存在两个数同余。 若 n > c n>c n>c 则一定存在两个数同余。
同时得出:
推论2.7: 一个由 n n n 个数构成的数列,总能找到若干个连续的数,使它们之和能被 n n n 整除(就是上面 n = c n=c n=c 的情况)
Code
typedef long long ll;
typedef int itn;
const int N = 500007, M = 5000007, INF = 0x3f3f3f3f;
const ll LINF = 4e18 +7;
int n, m;
ll c, a[N], sum[N];
int drawer[N];
int main()
{
while(scanf("%lld%d", &c, &n) != EOF && c | n) {
for(int i = 1; i <= n; ++ i)
drawer[i] = -1;
drawer[0] = 0;
sum[0] = 0;
for(int i = 1; i <= n; ++ i) {
scanf("%lld", &a[i]);
sum[i] = sum[i - 1] + a[i];
}
for(int i = 1; i <= n; ++ i) {
if(drawer[sum[i] %c] != -1) {
for(int j = drawer[sum[i] % c] + 1; j < i; ++ j)
printf("%d ", j);
printf("%d\n", i);
break;
}
drawer[sum[i] % c] = i;
}
}
return 0;
}
Problem B Flowers ( 2019-ICPC沈阳 - L )
给你 n n n 种花和每种花的数量,每束花要 m m m 朵,每束花里不能有同种的花,问最多能准备几束花。
Solution
由于每束花里的花的种类一定不同,那么据抽屉原理,每种花能用的数量,一定小于等于花束的数量,因为一旦大于,就存在一个花束中至少有一种花的数量大于 1 1 1 。题目要求的是最多能有几个花束,很自然地想到二分,我们二分花束的数量,判断的时候,假设当前二分到一共可以有 m i d mid mid 束花,则一共需要 m i d × m mid\times m mid×m 束花,我们根据上面的分析,每种花最多可以使用 min { a [ i ] , m i d } \min\{a[i], mid\} min{
a[i],mid} 枝,我们就可以计算当前最多能提供 ∑ i = 1 n min { a [ i ] , m i d } \sum\limits_{i=1}^{n}\min\{a[i], mid\} i=1∑nmin{
a[i],mid} 枝花,我们只需要判断二者的关系即可,也就是能满足,大于等于即为 true
,不够即为 false
。
Code
typedef long long ll;
typedef int itn;
const int N = 500007, M = 5000007, INF = 0x3f3f3f3f, mod = 1e9 + 7;
const ll LINF = 4e18 +7;
int n, t;
ll m;
ll a[N];
bool check(ll x)
{
ll res = 0;
for(int i = 1; i <= n; ++ i)
res += min(a[i], x);
return res >= x * m;
}
int main()
{
scanf("%d", &t);
while(t -- ) {
scanf("%d%lld", &n, &m);
ll sum = 0;
for(int i = 1; i <= n; ++ i) {
scanf("%lld", &a[i]);
sum += a[i];
}
ll l = 0, r = sum / m, ans = 0;
while(l <= r) {
ll mid = l + r >> 1ll;
if(check(mid)) l = mid + 1, ans = mid;
else r = mid - 1;
}
printf("%lld\n", ans);
}
return 0;
}
0x03 容斥原理
容斥原理是一种应用在集合上的较常用的计数方法,其基本思想是:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来(容),然后再把计数时重复计算的数目排斥出去(斥),使得计算的结果既无遗漏又无重复。
容斥原理核心的计数规则可以归为一句话:奇加偶减。
假设被计数的有 A 、 B 、 C A、B、C A、B、C 三类,那么, A 、 B 、 C A、B、C A、B、C 类元素个数总和 = = = 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 A∪B∪C=A+B+C−AB−BC−AC+ABC
当被计数的种类被推到 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} 1≤a,b,n≤231。
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,a−1] 区间内与 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} m−pm (其中 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+p2m−p1×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 a−1 的元素个数) − - − ( 1 1 1 ~ b b b 中与 n n n 不互质的数的个数) + + + ( 1 1 1 ~ a − 1 a-1 a−1 中与 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=n−∑i=1n⌊a[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 2、4、⋯、2k 个数就要减去他们最小公倍数能组成的数, 1 、 3 、 ⋯ 、 2 k + 1 1、3、\cdots、2k+1 1、3、⋯、2k+1 个数就要加上他们的最小公倍数,对于每一个数来说,我们都有选或不选两种情况,因此 m m m 个数就有 2 m 2^m 2m 种情况,也就是从 0 0 0(啥也不选)到 2 m − 1 2^m-1 2m−1,对应二进制就是 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 n−sum 。
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;
}
Problem C 硬币购物(Luogu P1450 [HAOI2008])
共有 4 4 4 种硬币。面值分别为 c 1 , c 2 , c 3 , c 4 c_1,c_2,c_3,c_4 c1,c2,c3,c4 。
某人去商店买东西,去了 n n n 次,对于每次购买,他带了 d i d_i di 枚 i i i 种硬币,想购买 s s s 的价值的东西。请问每次有多少种付款方法。
Solution
看起来像是一个多重背包求方案数的模板题,但是由于有 n n n 组,所以每次都求一次多重背包绝对会超时。
显然多重背包即有限制的方案数,直接计算有限制的方案数比较难,考虑正难则反:
有限制的方案数 = 无限的方案数 - 超过限制方案数
首先无限制的方案数显然就是完全背包,我们可以先用完全背包预处理出所有的方案数,即 f[i]
表示购买价值为 i
的方案数。
考虑减去超过显示的即不合法的部分,考虑使用容斥原理。
首先考虑一种硬币超额使用的方案数怎么计算。若第 j j j 种硬币超额使用,即超过了原定的 d j d_j dj 个硬币的限制,我们可以先强制选了 d j + 1 d_j+1 dj+1个第 j j j 种硬币,这样剩下的价值里, 4 4 4 种硬币随便选,这样就能保证第 j j j 种硬币一定超额使用,第 j j j 种硬币超额的不合法的方案数就等于 f [ s − ( d j + 1 ) × c j ] f[s-(d_j+1)\times c_j] f[s−(dj+1)×cj]。
然后我们只需要使用容斥原理奇加偶减即可。加上一种硬币不合法的方案数,减去两种硬币不合法的方案数,加上三种硬币不合法的方案数 ⋯ \cdots ⋯ 二进制枚举子集即可。
Code
// Problem: P1450 [HAOI2008]硬币购物
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1450
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 7;
int n, m, s, t, k, ans;
int f[N];
int c[50], d[50];
void pre_work(int n)
{
f[0] = 1;
for (int i = 1; i <= 4; ++ i)
for (int j = c[i]; j < n; ++ j)
f[j] += f[j - c[i]];
}
void solve()
{
scanf("%lld%lld%lld%lld%lld", &d[1], &d[2], &d[3], &d[4], &s);
int ans = f[s], now;
for (int i = 1; i <= (1 << 4) - 1 ; ++ i) {
now = s;
int tmp, j, k;
for (tmp = i, j = 1, k = 0; tmp; tmp >>= 1, ++ j) {
if(tmp & 1) {
k ^= 1;
now -= (d[j] + 1) * c[j];
}
}
if(now >= 0)
k ? ans -= f[now] : ans += f[now];
}
printf("%lld\n", ans);
}
signed main()
{
scanf("%lld%lld%lld%lld%lld", &c[1], &c[2], &c[3], &c[4], &n);
pre_work(N - 7);
while (n -- ){
solve();
}
return 0;
}
Hint
我们一般根据研究对象的有无,将组合数学分为排列问题和组合问题。
其根本不同是排列问题与元素顺序有关,组合问题与元素顺序无关。
在排列与组合问题中,经常会出现计数问题,解决计数问题的思路一般有以下三种:
1)只取需要的。将各种符合条件的情形枚举出来,再利用加法原理求和。
2)先取后排。将各步符合条件的排列或组合计算出来,再根据乘法原理求积。
3)先全部取,再减去不要的。利用容斥定理,将各种符合条件的情形枚举出来,再减去不符合条件的。
0x10 排列
0x11 排列
定义: 从 n n n 个元素的集合 S S S 中,有序的选出 r r r 个元素,叫做 S S S 的一个 r r r 排列,不同的排列总数记作: A n r \mathrm{A}_n^r Anr 或 P ( n , r ) P(n,r) P(n,r)
如果两个排列所含元素不全相同,或所含元素相同但顺序不同,就会被认为是不同的排列。
可重排列
从 n n n 个不同元素可重复的取出 m m m 个元素,按照一定顺序排成一列,叫做相异元素可重复排列。
相异元素可重复排列的方案数为: n m n^m nm (取 m m m 次,每次都有 n n n 种取法)
例如:从 1 、 2 、 3 、 4 、 5 1、2、3、4、5 1、2、3、4、5 中任取三个出来组成一个三位数,有 P ( 5 , 3 ) = 60 P(5,3)=60 P(5,3)=60 种情况,如果每个数字可以重复使用,则有: 5 3 = 125 5^3=125 53=125 种情况。
不可重排列
不可重排列是指在 n n n 个不同元素中选 r r r 个元素按照顺序排成一列。
选排列: 从 n n n 个不同元素取出 r r r 个元素,按照一定顺序排成一列,当 r < n r<n r<n 时,叫做从 n n n 个不同元素取出 r r r 个不同元素的一种选排列。
我们可以一步一步的考虑这个问题,首先从 n n n 个小朋友中抽取一个放在第一个位置有 n n n 种方法。之后从剩下的 n − 1 n−1 n−1 个小朋友中挑选一个放在第二个位置即有 n − 1 n−1 n−1 种方法。以此类推,根据乘法原理我们可以得到
A n r = n × ( n − 1 ) × ( n − 2 ) × ⋯ × ( n − r + 1 ) = n ! ( n − r ) ! \mathrm{A}_n^r=n\times(n-1)\times(n-2)\times\cdots\times(n-r+1)= \cfrac{n!}{(n-r)!} Anr=n×(n−1)×(n−2)×⋯×(n−r+1)=(n−r)!n!
注: 0 ! = 1 0\ ! = 1 0 !=1。
排列数的性质
性质11.1 : A n m = n A n − 1 m − 1 \mathrm{A}_n^m = n\mathrm{A}_{n-1}^{m-1} Anm=nAn−1m−1 (可以理解为 某特定位置 先安排,再安排其余位置。)
性质11.2 : A n m = m A n − 1 m − 1 + A n − 1 m \mathrm{A}_n^m = m\mathrm{A}_{n-1}^{m-1} + \mathrm{A}_{n-1}^m Anm=mAn−1m−1+An−1m(可理解为:含特定元素的排列有 m A n − 1 m − 1 m\mathrm{A}_{n-1}^{m-1} mAn−1m−1,不含特定元素的排列为 A n − 1 m \mathrm{A}_{n-1}^m An−1m)
Problem A 输出排列
输入两个整数 n n n、 r r r,输出 P ( n , r ) P(n,r) P(n,r) 的所有方案
Solution
n个数里选 r 个数,直接爆搜即可。
int n, r, a[N], vis[N];
void dfs(int i) {
//n个数里选 r 个数
if (i == r) {
for (int j = 1; j <= r; ++ j)
printf("%d", a[j]);
printf("%d\n", a[r]);
return;
}
for (int j = 1; j <= n; ++ j) {
if (vis[j] == 0) {
vis[j] = true;
a[i] = j;
dfs(i + 1);
vis[j] = false;
}
}
}
int main() {
memset(vis, 0, sizeof vis);
scanf("%d%d", &n, &r);
dfs(0);
return 0;
}
0x12 全排列
定义: 从 n n n 个不同元素取出 r r r 个元素,按照一定顺序排成一列,当 r = n r=n r=n 时,叫做 n n n 个不同元素的全排列。
全排列的方案数: A n n = n ! \mathrm{A}_n^n=n! Ann=n!
C++ 中,头文件<algorithm>
里的 next_permutation()
函数,可产生字典序的全排列。
例如:给出从一组全排列的情况,使用 next_permutation()
函数可以生成下一种的全排列的情况,因此一般先使用 sort()
进行排序,即可生成所有全排列的情况。
int a[N];
int main(){
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
sort(a,a+n);
do{
for(int i=0;i<n;i++)
printf("%d ",a[i]);
printf("\n");
}while(next_permutation(a,a+n));
return 0;
}
不全相异排列
(1)不全相异元素的选排列
若在 n n n 个元素中,有 n 1 n_1 n1 个元素彼此相同, n 2 n_2 n2 个元素彼此相同, . . . ... ..., n m n_m nm 个元素彼此相同,且 n 1 + n 2 + . . . + n m = r n_1+n_2+...+n_m=r n1+n2+...+nm=r,则这 n n n 个元素选出 r r r 个的选排列叫做不全相异元素的选排列。
排列数计算公式为: A ( n , r ) ( n 1 ! × n 2 ! × ⋯ × n m ! ) \cfrac{A(n,r)}{(n_1!\times n_2!\times \cdots\times n_m!)} (n1!×n2!×⋯×nm!)A(n,r)
(2)不全相异元素的全排列
若在 n n n 个元素中,有 n 1 n_1 n1 个元素彼此相同, n 2 n_2 n2 个元素彼此相同, . . . ... ..., n m n_m nm 个元素彼此相同,且 n 1 + n 2 + . . . + n m = n n_1+n_2+...+n_m=n n1+n2+...+nm=n,则这 n n n 个元素的全排列叫做不全相异元素的全排列。
排列数计算公式为: n ! ( n 1 ! × n 2 ! × ⋯ × n m ! ) \cfrac{n!}{(n_1!\times n_2!\times \cdots\times n_m!)} (n1!×n2!×⋯×nm!)n!
0x13 错位排列与圆排列
定义: 设 ( a 1 , a 2 , . . . , a n ) (a_1,a_2,...,a_n) (a1,a2,...,an) 是 { 1 , 2 , . . . , n } \{1,2,...,n\} { 1,2,...,n} 的一个全排列,若对任意的 i ∈ { 1 , 2 , . . . , n } i \in \{1,2,...,n\} i∈{ 1,2,...,n} 都有 a i ≠ i a_i\neq i ai=i ,则称 ( a 1 , a 2 , . . . , a n ) (a_1,a_2,...,a_n) (a1,a2,...,an) 是 { 1 , 2 , . . . , n } \{1,2,...,n\} { 1,2,...,n} 的错位排列。
用 D n D_n Dn 表示 { 1 , 2 , . . . , n } \{1,2,...,n\} { 1,2,...,n} 的错位排列的个数,有: D n = n ! × ( 1 − 1 1 ! + 1 2 ! − 1 3 ! + ⋯ + ( − 1 ) n n ! ) D_n=n!\times (1-\cfrac{1}{1}!+\cfrac{1}{2}!-\cfrac{1}{3}!+\cdots+\cfrac{(-1)^n}{n}!) Dn=n!×(1−11!+21!−31!+⋯+n(−1)n!)
错排公式
f [ 0 ] = 1 , f [ 1 ] = 0 , f [ 2 ] = 1 f[0]=1, f[1] = 0, f[2]=1 f[0]=1,f[1]=0,f[2]=1
f [ n ] = ( n − 1 ) ∗ ( f [ n − 1 ] + f [ n − 2 ] ) [n]=(n-1)*(f[n-1]+f[n-2]) [n]=(n−1)∗(f[n−1]+f[n−2])
圆排列
从 n n n 个不同元素中选取 r r r 个元素,不分首尾地围成一个圆圈的排列叫做圆排列,其排列方案数为: P ( n , r ) r \cfrac{P(n,r)}{r} rP(n,r)
当 r = n r=n r=n 时,则为圆排列的全排列,其排列方案数为: n ! n = ( n − 1 ) ! \cfrac{n!}{n}=(n-1)! nn!=(n−1)!
圆排列实际上就是群论里的旋转置换,也就是通过旋转任意度数,若相同,则属于同一种方案。我们可以将其想象成一个轮子,怎么转都是一样的,如图11.1,1 2 3 4 5
和 5 4 1 2 3
都算是一种方案,

例题:有男女各 5 5 5 人,其中有 3 3 3 对夫妇,沿 10 10 10 个位置的圆桌就座,若每对夫妇都要坐在相邻的位置上,有多少种坐法?
分析:先让 3 3 3 对夫妇中的妻子和其他 4 4 4 人就坐,根据圆排列公式,共有 ( n − 1 ) ! = 6 ! (n-1)!=6! (n−1)!=6! 种坐法,然后每位丈夫都可以做到自己的妻子左右两边,因此共有 6 ! × 2 × 2 × 2 = 5760 6!\times 2\times 2\times 2=5760 6!×2×2×2=5760 种坐法。
0x20 组合
0x21 组合
定义: 从 n n n 个元素的集合 S S S 中,无序的选出 r r r 个元素,叫做 S S S 的一个 r r r 组合。
如果两个组合中,至少有一个元素不同,它们就被认为是不同的组合(不考虑顺序,元素相同顺序不同仍然是同一个组合)。
不可重组合数
所有不同组合的个数,叫做组合数,记作: C n r C_n^r Cnr 或 ( n r ) \dbinom{n}{r} (rn)
由于每一种组合都可以扩展到 r ! r! r! 种排列,而总排列为 P ( n , r ) P(n,r) P(n,r)
所以组合数: C n r = ( n r ) = P ( n , r ) r ! = n ( n − 1 ) ( n − 2 ) ⋯ ( n − r + 1 ) r ! = n ! ( n − r ) ! r ! , r ⩽ n C_n ^r= \dbinom{n}{r}=\cfrac{P(n,r)}{r!}=\cfrac{n(n-1)(n-2)\cdots(n-r+1)}{r!} = \cfrac{n!}{(n-r)!r!} ,r\leqslant n Cnr=(rn)=r!P(n,r)=r!n(n−1)(n−2)⋯(n−r+1)=(n−r)!r!n!,r⩽n
特别的,规定: C ( n , 0 ) = 1 C(n,0)=1 C(n,0)=1
可重复组合数
从 n n n 个不同的元素中,无序的选出 r r r 个元素组成一个组合,且允许这 r r r 个元素可以重复使用,则称这样的组合为可重复组合。
其组合数记为: H n r = C n + r − 1 r = n + r − 1 r ! ( n − 1 ) ! H_n^r=C_{n+r-1}^r=\cfrac{n+r-1}{r!(n-1)!} Hnr=Cn+r−1r=r!(n−1)!n+r−1
不相邻组合数
从 A = { 1 , 2 , . . . , n } A=\{1,2,...,n\} A={ 1,2,...,n} 中选取 m m m 个不相邻的组合,其组合数为: C n − m + 1 m C_{n-m+1}^m Cn−m+1m
Example A
① 一班有 10 10 10 名同学,二班有 8 8 8 名同学,现每个班级要选出 2 2 2 名学生参加一个座谈会,求有多少种选法?
根据组合数与乘法原理,共有: C ( 10 , 2 ) × C ( 8 , 2 ) = 1260 C(10,2)\times C(8,2)=1260 C(10,2)×C(8,2)=1260 种
② 某班有 10 10 10 名同学,有 4 4 4 名女同学,现要选出 3 3 3 名学生,其中至少有一名女同学,求有多少种选法?
根据组合数与加法原理,共有: C ( 4 , 1 ) × C ( 6 , 2 ) + C ( 4 , 2 ) × C ( 6 , 1 ) + C ( 4 , 3 ) × C ( 6 , 0 ) = 60 + 36 + 4 = 100 C(4,1)\times C(6,2)+C(4,2)\times C(6,1)+C(4,3)\times C(6,0)=60+36+4=100 C(4,1)×C(6,2)+C(4,2)×C(6,1)+C(4,3)×C(6,0)=60+