组合数学与计数原理
组合数就是容斥和计数原理,你学的所有技巧,都是为了能够使用它们。要么找到一个好的划分,要么找到两个独立的对象。
要么构建双射!(x
1 常见公式与定理
1.1 排列数
1.2 组合数
1.3 抽屉原理
个球放在 个箱子中,每个箱子至少有 个球。
1.4 容斥原理
若有集合 ,那么
1.5 min-max 容斥
对于 和 我们有 。
但是我们没有 。 同理。那么我们需要计算后面这个东西怎么办呢?
考虑 表示 集合中的最大值, 相反。那么有
为什么呢?考虑对于 ,如果它等于 ,那么 。
对于其他集合,证明它们都会被抵消。考虑 ,其中 。那么考虑 为 中第一个大于 的数。考虑 ,如果 ,那么 排除 。否则 。容易证明 和 一一对应,并且求和之后为 。
因此一共 个真子集,减去一个 之外所有集合都可以两两配对,最后消掉。
因此结论成立。
如果用通用方法证明:考虑一个数被算了几次。如果有 个数比它大,那么只有其他数都是比它大的数的时候才会被计算,贡献为 。
如果知道其中一个,就可以 知道另一个。
HDU4336
【题意】
有 种卡片,每开一个袋子,有 的概率开出第 种。保证 。求要集齐所有卡片至少一张的期望开卡次数。
【分析】
考虑 为收集到第一张 卡片的期望开卡次数。那么我们要求 。
而我们有 。于是可以做了。
1.6 捆绑法/插空法
个人排队, 和 相邻。求方案数?
将他们两个人捆绑在一起视为一个人,答案为 。
个人排队, 和 不相邻。求方案数?
考虑其他 个人先排,然后 和 分别插进空里。答案为 。
P3166
【题意】
给定一个 的网格,请计算三点都在格点上的三角形共有多少个。注意三角形的三点不能共线。
【分析】
考虑容斥。横着和竖着共线是容易的。考虑斜着共线怎么算。
考虑枚举 ,那么 和 这两个点和 个不同的点共线。注意为什么 ,因为不能是和 共线。
时间复杂度 。
有 做法,需要用到莫比乌斯反演和欧拉反演。待补
https://www.luogu.com.cn/blog/emptyset/solution-p3166
1.7 卡特兰数
递推式:
什么意义?考虑 个节点形成的二叉树的形态数。一个根,左子树 个节点,右子树 个节点。
还有什么意义? 对括号形成合法括号序列的方案数。考虑第一个括号和哪一个括号匹配,这两个括号中间和后面分成两个区域。
还有什么意义? 边形三角划分数,这一块还需要加 条边,加了一条边之后分成了两块。
考虑其通项公式:。
考虑从 开始一步只能往右边或上面走,走到 并且不能超过对角线的方案数,容易证明它等于卡特兰数。
考虑容斥。对于没有不能超过对角线的性质,方案数是 。
对于超过对角线的方案,在第一次超过之后每一次都取反,最后一定走到 。因为这个点在对角线上方,容易证明这样的方案与原方案一一对应。方案数是 。
也可以换一个形式,。
1.8 卢卡斯定理
当 是素数时,
可以用来求解 的 。时间复杂度 。
当 不是素数时,有扩展卢卡斯定理。待补
https://oi-wiki.org/math/number-theory/lucas/
P4478
【题意】
小B 所在的城市的道路构成了一个方形网格,它的西南角为 ,东北角为 。
小B 家住在西南角,学校在东北角。现在有 个路口进行施工,小B 不能通过这些路口。小B 喜欢走最短的路径到达目的地,因此他每天上学时都只会向东或北行走;而小B又喜欢走不同的路径,因此他问你按照他走最短路径的规则,他可以选择的不同的上学路线有多少条。由于答案可能很大,所以小B 只需要让你求出路径数 的值。
或者
【分析】
先对障碍按 排序,这样一定只会从前面的障碍跳到后面的障碍。
令 表示到第 个点且不经过任何障碍的方案数。
表示第 个点走到第 个点的方案数。这个是卡特兰数,需要卢卡斯定理算组合数。
然后有:
不会算重是因为“不经过任何障碍”。
1.9 矩阵优化计数
矩阵乘法是“行 × 列”原则,也就是答案矩阵的第 行第 列是由左矩阵的第 行乘以右矩阵的第 列得到。
对于 阶 层递推,时间复杂度为 。
实际上可以用 FFT 优化到 ,但是现在不需要会(也差不太多)。
主要是代码实现问题需要注意。
P3216
【题意】
小 C 数学成绩优异,于是老师给小 C 留了一道非常难的数学作业题:
给定正整数 ,要求计算 的值,其中 是将 所有正整数 顺序连接起来得到的数。
例如, , 。小C 想了大半天终于意识到这是一道不可能手算出来的题目,于是他只好向你求助,希望你能编写一个程序帮他解决这个问题。
【分析】
考虑线性递推。
我们有:,其中 。
考虑怎么转化为矩阵递推式:
分 块处理,剩下的问题在于实现。
1.10 行列式
代数余子式:对于 ,在 阶行列式中所有不属于第 行也不属于第 列的元素按照原来的顺序组合成一个 阶余子式,它叫做 也就是原矩阵中元素 的代数余子式。
一个 阶行列式的值等于:
其中 其中一个任选。也就是说,任意行(或者列)的元素与之对应的代数余子式乘积之和。
性质:
- 交换某两行/列,行列式的值 *= -1。
- 一行(列)加上另一行(列),行列式的值不变。
- 一行/列乘上 ,行列式的值 *= k。
1.11 二项式定理
这玩意当 的时候只有 的时候等于 ,和容斥原理有关。
当 的时候,。组合数行求和就是这个东西。
就是这样:
列求和怎么办?考虑如下图,就知道 。
1.12 吸收恒等式
的话,
也就是,一项可以推导它的邻项。(也可以是上下左右的邻项)
1.13 错排数
的排列,满足 的有几个?记它为 。考虑最后一个数为 。如果 ,那么有 种情况。如果 ,考虑 一个环的终点,有 种情况。因此 。
1.14 多项式定理
表示 个球, 个红球, 个蓝球……有多少种方案。
这个东西显然等于
等于
等于
等于
1.15 范德蒙德卷积
属于是需要找出来,但是证明很简单。
主要是变换组合数下标的 trick。
复杂一点的应用:
吸收恒等式也很有用。
ABC276G
【题意】
给定 ,求出满足以下条件的数列的个数:
- 数列长度为 。
- 数列的每一个数都在 之间。
- 数列的相邻两个数模 不同余。
【思想】
组合计数问题,需要利用一些一一映射,将要算的东西改写成能算的东西,比如 (选择,子集,轮换)。
【分析】
“模 不同余”这个条件首先一看就不是很好直接算,我们考虑构造差分数组 ,满足对于 有 。
然后显然考虑拆分成 和 。记 ,则有对于 ,;对于 ,。
我们先考虑整块,也就是钦定了一套 之后, 的个数怎么算?也就是计算使得 的方案数。可以发现这个东西只和 有关,那么如果计算上述式子的时间为 ,那么我们可以枚举 存在多少个 ,从而得到存在多少个 。相同的方案数可以二项式求出,那么可以 。
那么问题转化为:计算使得 的方案数。我们知道 的方案数是插板法算的,那么一种方法是预处理出所有 的答案,那么每次询问可以 得到解。总时间复杂度 ,其中 。
还有一种方法:
引理: 都是自然数,使得 的方案数为 。
证明:考虑插板法的过程。对于 的方案数,也就相当于 个球,插 个板,分成 个区域。
那么我们可以增加一个区域,这个区域表示把这些球扔掉,其他 个区域表示分成的 个区域。那么也就是把 个球分成 个区域。因此是一共 个球,插 个板,分成 个区域,答案为 。
2022.11.10 NOIP Monisai Round #1 A
【题意】
有 个景点和 个游客。这些景点横向一字排开。每个乘客都会选择一个景点到达并选择向左或者右行走。每一个景点都有一个小礼物。若一个人走到一个景点,满足这个人没有收到过礼物并且这个景点的小礼物没有被送出,那么这个景点发一份礼物给这个人。求每个人都拿到一个礼物的方案数。
【思想】
巧妙地增补方案构造一一对应,利用美妙的对称性寻找性质。
人为增设对称性!
【分析】
考虑增加一个景点,使得这些景点组成一个长度为 的环。从环上任意一点可以向左或向右走。考虑这个模型和原题的差异:
- 若 景点的礼物被送出去,那么就失败。
- 这个模型中,可以从 景点出发。
- 这个模型中,增加了一些横跨过 的路线。
这三个差异其实是统一的。也就是当 景点的礼物没有被送出去时,才是成功的,并且方案与题目中一一对应。考虑选出这样的方案的概率:
由于每个点都对称,所以概率为 。
所有方案的个数为 。
答案为所有方案个数乘以概率。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int mod = 998244353;
const int inf = 1e9;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
int qpow(int x,int k){
int ans=1;
while(k){
if(k&1)ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
time_t start = clock();
freopen("breeze.in","r",stdin);
freopen("breeze.out","w",stdout);
//think twice,code once.
//think once,debug forever.
int n, m; cin >> n >> m;
int ans = 1;
f(i, 1, m) {
ans = (ans * (2 * (n+1) % mod)) % mod;
}
ans = (ans * (n + 1 - m)) % mod;
// cout << ans << endl;
ans = (ans * qpow(n + 1, mod - 2)) % mod;
cout << ans << endl;
time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
CF1761D
【题意】
求 的个数,其中 ,并且 在二进制下加法的进位个数为 。
【分析】
考虑上一位如果进位了,那么这一位有三种选法也进位,一种选法不会进位。没进位的话,三种选法不进位,一种选法会进位。
然后 DP?不好意思,不太好转移。矩阵快速幂试过了不行。
这么优美的式子怎么不想想组合方法。我们从这个方向往下走。
考虑 表示该位有没有进位(假设二进制下分别为第 位),特别地 ,那么这总共 位数,如果存在 个连续段,那么方案数就是 。(连续段的开头只有一种选择方法,后面每一位都有三种)
于是变成了数 个 , 个 ,组成 个连续段,一共有几种情况?(数连续段模型)考虑第一段一定为 ,那么 和 的段数是确定的。然后考虑 个 分成 段的个数(插板法)乘以 个 分成 段的个数即可。
注意 个数分成 段的方案数是 。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
int jc[1000010], ny[1000010], pow3[1000010];
const int mod = 1e9 + 7;
int qpow(int x,int k){
int ans=1;
while(k){
if(k&1)ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
int c(int n, int m) {
if(n == -1 && m == -1) return 1;
else if(m > n) return 0;
else if(m < 0 || n < 0) return 0;
else return jc[n] * ny[m] % mod * ny[n - m] % mod;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
time_t start = clock();
//think twice,code once.
//think once,debug forever.
jc[0]=ny[0]=pow3[0]=1;
int n, k; cin >> n >> k;
f(i,1,n){
jc[i]=jc[i-1]*i%mod;
ny[i]=qpow(jc[i],mod-2);
pow3[i]=pow3[i-1]*3%mod;
}
int ans = 0;
f(i,1 ,n+1 ) {
int t = 1;
//分的段数
int p = i /2 ,q = i - p;
t *= c(n + 1 - k-1,q-1);
t *= c(k - 1, p - 1);
t %= mod;
t *= pow3[n + 1 - i];
t %= mod;
ans += t;
ans %= mod;
}
cout << ans << endl;
time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
2 有没有标号?
做题时,一定要思考清楚我这个求的是有没有标号的。怎么考虑?例如有向图, 条边,一共有 种生成图。这样就算同构的两张图,如果节点不一样也会被算两次。这就是有标号的。我们从 个点的图中,选 个点,有多少生成图?,这还是有标号的。选出 个点,然后再赋值 ,发现这种情况下任意两张有标号下不同的图都是算上了的。 中,选 和 这两种都算上了,也是有标号的。所以做题的时候要分清楚,不要搞糊涂了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析