组合计数是组合数学的基础,研究某组离散对象满足一定条件的安排的存在性、构造及计数等问题。看似名字人畜无害,实则“算死人,不偿命”。本文将引导你学习简单的组合计数,为接下来学习毒瘤的计数问题作准备。
基本概念
组合计数中有一些基本概念,在这里简略地进行介绍。
加法原理
比如 james1 要吃东西,他可以吃傻瓜果和笨蛋果两种果子,市面上在售的傻瓜果有 种,笨蛋果有 种,如果 james1 只吃一颗果子就能吃饱,那么他就有 种选择去吃饱。
形式化地,完成一个 Project 可以有 类办法, 代表第 类方法的数目。那么完成这个 Project 共有 种不同的方法。
乘法原理
比如 james1 要变傻(雾),要吃一种傻瓜果和一种笨蛋果,市面上在售的傻瓜果有 种,笨蛋果有 种,那么 james1 变傻的方式就有 。
形式化地,完成一个 Project 可以有 个步骤, 代表完成第 步的方法数目。那么完成这个 Project 共有 种不同的方法。
抽屉原理(鸽笼原理)
常用于存在性证明与极端情况求解,分为两种情况:
简单形式
james1 要将 个笨蛋果放到 个鸽笼中,那么可以得出至少有一个鸽笼中有两个(或以上)笨蛋果。
证明:
采用反证法。
假如每个分组有至多 个物体,那么最多有 个物体,而却有 个物体,矛盾。
证毕。
推广
james1 要将 个傻瓜果放到 个鸽笼中,那么可以得出至少有一个鸽笼中有大于或等于 个笨蛋果。
证明同样采用反证法,留给读者撕烤。
排列数
从 个不同元素中,任取 ()个元素按照一定的顺序排成一列,方案个数记作 (推荐)或 。
显然,第一个数有 种取法。
第二个数有 种。
第 个数有 种取法。
综上所述,有:
置换和排列
一个有限集合 到自身的双射称为 的一个置换,集合 的置换可以表示为:
是将 映射为 ,这样 是 的一个排列, 上的所有置换的数量为 。
置换的过程可以使用有向图来理解,连边 ,就是所有点移动 的距离。
对于两个置换 的乘积记作 ,代表先通过 的映射,再通过 的映射。
一个排列中的逆序对个数,也叫做反序数,如果是偶数就是偶排列,奇数则是奇排列。
对于一个排列 ,如果将任意两个数 交换,其它数保持不动,就会得到一个新的排列,那么这样一个变换叫做对换,用 表示。
组合数
从 个不同元素中,任取 ()个元素按照任意的顺序组成一个集合,方案个数记作 。
这 个人是没有顺序的,所以 个方案是同一个方案,所以总方案数为:
实际上,组合数也通常用 表示,相当于 ,这也被称之为二项式系数。
组合数有以下性质:
- ;
- ;
- 。
证明:
- 从 个数中选 作为子集,剩下的 个数也对应一个集合(这一点可以用公式证明,留给读者撕烤)。
- 从 个不同元素取出若干个元素组成一个集合,当然可以取 个数,即 ,每个数又有取和不取两种可能,所以它等于 。
- 由组合数的公式可以推导出,它经常被用来递推求单个组合数。大概长这样:
int C(int n, int m) {
int res = 1;
for (int i = 1; i <= m; ++i)
res = 1ll * res * (n - i + 1) / i;
return res;
}
然后是一些不是那么好想,但是在关键时刻非常有用的内容,推荐背诵:
- ;
- ,当 时有 ;
- ;
- ,相当于是 只喵喵中选 个队长,再在队长中选择 个大队长,等价于 只喵喵先选 个大队长再选 个队长,因为大队长也是队长。
杨辉三角
杨辉三角长这样(所有的空格值都为 ):
杨辉三角 | 第 列 | 第 列 | 第 列 | 第 列 | 第 列 |
---|---|---|---|---|---|
第 行 | |||||
第 行 | |||||
第 行 | |||||
第 行 | |||||
第 行 |
可以观察出 。
证明:
想选出 个数,要么选第 个数,要么不选,分别对应 和 。
所以组合数的杨辉三角递推代码如下:
C[0][0] = 1;
for (int i = 1; i <= n; i++) {
C[i][0] = 1;
for (int j = 1; j <= i; j++)
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
二项式定理
高中学过二项式定理:
这个式子非常有用,一定要熟记。另外可以发现,多项式的系数就是杨辉三角。
排列组合方法
我们来看三个问题,来引出排列组合的经典计算方法。
捆绑法
只兔子参观大连市第二十四中学,其中 只兔子关系特别好,它们一定要站在一块。那么有多少种排列方法?
我们把这 只兔子看作一只大兔子,那么总共就有 只兔子,排列方案数是 ,然而大兔子里面也有 中方法,那么总方法数就是 。这就是捆绑法。
插空法
只兔子参观大连市第二十四中学,其中 只兔子有着不共戴天之仇,它们一定要不能站在一块。那么有多少种排列方法?
我们先把 只兔子给排列好,有 种方法。这些兔子之间有 个空(算最左和最右),再把这些不共戴天的兔子放到这些空里,有 个方法。总方案数就是 。这就是插空法。
插板法
james1
要将 个相同的胡萝卜分给 只兔子,他秉持雨露均沾的原则,每只兔子至少分到 根胡萝卜,有多少种方案?
我们先介绍隔板法(插板法),是指在 个元素的 个空中插入 个板,可以把 个元素分为 组。
我们把这 个胡萝卜排成 行,当中就有 个空。现在往里面插入 个板,就可以将胡萝卜分为 组,正好可以分给 只兔子,而且由于不存在在同一个地方插两个板的情况,所以正好每一只兔子都能至少分到 根胡萝卜。那么答案就是 。
实际上这个问题相当于求不定方程 的正整数解的数量。
如果他是个大魔王(不可能,绝对不可能),有的兔子可能 根胡萝卜都得不到,那么有多少种方案?
同样的方法,如果允许有兔子分到 根胡萝卜,我们只需要再加上 根胡萝卜,就相当于刚才的问题了。答案是 。
这个问题本质上是要求 的自然数解的数量。
如果
james1
偏爱一些兔子,要求第 个兔子至少分到 个胡萝卜,那么有多少种分法呢?
类比上一个问题,我们再加上 个胡萝卜,答案就是 。
在 个数中选 个组合,要求任意两个数都不相邻,那么方案数有多少?
,因为我们需要插入 个空。
简单问题
真的只是加法原理和乘法原理而已。
[Luogu P1866] 编号
为了尽可能防止负数需要先排序,然后使用乘法原理计算。但如果负数真的出现了,就无解。
查看代码
#include <bits/stdc++.h>
#define i64 long long
using namespace std;
const i64 MOD = 1000000007;
int n;
int a[55];
i64 ans = 1;
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i);
sort(a + 1, a + n + 1);
for (int i = 1, k; i <= n; i++) {
if ((k = a[i] - (i - 1)) > 0) ans = ans * k % MOD;
else return puts("0"), 0;
}
printf("%d\n", ans);
return 0;
}
[NOIP2016 提高组] 组合数问题
我们直接利用杨辉三角预处理组合数,然后使用二维前缀和计算答案即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
int C[2005][2005];
int ans[2005][2005];
int main(void) {
int T, k;
scanf("%d%d", &T, &k);
for (int i = 0; i <= 2002; ++i) {
C[i][0] = 1, C[i][i] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % k;
}
for (int i = 0; i <= 2002; ++i) {
for (int j = 1; j <= i; ++j)
ans[i][j] = ans[i - 1][j] + ans[i][j - 1] - ans[i - 1][j - 1] + (C[i][j] == 0);
ans[i][i + 1] = ans[i][i];
}
while (T--) {
int n, m;
scanf("%d%d", &n, &m);
printf("%d\n", ans[n][min(n, m)]);
}
return 0;
}
[Luogu P1287] 盒子与球
设 代表考虑前 个球,有 个盒子的方案数。显然可以是由前 个球, 个盒子放在这 个盒子中的任意一个,也可以只考虑 个盒子,只能放在第 个盒子,但是这第 个盒子的位置可以任意摆放。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, r;
int f[15][15];
int main(void) {
scanf("%d%d", &n, &r);
f[0][0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= min(i, r); ++j)
f[i][j] = j * (f[i - 1][j] + f[i - 1][j - 1]);
printf("%d\n", f[n][r]);
return 0;
}
那么盒子相同呢?这样可以乘 的就只有 了,可以做一下 [Luogu P1655] 小朋友的球。
[Luogu P2638] 安全系统
不难看出放 01 可以分开计算,最后使用乘法原理组合在一起。而任何一个都可以使用插板法的结论进行计算。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long u64;
int n, a, b;
u64 C[105][105];
int main(void) {
C[0][0] = 1;
for (int i = 1; i <= 100; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
scanf("%d%d%d", &n, &a, &b);
u64 ans = 0;
for (int i = 0; i <= a; ++i)
for (int j = 0; j <= b; ++j)
ans += C[n + i - 1][i] * C[n + j - 1][j];
printf("%llu\n", ans);
return 0;
}
[Luogu P8557] 炼金术
一种金属有 种可能被熔炼出来,然后每一种金属的可能都要乘起来(乘法原理)。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
inline int poww(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1ll * res * a % MOD;
a = 1ll * a * a % MOD;
b >>= 1;
}
return res;
}
int main(void) {
int n, k; cin >> n >> k;
cout << poww(poww(2, k) - 1, n) << '\n';
return 0;
}
[UVA11609] Teams
队长有 中可能,每一个人当队长剩下的人都有 种可能。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1000000007;
int poww(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1ll * res * a % MOD;
a = 1ll * a * a % MOD;
b >>= 1;
}
return res;
}
int main(void) {
int T, kase = 0, n;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
printf("Case #%d: %d\n", ++kase, 1ll * n * poww(2, n - 1) % MOD);
}
return 0;
}
组合工具
计数问题有一些常见的工具可以辅助计算。
组合公式
还记得有关组合数的公式吗?事实上,最重要的内容有名有姓,并且非常有用:
- 吸收恒等式:,当二项式外有一个无用的系数时,我们可以将它“吸收”进二项式系数。
- 下指标求和(行求和):,相当于是二项式定理中 ,它还有变式:
- ,这是二项式定理中 ;
- ,因为 。
- 上指标求和(列求和):,可以看作是枚举第 个数的位置 。
- 对角线求和:,反复利用 即可证明。
- 范德蒙德卷积:。从组合意义上很容易证明(枚举 和 中选的个数),常用于合并组合数,考虑它的推论:
- ,证明很简单,因为 ;
- ,证明基本同理;
- ,这个也是网格图路径计数方案。
N 项式定理
二项式定理也能扩展为 项式定理:
其中
Lucas 定理
Lucas 定理是说,对于质数 ,有:
模板,代码如下:
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, p;
int fac[100005];
void exgcd(int a, int b, int &x, int &y) {
if (b == 0) return x = 1, y = 0, void();
exgcd(b, a % b, y, x);
y -= a / b * x;
}
int inv(int t) {
int x, y;
exgcd(t, p, x, y);
return (x % p + p) % p;
}
int C(int n, int m) {
if (m > n) return 0;
if (m == 0 || m == n) return 1;
return 1ll * fac[n] * inv(fac[m]) % p * inv(fac[n - m]) % p;
}
int Lucas(int n, int m) {
if (m == 0 || m == n) return 1;
return 1ll * Lucas(n / p, m / p) * C(n % p, m % p) % p;
}
int main(void) {
fac[0] = 1;
int T; cin >> T;
while (T--) {
cin >> n >> m >> p;
for (int i = 1; i <= p; ++i) fac[i] = 1ll * fac[i - 1] * i % p;
cout << Lucas(n + m, m) << '\n';
}
return 0;
}
因此可以发现,Lucas 定理面对的模数不会很大,否则是无法计算的。
而且当模数小的时候,往往需要使用 Lucas 定理。因为 可能很大,用公式计算会使得阶乘在模意义下直接变成 。
扩展 Lucas 定理
模板。
容斥原理
容斥原理是非常重要的计数原理,能够对问题的角度进行转化,弱化或者加强问题的限制,从而使问题更加简洁。
引入
容斥原理大家一定不陌生,我们现在要给出 个集合的情况。设全集为 ,拥有属性 的元素构成集合 ,那么:
集合的交集
可以使用全集 减去补集的并集得到。
子集枚举
子集枚举可以让容斥原理的计算变得非常简便,代码如下:
for (int i = S; i; i = i - 1 & S)
这样 枚举的就是 的非空子集。
应用
容斥原理最经典的用处是“至少”与“恰好”之间的转化,实际上是一个子集反演的过程。比较抽象,请参考《组合计数进阶》。
经典计数
包括一些经典问题和计数数列。但是限于本文的定位,并不会讲的很深,更深的内容请参考《组合计数进阶》,并先阅读《多项式与生成函数基础》中的生成函数部分。
错排数
是指没有任何元素出现在其有序位置的排列,也就是说不存在 。
我们使用递推计算这个问题,设 为长度为 的答案。那么可以前 个全部排错,只需要第 个与前 个中的任意一个互换即可,方案数为 ;也可以前面仅有一个没有排错,只需要将这个与 互换,方案数为 (没错排的可以是任意一个)。
因此:。
斯特林数
分为第一类斯特林数和第二类斯特林数。由于后者更为常用,因此我们先来研究后者。
第二类斯特林数
第二类斯特林数使用 或者 来表示,意义是将 的整数划分为 个不交的集合的方案数。显然 。可以采用暴力递推法求解:
什么意思呢?将第 个元素放入一个新的集合有 种方案,将第 个元素插入原来任意一个集合有 的方案,根据加法原理可得递推式。
可能需要记住一些第二类斯特林数(就比如说,一个出现 的问题就很大能与第二类斯特林数有关):
1
1 1
1 3 1
1 7 6 1
1 15 25 10 1
1 31 90 65 15 1
1 63 301 350 140 21 1
1 127 966 1701 1050 266 28 1
1 255 3025 7770 6951 2646 462 36 1
1 511 9330 34105 42525 22827 5880 750 45 1
第二类斯特林数的通项公式:
第一类斯特林数
称之为斯特林轮换数,记作 ,表示将 的整数划分为 个互不区分的非空轮换方案数。
一个轮换是指一个首尾相接的环形排列,两个可以通过旋转而互相得到的轮换是等价的。
第一类斯特林数的递推式:
前者是将 放在一个单独的轮换中,后者是将其放入一个现有的轮换中。
这个玩意没有实用的通项公式。
卡特兰数
前几项长这样:1 1 2 5 14 42 132
,需要记住。它有非常多的含义:
- 个节点可构造的不同二叉树个数;
- 个数不同的出栈序列个数;
- ……
常见公式:
多重集相关问题
我们知道一个多重集 可以不满足互异性。设 代表由 个 组成的多重集。
多重集的排列数 | 多重组合数
这两个是同一个概念,是要求大小为 ,第 个数出现次数为 的多重集 的全排列个数,我们要除掉重复的个数,那么 的全排列个数等于(其实就是把相同的数除掉了):
多重组合数的意义就是先选 个,再在剩下的选择 个,以此类推。
给出 个数字,求出他们排列后能形成的所有整数的和,形成的整数不重复。
数的个数显然就是多重组合数,然后有 个位置,总共的序列个数有多重组合数 个。设所有数的和为 ,那么平均每一位的值就是 ,然后再乘上基数,全是 即可。
查看代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef unsigned long long u64;
int main(void)
{
int n; u64 f[15] = {1};
const u64 one[] = {0, 1, 11, 111, 1111, 11111, 111111, 1111111, 11111111, 111111111, 1111111111, 11111111111, 111111111111};
for (int i = 1; i <= 12; ++i)
f[i] = f[i - 1] * i;
while (scanf("%d", &n) == 1 && n)
{
static int cnt[10];
u64 sum = 0;
memset(cnt, 0, sizeof(cnt));
for (int i = 1, x; i <= n; ++i)
scanf("%d", &x), ++cnt[x], sum += x;
u64 s = f[n];
for (int i = 0; i < 10; ++i) s /= f[cnt[i]];
printf("%llu\n", sum * s * one[n] / n);
}
return 0;
}
Problemset
这里的题目都是比较基础的数数题。
简单问题
可以用来巩固基础。
[USACO20FEB] Help Yourself G
在一个数轴上有 条线段,第 条线段覆盖了 。
定义若干条线段的并为一个包含了所有被至少一个线段覆盖的点的集合。
定义若干条线段的复杂度为这些线段的并形成的连通块的数目。
现在 Bessie 想要求出给定 条线段的所有子集(共有 个)的复杂度之和对 取模的结果。
好题!似乎没有什么特殊做法,那么考虑计数 DP。设 代表考虑前 条线段的复杂度之和。如果不选第 条线段,那么复杂度和是 ;如果选第 条线段,那么是多少?不好做!加入这一条线段之后可能新增联通块,也可能合并联通块,鬼知道贡献是什么!
换句话说,状态的转移顺序有问题,导致贡献不好计算。我们可以按照线段的左端点从小到大排序之后再进行计算。这样就不可能合并联通块,所以原来的贡献 都在;新增联通块是可以的,假设前面与当前线段不交的线段有 条(可以使用前缀和统计),那么新增的复杂度就是 (就是只选这 条线段的子集时,复杂度会增加 )。
查看代码
#include <bits/stdc++.h>
#define L first
#define R second
using namespace std;
const int MOD = 1000000007;
int n, f[100005], s[200005];
pair<int, int> a[100005];
int poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % MOD)
if (b & 1) res = 1ll * res * a % MOD;
return res;
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d%d", &a[i].L, &a[i].R), s[a[i].R]++;
sort(a + 1, a + n + 1);
for (int i = 1; i <= n * 2; ++i) s[i] += s[i - 1];
for (int i = 1; i <= n; ++i)
f[i] = (f[i - 1] * 2 % MOD + poww(2, s[a[i].L - 1])) % MOD;
printf("%d\n", f[n]);
return 0;
}
[CF340E] Iahub and Permutations
本质上是一个错排问题,但是其中的 可以有随便填的,加一个转移来处理它!
查看代码
#include <bits/stdc++.h>
using namespace std;
const int P = 1000000007;
int n, a[2005], tot1, tot2; // 不能放自身,可以随便放
bool p[2005];
int f[2005];
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i);
if (a[i] == i) return puts("0"), 0;
if (a[i] != -1) p[a[i]] = true;
else ++tot1;
}
for (int i = 1; i <= n; ++i)
if (a[i] == -1 && p[i]) tot1--, tot2++;
for (int i = f[0] = 1; i <= tot2; ++i) f[0] = 1ll * f[0] * i % P;
f[1] = 1ll * tot2 * f[0] % P;
for (int i = 2; i <= tot1; ++i) {
f[i] = 1ll * (f[i - 1] + f[i - 2]) * (i - 1) % P;
f[i] = (f[i] + 1ll * tot2 * f[i - 1]) % P;
}
return !printf("%d\n", f[tot1]);
}
[AHOI2022] 排列
建出置换的有向图,每个环独立,那么 就是每个环大小的 LCM。如果 在一个环里,那么 。
这个 swap 操作相当于合并了两个环,设第 个环的大小为 ,那么由于 ,因此值不同的 在 级别。我们只需要暴力枚举两个环的大小,这样的时间复杂度是 的。
也就是说,我们需要维护一个集合的 LCM,支持删除两个数并加入一个新的数求出 LCM。将数质因数分解,对于每个质数只保留三个幂次最大的(删除两个数时最坏只会删掉两个最大的),加入的时候暴力统计一下即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int P = 1000000007;
const int N = 500000;
void add(int &x, int t) { x += t; if (x >= P) x -= P; }
int n;
int a[500005];
int fa[500005], siz[500005];
int find(int x) {
if (fa[x] == x) return x;
return find(fa[x]);
}
void uni(int x, int y) {
x = find(x), y = find(y);
if (x == y) return;
if (siz[x] > siz[y]) swap(x, y);
fa[x] = y; siz[y] += siz[x];
}
int cnt[500005];
int prime[500005], tot, p[500005], inv[500005]; // p: 最小质因子
vector<pair<int, int>> fac[500005]; // fac[i] 保存 i 质因数分解后的结果,first 为底数,second 为幂
vector<int> G[500005]; // G[i] 保存质数 i 为底数的最大幂
vector<pair<int, int>> E[500005]; // E 为临时附加
int A[1005], t, now;
int C[500005];
int get(int v) { // 获取质数 v 的最大幂
int mx = 1;
for (int x : G[v]) ++C[x];
for (auto [x, k] : E[v]) C[x] += k;
for (int x : G[v]) if (C[x]) mx = max(mx, x), C[x] = 0;
for (auto [x, k] : E[v]) if (C[x]) mx = max(mx, x), C[x] = 0;
return mx;
}
void update(int v, int flag) {
for (auto [x, y] : fac[v]) {
now = 1ll * now * inv[get(x)] % P;
E[x].emplace_back(y, flag);
now = 1ll * now * get(x) % P;
}
}
void remove(int v) {
for (auto [x, y] : fac[v]) E[x].clear();
}
int main(void) {
p[1] = inv[1] = 1;
for (int i = 2; i <= N; ++i) inv[i] = 1ll * (P - P / i) * inv[P % i] % P;
for (int i = 2; i <= N; ++i) {
if (!p[i]) prime[++tot] = i, p[i] = i;
for (int j = 1; j <= tot && i * prime[j] <= N; ++j) {
p[i * prime[j]] = prime[j];
if (i % prime[j] == 0) break;
}
}
for (int i = 1; i <= N; ++i) {
int x = i;
while (x != 1) {
int tmp = p[x], v = 1;
while (x % tmp == 0) x /= tmp, v *= tmp;
fac[i].emplace_back(tmp, v);
}
}
int T; scanf("%d", &T);
while (T--) {
memset(cnt, 0, sizeof cnt); t = 0;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", a + i), fa[i] = i, siz[i] = 1;
for (int i = 1; i <= n; ++i) uni(i, a[i]), G[i].clear();
for (int i = 1; i <= n; ++i) if (find(i) == i) {
++cnt[siz[i]];
for (auto [x, y] : fac[siz[i]]) G[x].emplace_back(y);
}
for (int i = 1; i <= n; ++i) if (cnt[i]) A[++t] = i;
now = 1;
for (int i = 1; i <= n; ++i) if (G[i].size()) {
sort(G[i].begin(), G[i].end(), greater<int>());
while (G[i].size() > 3) G[i].pop_back();
now = 1ll * now * G[i][0] % P;
}
int ans = 0;
for (int i = 1; i <= t; ++i) {
int u = A[i];
if (cnt[u] > 1) {
int last = now;
update(u, -1); update(u, -1); update(u + u, 1);
add(ans, 1ll * cnt[u] * (cnt[u] - 1) % P * u % P * u % P * now % P);
remove(u); remove(u + u);
now = last;
}
for (int j = i + 1; j <= t; ++j) {
int last = now, v = A[j];
update(u, -1); update(v, -1); update(u + v, 1);
add(ans, 2ll * cnt[u] * cnt[v] % P * u % P * v % P * now % P);
remove(u); remove(v); remove(u + v);
now = last;
}
}
printf("%d\n", ans);
}
return 0;
}
[CF348D] Turtles
只有一只乌龟的话是经典问题,现在考虑两条路径什么时候有交。
只能是一只乌龟从 走到 ,另一只乌龟从 走到 。如果一只乌龟从 走到 ,另一只乌龟从 走到 ,那么这样路径是会有交的。
但是发现前者自身也有可能有路径交!比如说中途在一个点路径相交。我们把这个点之后的路径互换,发现都可以对应到第二种的走法。这样只需要用前者减去后者即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int P = 1000000007;
int n, m;
char s[3005][3005];
int f[3005][3005];
int dp(int x, int y, int n, int m) {
memset(f, 0, sizeof f);
for (int i = x; i <= n; ++i)
for (int j = y; j <= m; ++j) if (s[i][j] != '#') {
if (i == x && j == y) f[i][j] = 1;
else f[i][j] = (f[i - 1][j] + f[i][j - 1]) % P;
}
return f[n][m];
}
int main(void) {
scanf("%d%d", &n, &m); f[1][1] = 1;
for (int i = 1; i <= n; ++i) scanf("%s", s[i] + 1);
printf("%d\n", ((1ll * dp(1, 2, n - 1, m) * dp(2, 1, n, m - 1) - 1ll * dp(1, 2, n, m - 1) * dp(2, 1, n - 1, m)) % P + P) % P);
return 0;
}
实际上本题是 LGV 引理的一个应用,相关内容在《组合计数进阶》有介绍。由于本题只有两只乌龟,因此可以比较方便的用容斥原理理解。
计数问题
借助动态规划算法的多阶段决策,可以高效的解决计数问题。
[HAOI2008] 硬币购物
共有 种硬币。面值分别为 。
某人去商店买东西,去了 次,对于每次购买,他带了 枚 种硬币,想购买 的价值的东西。请问每次有多少种付款方法。
如果每一种硬币都有无限多,那么这就成了个完全背包,直接做就可以,记为 。
接下来考虑性质。利用补集思想,我们可以用 减去至少有一个超过。也就是 ,以此类推。使用容斥原理计算即可。
查看代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long i64;
const int M = 100000;
int c1, c2, c3, c4, q;
i64 f[M + 5];
inline i64 g(i64 x) { return x < 0 ? 0 : f[x]; }
int main(void) {
cin >> c1 >> c2 >> c3 >> c4 >> q;
f[0] = 1;
for (int i = c1; i <= M; ++i) f[i] += f[i - c1];
for (int i = c2; i <= M; ++i) f[i] += f[i - c2];
for (int i = c3; i <= M; ++i) f[i] += f[i - c3];
for (int i = c4; i <= M; ++i) f[i] += f[i - c4];
while (q--) {
int d1, d2, d3, d4, s;
cin >> d1 >> d2 >> d3 >> d4 >> s;
d1 = (d1 + 1) * c1, d2 = (d2 + 1) * c2, d3 = (d3 + 1) * c3, d4 = (d4 + 1) * c4;
cout << g(s) - g(s - d1) - g(s - d2) - g(s - d3) - g(s - d4) +
g(s - d1 - d2) + g(s - d1 - d3) + g(s - d1 - d4) + g(s - d2 - d3)
+ g(s - d2 - d4) + g(s - d3 - d4) - g(s - d1 - d2 - d3) - g(s - d1 - d3 - d4)
- g(s - d1 - d2 - d4) - g(s - d2 - d3 - d4) + g(s - d1 - d2 - d3 - d4) << '\n';
}
return 0;
}
[ZJOI2010] 排列计数
发现这个序列描述的就是一个小根堆。设 为 个数组成的满足排列的个数。我们先计算出这个堆的根节点的左子节点个数和右子节点个数,那么当前方案数就是左子树方案数和右子树方案数。左子树可以在 中选择 个,那么右子树能选的数也就确定,方案数就是 了。注意使用 Lucas 定理计算( 可能大于模数,逆元计算不成立)。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, p;
int f[1000005], lg[1000005], fac[1000005];
int poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % p)
if (b & 1) res = 1ll * res * a % p;
return res;
}
int C(int n, int m) {
return 1ll * fac[n] * poww(fac[m], p - 2) % p * poww(fac[n - m], p - 2) % p;
}
int Lucas(int n, int m) {
if (m == n || m == 0) return 1;
return 1ll * Lucas(n / p, m / p) * C(n % p, m % p) % p;
}
int main(void) {
scanf("%d%d", &n, &p);
f[0] = f[1] = 1;
for (int i = 2; i <= n; ++i) lg[i] = lg[i >> 1] + 1;
fac[0] = 1;
for (int i = 1; i <= n; ++i) fac[i] = 1ll * fac[i - 1] * i % p;
int l = 0, r = 0;
for (int i = 2; i <= n; ++i) {
int dep = lg[i] + 1;
if (i < ((1 << dep) - (1 << (dep - 2)))) ++l;
else ++r;
f[i] = 1ll * f[l] * Lucas(i - 1, l) % p * f[r] % p;
}
printf("%d\n", f[n]);
return 0;
}
『JROI-4』沈阳大街 2
相当于是可以随便排,那么将 合并为 ,将其从大到小排序后可得贡献是最后一个数。设 代表考虑到 选了 对,然后转移。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
int poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % MOD)
if (b & 1) res = 1ll * res * a % MOD;
return res;
}
int n, f[10005][5005], cnt[2][10005];
pair<int, bool> a[10005];
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i].first);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i + n].first), a[i + n].second = 1;
sort(a + 1, a + n * 2 + 1, greater<pair<int, bool>>());
f[0][0] = 1;
for (int i = 1; i <= n * 2; ++i) {
cnt[0][i] = cnt[0][i - 1], cnt[1][i] = cnt[1][i - 1];
f[i][0] = 1; ++cnt[a[i].second][i];
int t = cnt[!a[i].second][i];
for (int j = 1; j <= (i >> 1); ++j) {
if (t - j + 1 >= 0)
f[i][j] = 1ll * f[i - 1][j - 1] * a[i].first % MOD * (t - j + 1) % MOD;
f[i][j] = (f[i][j] + f[i - 1][j]) % MOD;
}
}
int res = 1;
for (int i = 1; i <= n; ++i) res = 1ll * res * i % MOD;
printf("%d\n", 1ll * poww(res, MOD - 2) * f[n * 2][n] % MOD);
return 0;
}
经典模型
这些问题与经典模型关系很大!
[SDOI2016] 排列计数
除掉这 个数就是错排问题,而选择 个数共有 种方案。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1000000007;
const int N = 1000000;
int n, m;
int fac[1000005], f[1000005];
int poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % MOD)
if (b & 1) res = 1ll * res * a % MOD;
return res;
}
int C(int n, int m) {
return 1ll * fac[n] * poww(1ll * fac[m] * fac[n - m] % MOD, MOD - 2) % MOD;
}
int main(void) {
int T; scanf("%d", &T);
fac[0] = 1; f[2] = 1; f[0] = 1;
for (int i = 1; i <= N; ++i) fac[i] = 1ll * fac[i - 1] * i % MOD;
for (int i = 3; i <= N; ++i) f[i] = 1ll * (i - 1) * (f[i - 1] + f[i - 2]) % MOD;
while (T--) {
scanf("%d%d", &n, &m);
printf("%d\n", 1ll * C(n, m) * f[n - m] % MOD);
}
return 0;
}
[CF785D] Anton and School - 2
直接暴力计数,然后使用范德蒙德卷积化简。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const int MOD = 1000000007;
int n, a[200005], b[200005];
i64 fac[200005] = {1};
char s[200005];
i64 poww(i64 a, int b) {
i64 res = 1; a %= MOD;
for (; b; b >>= 1, a = a * a % MOD)
if (b & 1) res = res * a % MOD;
return res;
}
int main(void) {
scanf("%s", s + 1); n = strlen(s + 1);
for (int i = 1; i <= n; ++i) a[i] = a[i - 1] + (s[i] == '(');
for (int i = n; i >= 1; --i) b[i] = b[i + 1] + (s[i] == ')');
for (int i = 1; i <= n; ++i) fac[i] = fac[i - 1] * i % MOD;
i64 ans = 0;
for (int i = 1; i <= n; ++i) if (s[i] == '(') {
int x = a[i], y = b[i];
ans = (ans + fac[x + y - 1] * poww(fac[y - 1] * fac[x], MOD - 2)) % MOD;
}
printf("%lld\n", ans);
return 0;
}
[Cnoi2020] 四角链
设 为答案,那么 。试几个数发现 ,因此使用第二类斯特林数求解即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
int n, m, k;
int fac[1000005];
int f[5005][5005];
int poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % MOD)
if (b & 1) res = 1ll * res * a % MOD;
return res;
}
int main(void) {
scanf("%d%d", &n, &k); m = n - k; fac[0] = 1;
for (int i = 1; i <= n; ++i) fac[i] = 1ll * fac[i - 1] * i % MOD;
int ans = 0;
for (int i = 0; i <= m; ++i) {
int tmp = 1ll * poww(i, n) * poww(fac[i], MOD - 2) % MOD * poww(fac[m - i], MOD - 2) % MOD;
ans = (ans + tmp * ((m - i) % 2 == 0 ? 1 : -1)) % MOD;
}
printf("%d\n", (ans + MOD) % MOD);
return 0;
}
[NOI2021] 量子通信
是关键突破点,我们把每 位压成一位,必然有一个数是不同的(鸽巢原理)。将相同的压成一位的单词存在一个 vector 里,暴力判断即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
bool s[400005][256];
inline ull myRand(ull &k1, ull &k2) {
ull k3 = k1, k4 = k2;
k1 = k4;
k3 ^= (k3 << 23);
k2 = k3 ^ k4 ^ (k3 >> 17) ^ (k4 >> 26);
return k2 + k4;
}
void gen(int n, ull a1, ull a2) {
for (int i = 1; i <= n; i++)
for (int j = 0; j < 256; j++)
s[i][j] = (myRand(a1, a2) & (1ull << 32)) ? 1 : 0;
}
int n, m, val[400005][16], aa[16];
vector<int> buc[16][65536];
ull a1, a2;
bool a[256];
int main(void) {
cin >> n >> m >> a1 >> a2; gen(n, a1, a2);
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < 16; ++j) {
for (int k = 0; k < 16; ++k)
val[i][j] += (s[i][j * 16 + k] << k);
buc[j][val[i][j]].emplace_back(i);
}
}
bool last = 0; int k;
while (m--) {
for (int i = 0; i < 64; ++i) {
char c = getchar(); int v;
if (isdigit(c)) v = c - '0';
else if (c >= 'A' && c <= 'F') v = 10 + c - 'A';
else { --i; continue; }
for (int j = 0; j < 4; ++j)
a[i * 4 + j] = (((v >> (3 - j)) & 1) ^ last);
}
bool flag = 0; scanf("%d", &k);
memset(aa, 0, sizeof(aa)); // aa 存储当前字符串被压缩后的结果
for (int i = 0; i < 16; ++i)
for (int j = 0; j < 16; ++j)
aa[i] += (a[i * 16 + j] << j);
for (int i = 0; i < 16 && !flag; ++i) {
for (auto x : buc[i][aa[i]]) {
int it = x, cnt = 0;
for (int j = 0; j < 16 && cnt <= k; ++j)
cnt += __builtin_popcount(val[it][j] ^ aa[j]);
if (cnt <= k) { flag = true; break; }
}
}
printf("%d\n", (last = flag));
}
return 0;
}
[JSOI2015] 染色问题
设 代表最多使用 种颜色完成目标的方案数。考虑 代表有第 种颜色的方案集合。“每个颜色都至少出现一次”为 ,可以写为:
后面这个 是说至少不选 种颜色,有 种选择方式,单个选择方式有 种合法方案数。可以使用容斥原理计算,那么最终:
也可以使用类似的方式计算出来。 代表第 列有颜色的方案集合,然后推导出来的 就是 这 列没有颜色,那么:
查看代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long i64;
const int MOD = 1000000007;
i64 poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % MOD)
if (b & 1) res = 1ll * res * a % MOD;
return res;
}
void add(i64 &a, int t) {
a = ((a + t) % MOD + MOD) % MOD;
}
int n, m, c;
int C[405][405];
i64 f[405];
int main(void) {
for (int i = 0; i <= 400; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;
}
scanf("%d%d%d", &n, &m, &c);
for (int i = 1; i <= c; ++i)
for (int k = 0; k <= m; ++k)
add(f[i], (k & 1 ? -1 : 1) * C[m][k] * poww(poww(i + 1, m - k) - 1, n) % MOD);
i64 ans = 0;
for (int i = 0; i <= c; ++i)
add(ans, (i & 1 ? -1 : 1) * C[c][i] * f[c - i] % MOD);
printf("%lld\n", ans);
return 0;
}
数学推导
推式子(柿子)是一件有趣的事情!
[CF1545B] AquaMoon and Chess
只有 和 是有用的,答案是 。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
int n;
char a[100005];
int poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % MOD)
if (b & 1) res = 1ll * res * a % MOD;
return res;
}
int C(int n, int m) {
int res = 1;
for (int i = 1; i <= m; ++i)
res = 1ll * res * (n - i + 1) % MOD * poww(i, MOD - 2) % MOD;
return res;
}
void solve(void)
{
cin >> n >> a;
int x = 0, y = 0;
for (int i = 0; i < n; ++i)
if (i < n - 1 && a[i] == '1' && a[i + 1] == '1') ++x, ++i;
else if (a[i] == '0') ++y;
cout << C(x + y, y) << '\n';
}
int main(void)
{
ios::sync_with_stdio(false);
int T; cin >> T;
while (T--) solve();
return 0;
}
[SCOI2010] 生成字符串
把 个 和 个 组成字符串,在任意的前 个字符中, 的个数不能少于 的个数。满足要求的字符串共有多少个?
考虑其几何意义:选 1 代表向右上走,选 0 代表向右下走,要走到 ,而且需要满足任意时刻的纵坐标是非负的。
怎么办呢?利用补集思想,总共有 种走法(网格图路径计数),能够走到 ,相当于从 开始走,,走到 ,方案数为 ,因此答案为 。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 20100403;
int fac[2000005];
int poww(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1ll * res * a % MOD;
a = 1ll * a * a % MOD;
b >>= 1;
}
return res;
}
int inv(int a) {
return poww(a, MOD - 2);
}
int C(int n, int m) {
return 1ll * fac[n] * inv(fac[m]) % MOD * inv(fac[n - m]) % MOD;
}
int main(void) {
fac[0] = 1;
for (int i = 1; i <= 2000000; ++i) fac[i] = 1ll * fac[i - 1] * i % MOD;
int n, m;
scanf("%d%d", &n, &m);
printf("%d\n", (C(n + m, m) - C(n + m, m - 1) + MOD) % MOD);
return 0;
}
[GXOI/GZOI2019] 逼死强迫症
设 表示 的答案,那么 。使用矩阵快速幂计算即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1000000007;
struct Matrix {
int a[5][5];
Matrix() { memset(a, 0, sizeof(a)); }
Matrix friend operator * (const Matrix &a, const Matrix &b) {
Matrix c;
for (int i = 0; i < 5; ++i)
for (int k = 0; k < 5; ++k) {
int r = a.a[i][k];
for (int j = 0; j < 5; ++j)
c.a[i][j] = (c.a[i][j] + 1ll * r * b.a[k][j]) % MOD;
}
return c;
}
};
Matrix poww(Matrix a, int b) {
Matrix res; for (int i = 0; i < 5; ++i) res.a[i][i] = 1;
for (; b; b >>= 1, a = a * a) if (b & 1) res = res * a;
return res;
}
int main(void) {
int T, n; scanf("%d", &T);
Matrix f, a; a.a[2][0] = a.a[3][0] = a.a[4][0] = 1;
f.a[0][0] = f.a[0][1] = f.a[1][0] = f.a[2][2] = f.a[2][3]
= f.a[3][3] = f.a[3][4] = f.a[4][3] = 1; f.a[0][2] = 2;
Matrix t = f * a;
while (T--) {
scanf("%d", &n);
if (n <= 2) { puts("0"); continue; }
printf("%d\n", (poww(f, n - 2) * a).a[0][0]);
}
return 0;
}
「KDOI-02」一个仇的复
你有 ( 为任意正整数)的矩形各无穷多个和一个 的网格,请求出恰好选择其中 个矩形(可以选择相同的矩形)不重不漏地铺满整个网格的方案数。矩形可以旋转。答案对 取模,。
先解决一个简单问题,只允许横着放?插板,枚举第 行用 个,那么总方案数为 ,用范德蒙德卷积合并可得答案是 。
由于宽度为 ,竖着的长方形仅能有 。考虑先用 个 的竖着的长方形,然后分割成了 的小长方形,独立统计即可。
设要分成 段,使用 个竖着的,那么分段方式插板计算有 种(只有两头允许放空,增加 个板),然后要将剩余的 个位置分给 段,插板有 种方案。
然后就是把 个横着的放到每段长度为 的每段中,方案数:
这是个什么?想一想范德蒙德卷积的组合意义,就会发现跟那个是一样的:枚举每个子集选的个数,选的总数是 。于是我们将它合起来(就是原本二项式系数上面的和下面的和形成新的二项式系数),就成了:。
注意一下需要特判 的时候,可以放 个竖着的。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
inline int poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % MOD)
if (b & 1) res = 1ll * res * a % MOD;
return res;
}
int n, k;
int fac[40000005];
int C(int n, int m) {
return 1ll * fac[n] * poww(1ll * fac[m] * fac[n - m] % MOD, MOD - 2) % MOD;
}
int main(void) {
scanf("%d%d", &n, &k); fac[0] = 1;
for (int i = 1; i <= n * 2; ++i) fac[i] = 1ll * fac[i - 1] * i % MOD;
int ans = 0;
for (int i = 1; i <= k; ++i) {
int R = min({k - 2 * i, n - i, 2 * n - k});
for (int j = i - 1; j <= R; ++j) {
int a = C(2 * (n - j) - 2 * i, k - j - 2 * i);
int b = C(j + 1, i), c = C(n - j - 1, i - 1);
ans = (ans + 1ll * a * b % MOD * c % MOD) % MOD;
}
}
printf("%lld\n", ans + (k == n));
return 0;
}
[FJOI2017] 矩阵填数
给定一个 的矩阵,在这个矩阵中你需要在每个格子中填入 中的某个数。
给这个矩阵填数的时候有一些限制,给定 个该矩阵的子矩阵,以及该子矩阵的最大值 ,要求你所填的方案满足该子矩阵的最大值为 。
现在,你的任务是求出有多少种填数的方案满足 个限制。输出答案对 取模后的结果。
对于 的数据,,,,。
没有矩形限制的地方是可以任意填的。
将限制条件按照 从小到大排序,然后大小相同的统一处理(先考虑限制更严的,限制更松的自然满足)。对于一个 ,使用一个套路:最大值等于 可以转化为最大值不超过 和最大值不超过 。这样就可以很方便的使用容斥原理计算了。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const int MOD = 1000000007;
const int N = 10;
i64 poww(i64 a, int b) {
int res = 1;
for (; b; b >>= 1, a = a * a % MOD)
if (b & 1) res = res * a % MOD;
return res;
}
int h, w, m, n;
int bitcount[1030];
int s[1030], u[1030]; // 交集,并集
struct rec {
int x1, y1, x2, y2, v;
rec(int x1 = 0, int y1 = 0, int x2 = 0, int y2 = 0) :
x1(x1), y1(y1), x2(x2), y2(y2) {}
void rd(void) { scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &v); }
bool empty(void) { return x1 > x2 || y1 > y2; }
int square(void) { return (x2 - x1 + 1) * (y2 - y1 + 1); }
void operator&= (const rec &a) {
x1 = max(x1, a.x1), y1 = max(y1, a.y1);
x2 = min(x2, a.x2), y2 = min(y2, a.y2);
}
bool operator< (const rec &a) const { return v < a.v; }
} a[15];
int main(void) {
for (int s = 0; s < 1 << N; ++s)
bitcount[s] = bitcount[s >> 1] + (s & 1);
int T; scanf("%d", &T); while (T--) {
scanf("%d%d%d%d", &h, &w, &m, &n); int xtot = 0, ytot = 0;
for (int i = 0; i < n; ++i) a[i].rd(); sort(a, a + n);
for (int i = 1; i < 1 << n; ++i) {
rec tmp(1, 1, h, w); s[i] = -1;
for (int p = i, j = 0; p; p >>= 1, ++j)
if (p & 1) {
tmp &= a[j];
if (tmp.empty()) { s[i] = 0; break; }
}
if (s[i] == -1) s[i] = tmp.square();
}
for (int i = 1; i < 1 << n; ++i) {
u[i] = 0;
for (int j = i; j; j = j - 1 & i)
if (bitcount[j] % 2) u[i] += s[j]; else u[i] -= s[j];
}
int ns = 0, ls = 0; i64 res = 1;
for (int i = 0; i < n; ++i) {
ns |= (1 << i); if (a[i].v == a[i + 1].v) continue;
int tot = u[ns | ls] - u[ls], st = tot;
i64 ret = poww(a[i].v, tot); // 最大值不超过 v
for (int j = ns; j; j = j - 1 & ns) {
tot = u[j | ls] - u[ls];
i64 del = poww(a[i].v - 1, tot) * poww(a[i].v, st - tot) % MOD; // 枚举的子集中的这些最大值不超过 v-1,剩下的随便
if (bitcount[j] % 2) ret = (ret - del + MOD) % MOD;
else ret = (ret + del) % MOD;
}
res = res * ret % MOD; ls |= ns; ns = 0;
}
printf("%lld\n", res * poww(m, h * w - u[(1 << n) - 1]) % MOD);
}
return 0;
}
组合综合
这里的问题会与数论、DP 等内容结合在一起,综合性会比较强。
[SDOI2010] 古代猪文
翻译成人话,就是要求:
使用费马小定理降幂可得:
,这样小的质数模数已经可以使用 Lucas 定理,求四遍之后使用 CRT 合并出解即可。
查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int MOD = 999911659;
int fac[40005] = {1};
int poww(int a, int b, int p) {
int res = 1;
while (b) {
if (b & 1) res = 1ll * res * a % p;
a = 1ll * a * a % p;
b >>= 1;
}
return res % p;
}
void exgcd(int a, int b, int &x, int &y) {
if (b == 0) return x = 1, y = 0, void();
exgcd(b, a % b, y, x);
y -= a / b * x;
}
int inv(int t, int p) {
int x, y; exgcd(t, p, x, y);
return (x % p + p) % p;
}
int C(int n, int m, int p) {
if (m > n) return 0;
if (m == 0 || m == n) return 1;
return 1ll * fac[n] * inv(fac[m], p) % p * inv(fac[n - m], p) % p;
}
int Lucas(int n, int m, int p) {
if (m == 0 || m == n) return 1;
return 1ll * Lucas(n / p, m / p, p) * C(n % p, m % p, p) % p;
}
int a[10], m[10] = {0, 2, 3, 4679, 35617}, M[10];
int CRT(void) {
int ans = 0, x;
for (int i = 1; i <= 4; ++i) {
M[i] = (MOD - 1) / m[i];
ans = (ans + 1ll * a[i] * M[i] % (MOD - 1) * inv(M[i], m[i])) % (MOD - 1);
}
return (ans % (MOD - 1) + (MOD - 1)) % (MOD - 1);
}
int n, g;
int calc(void) {
for (int op = 1; op <= 4; ++op) {
for (int i = 1; i <= 40000; ++i) fac[i] = 1ll * fac[i - 1] * i % m[op];
for (int i = 1; i * i <= n; ++i)
if (n % i == 0) {
a[op] = (a[op] + Lucas(n, i, m[op])) % m[op];
if (i * i != n) a[op] = (a[op] + Lucas(n, n / i, m[op])) % m[op];
}
}
return CRT();
}
int main(void) {
cin >> n >> g;
int t = calc();
if (g == MOD && t == 0) puts("0"); // 注意这里,因为费马小定理取模的原因,所以 t == 0 时其实 t == k(MOD - 1),所以答案是 0 不是 1,扩展欧拉定理降幂的时候也有类似的问题
else cout << poww(g, t, MOD) << '\n';
return 0;
}
组合数奇偶性公式 | [CTSC2017] 吉夫特
结论:。使用 Lucas 定理来证明,需保证不出现 。这就是组合数奇偶性公式。
于是直接整一个 DP,设 代表以 结尾的子序列个数,然后采用刷表转移。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1000000007;
int n, ans = 0;
int f[233340];
int main(void) {
scanf("%d", &n);
for (int i = 1, a; i <= n; ++i) {
scanf("%d", &a); f[a] += 1;
for (int S = a - 1 & a; S; S = S - 1 & a)
f[S] = (f[S] + f[a]) % MOD;
ans = (ans + f[a]) % MOD;
}
printf("%d\n", ans - n); // 减去只有一个数的
return 0;
}
「KDOI-03」构造数组
你现在有一个长度为 的数组 。一开始,所有 均为 。给出一个同样长度为 的目标数组 。求有多少种方案,使得通过若干次以下操作,可以让 数组变成 。
- 选出两个不同的下标 ,并将 和 同时增加 。
两种方案被称之为不同的,当且仅当存在一个 使得一种方案中第 次操作选择的两个下标 与另一种方案中的不同。答案对 取模。
对于 的数据,,,。
整一个 DP。设 代表考虑到第 个数,填好了 个操作数对,可以计算出此时填了 个操作数对的一个数和 个空的操作数对。采用刷表法转移, 可以转移到 ,有 种方案数。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
inline int poww(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % MOD)
if (b & 1) res = 1ll * res * a % MOD;
return res;
}
int n, m;
int b[5005], s[5005], fac[30005], inv[30005];
int f[2][30005];
inline int C(int n, int m) {
return 1ll * fac[n] * inv[m] % MOD * inv[n - m] % MOD;
}
inline void add(int &x, int k) { x = (x + k) % MOD; }
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", b + i), s[i] = s[i - 1] + b[i];
m = s[n];
if (m & 1) return !puts("0");
m >>= 1; fac[0] = 1;
for (int i = 1; i <= m; ++i) fac[i] = 1ll * fac[i - 1] * i % MOD;
inv[m] = poww(fac[m], MOD - 2);
for (int i = m - 1; i >= 0; --i) inv[i] = 1ll * inv[i + 1] * (i + 1) % MOD;
f[0][0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= m; ++j) f[i & 1][j] = 0;
for (int j = 0; j <= m; ++j) {
int m2 = j, m1 = s[i - 1] - 2 * m2, m0 = m - m1 - m2;
if (m0 < 0 || m1 < 0) continue;
int R = min({m1, b[i], m - m2});
for (int k = max(0, b[i] - m0); k <= R; ++k)
add(f[i & 1][m2 + k], 1ll * f[i - 1 & 1][m2] * C(m1, k) % MOD * C(m0, b[i] - k) % MOD);
}
}
printf("%d\n", f[n & 1][m]);
return 0;
}
[CF985G] Team Players
一眼看去在补图上跑三元环计数,然后发现边数爆炸,直接告辞。
但是唯一会的好像就是数三元环。考虑求答案的补集,答案应该是所有三元组的答案,减去至少有一条边的三元组的答案。
然后后面这个怎么做呢?我们肯定是要去看边的,这样就会导致对于一个有两条边的三元组,被统计两次。因此后面这个也需要容斥。
最终答案就是所有三元组的答案(1),减去至少有一条边的答案(2),加上至少有两条边的答案(3),减去有三条边的答案(4)。接下来分别看这四个东西怎么做。
-
枚举 中在三元组 的位置,然后利用乘法原理计算答案。
-
只有一条边,那么枚举所有边 ,不妨令 ,然后令第三个点为 ,考虑 对三元组 的贡献。
- ,此时 , 的贡献为 ;
- ,此时 , 的贡献为 ;
- ,此时 , 的贡献为 ;
- ,此时 , 的贡献为 ;
- ,此时 , 的贡献为 ;
- ,此时 , 的贡献为 ;
- ,此时 , 的贡献为 。
-
两条边,要求的是三个点的链。不妨考虑枚举的是中间点 ,此时 。枚举 的每一条出边到达点 ,设 的出度为 。由于 也会影响 充当的是 还是 ,因此不妨把 也加进 的出边中( 同时也增大 )。设 在这些数中的排名为 ,分两种情况计算 的贡献:
- ,此时考虑第三个点 :
- , 的贡献为 ;
- , 的贡献为 ;
- ,此时考虑第三个点 ;
- , 的贡献为 ;
- , 的贡献为 。
然后对于 自己要进行一个统计,考虑三种情况:
- , 的贡献为 ;
- , 的贡献为 ;
- , 的贡献为 。
- ,此时考虑第三个点 :
-
直接搞一个三元环计数模板就行。
于是就很高兴地做完了,时间复杂度应该是 。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long u64;
int n, m, u[200005], v[200005], deg[200005];
u64 A, B, C;
vector<int> G[200005], E[200005];
u64 calc0(void) {
u64 ans = 0;
for (int i = 0; i < n; ++i) {
ans += A * (n - i - 1) * (n - i - 2) / 2 * i;
ans += B * i * (n - i - 1) * i;
ans += C * i * (i - 1) / 2 * i;
}
cerr << "A0 " << ans << "\n";
return ans;
}
u64 calc1(void) {
u64 ans = 0;
for (int i = 0; i < m; ++i) {
int x = u[i], y = v[i];
ans += A * x * (n - x - 2);
ans += B * x * x;
ans += B * y * (n - y - 1);
ans += C * y * (y - 1);
ans += A * x * (x - 1) / 2;
ans += B * (x + y) * (y - x - 1) / 2;
ans += C * (n + y) * (n - y - 1) / 2;
}
cerr << "A1 " << ans << "\n";
return ans;
}
u64 calc2(void) {
u64 ans = 0;
for (int x = 0; x < n; ++x) {
int t = G[x].size(); // 算 x 自己
for (int i = 0; i < t; ++i) {
int y = G[x][i];
if (y < x) {
ans += A * y * (t - i - 2);
ans += B * y * i;
} else if (y > x) {
ans += B * y * (t - i - 1);
ans += C * y * (i - 1);
} else {
ans += C * i * (i - 1) / 2 * x;
ans += A * (t - i - 1) * (t - i - 2) / 2 * x;
ans += B * i * (t - i - 1) * x;
}
}
}
cerr << "A2 " << ans << "\n";
return ans;
}
int vis[200005];
u64 calc3(void) {
u64 ans = 0; memset(vis, 0xff, sizeof vis);
for (int x = 0; x < n; ++x) {
for (int y : E[x]) vis[y] = x;
for (int y : E[x]) for (int z : E[y]) if (vis[z] == x) {
int t[] = {x, y, z}; sort(t, t + 3);
ans += A * t[0] + B * t[1] + C * t[2];
}
}
cerr << "A3 " << ans << "\n";
return ans;
}
int main(void) {
ios::sync_with_stdio(0);
cin >> n >> m >> A >> B >> C;
for (int i = 0; i < m; ++i) {
cin >> u[i] >> v[i]; ++deg[u[i]]; ++deg[v[i]];
G[u[i]].emplace_back(v[i]); G[v[i]].emplace_back(u[i]);
if (u[i] > v[i]) swap(u[i], v[i]);
}
for (int i = 0; i < n; ++i) G[i].emplace_back(i), sort(G[i].begin(), G[i].end());
for (int i = 0; i < m; ++i) {
int x = u[i], y = v[i];
if (deg[x] > deg[y] || (deg[x] == deg[y] && x > y)) swap(x, y);
E[x].emplace_back(y);
}
cout << calc0() - calc1() + calc2() - calc3() << "\n";
return 0;
}