组合数学与计数原理

组合数就是容斥和计数原理,你学的所有技巧,都是为了能够使用它们。要么找到一个好的划分,要么找到两个独立的对象。

要么构建双射!(x

1 常见公式与定理

1.1 排列数

\(P_n^m = \cfrac{n!}{(n-m)!}\)

1.2 组合数

\(\dbinom{n}{m} = \cfrac{n!}{(n-m)!m!}\)

1.3 抽屉原理

\(n\) 个球放在 \(m\) 个箱子中,每个箱子至少有 \(\lceil\cfrac{n}{m}\rceil\) 个球。

1.4 容斥原理

若有集合 \(S = s_1 \bigcup s_2 \bigcup ... \bigcup s_n\),那么 \(|S| = \sum \limits_{T \subseteq S} (-1)^{|T|-1} |\bigcap\limits_{x \in T} x| (T \neq \varnothing)\)

1.5 min-max 容斥

对于 \(x\)\(y\) 我们有 \(E(x) + E(y) = E(x + y)\)
但是我们没有 \(\max\{E(x), E(y)\} = E(\max(x,y))\)\(min\) 同理。那么我们需要计算后面这个东西怎么办呢?

考虑 \(\max(S)\) 表示 \(S\) 集合中的最大值,\(\min(S)\) 相反。那么有

\[\max(S) = \sum \limits_{T \subseteq S} (-1)^{|T|-1} \min(T) \\ \min(S) = \sum \limits_{T \subseteq S} (-1)^{|T|-1} \max(T) \\ (T \neq \varnothing) \]

为什么呢?考虑对于 \(\max(S)\),如果它等于 \(\min(T)\),那么 \(T = \{\max(s)\}\)
对于其他集合,证明它们都会被抵消。考虑 \(T = \{x, y_1, ...,y_k\}\),其中 \(x = \min(T)\)。那么考虑 \(y\)\(S\) 中第一个大于 \(x\) 的数。考虑 \(T'\),如果 \(y \in T\),那么 \(T' = T\) 排除 \(y\)。否则 \(T' = T \cup \{y\}\)。容易证明 \(T\)\(T'\) 一一对应,并且求和之后为 \(0\)
因此一共 \(2^n - 1\) 个真子集,减去一个 \(T = \{\max(s)\}\) 之外所有集合都可以两两配对,最后消掉。
因此结论成立。

如果用通用方法证明:考虑一个数被算了几次。如果有 \(k\) 个数比它大,那么只有其他数都是比它大的数的时候才会被计算,贡献为 \(\sum \limits_{i = 0}^{k} \dbinom{k}{i} \times -1^i = 0^k\)

如果知道其中一个,就可以 \(O(2^n)\) 知道另一个。

HDU4336

【题意】
\(n\) 种卡片,每开一个袋子,有 \(p_i\) 的概率开出第 \(i\) 种。保证 \(\sum p_i \le 1\)。求要集齐所有卡片至少一张的期望开卡次数。
\(n \le 20\)

【分析】
考虑 \(x_i\) 为收集到第一张 \(i\) 卡片的期望开卡次数。那么我们要求 \(E(\max\{x_i\})\)
而我们有 \(E(\min\{x_i\}) = \cfrac{1}{\sum p_i}\)。于是可以做了。

1.6 捆绑法/插空法

\(20\) 个人排队,\(A\)\(B\) 相邻。求方案数?

将他们两个人捆绑在一起视为一个人,答案为 \(19! \times 2!\)

\(20\) 个人排队,\(A\)\(B\) 不相邻。求方案数?

考虑其他 \(18\) 个人先排,然后 \(A\)\(B\) 分别插进空里。答案为 \(18! \times P_{19}^2\)

P3166

【题意】
给定一个 \(N×M\) 的网格,请计算三点都在格点上的三角形共有多少个。注意三角形的三点不能共线。

\(N,M\le 1000\)
【分析】
考虑容斥。横着和竖着共线是容易的。考虑斜着共线怎么算。

考虑枚举 \(i,j\),那么 \((0,0)\)\((i,j)\) 这两个点和 \(\gcd{(i,j)} - 1\) 个不同的点共线。注意为什么 \(-1\),因为不能是和 \((i,j)\) 共线。

时间复杂度 \(O(n^2)\)

\(O(n)\) 做法,需要用到莫比乌斯反演和欧拉反演。待补

https://www.luogu.com.cn/blog/emptyset/solution-p3166

1.7 卡特兰数

递推式:\(C_n = \sum \limits_{i=0}^{n-1} C_i C_{n-1-i}\)

什么意义?考虑 \(n\) 个节点形成的二叉树的形态数。一个根,左子树 \(0 \sim n-1\) 个节点,右子树 \(n-1-i\) 个节点。

还有什么意义?\(n\) 对括号形成合法括号序列的方案数。考虑第一个括号和哪一个括号匹配,这两个括号中间和后面分成两个区域。

还有什么意义?\(n\) 边形三角划分数,这一块还需要加 \(n-2\) 条边,加了一条边之后分成了两块。

考虑其通项公式:\(C_n = \dbinom{n}{2n} - \dbinom{n+1}{2n}\)

考虑从 \((0,0)\) 开始一步只能往右边或上面走,走到 \((n,n)\) 并且不能超过对角线的方案数,容易证明它等于卡特兰数。

考虑容斥。对于没有不能超过对角线的性质,方案数是 \(\dbinom{n}{2n}\)

对于超过对角线的方案,在第一次超过之后每一次都取反,最后一定走到 \((n-1,n+1)\)。因为这个点在对角线上方,容易证明这样的方案与原方案一一对应。方案数是 \(\dbinom{n+1}{2n}\)

也可以换一个形式,\(C_n = \cfrac{\dbinom{n}{2n}}{n+1}\)

1.8 卢卡斯定理

\(p\) 是素数时,

\(\dbinom{n}{m} \mod p = \dbinom{n/p}{m/p} \dbinom{n \bmod p}{m \bmod p} \mod p\)

可以用来求解 \(n,m \ge p\)\(\dbinom{n}{m}\)。时间复杂度 \(O(p + \log_p n)\)

\(p\) 不是素数时,有扩展卢卡斯定理。待补

https://oi-wiki.org/math/number-theory/lucas/

P4478

【题意】
小B 所在的城市的道路构成了一个方形网格,它的西南角为 \((0,0)\),东北角为 \((N,M)\)

小B 家住在西南角,学校在东北角。现在有 \(T\) 个路口进行施工,小B 不能通过这些路口。小B 喜欢走最短的路径到达目的地,因此他每天上学时都只会向东或北行走;而小B又喜欢走不同的路径,因此他问你按照他走最短路径的规则,他可以选择的不同的上学路线有多少条。由于答案可能很大,所以小B 只需要让你求出路径数 \(\bmod P\) 的值。
\(N,M\le 10^9, P = 1000003\) 或者 \(1019663265=3 \times 5 \times 6793 \times 10007, T \le 100\)
【分析】
先对障碍按 \((x,y)\) 排序,这样一定只会从前面的障碍跳到后面的障碍。
\(f_i\) 表示到第 \(i\) 个点且不经过任何障碍的方案数。
\(g_{i,j}\) 表示第 \(i\) 个点走到第 \(j\) 个点的方案数。这个是卡特兰数,需要卢卡斯定理算组合数。
然后有:

\[f_i = g_{0, i} - \sum g_{j, i} \times f_i \]

不会算重是因为“不经过任何障碍”。

1.9 矩阵优化计数

矩阵乘法是“行 × 列”原则,也就是答案矩阵的第 \(i\) 行第 \(j\) 列是由左矩阵的第 \(i\) 行乘以右矩阵的第 \(j\) 列得到。

对于 \(n\)\(k\) 层递推,时间复杂度为 \(O(n^3 \log k)\)

实际上可以用 FFT 优化到 \(O(n \log n \log k)\),但是现在不需要会(也差不太多)。

主要是代码实现问题需要注意。

P3216

【题意】

小 C 数学成绩优异,于是老师给小 C 留了一道非常难的数学作业题:

给定正整数 \(n,m\),要求计算 \(\text{Concatenate}(n) \bmod \ m\) 的值,其中 \(\text{Concatenate}(n)\) 是将 \(1 \sim n\) 所有正整数 顺序连接起来得到的数。

例如,\(n = 13\) , \(\text{Concatenate}(n) = 12345678910111213\)。小C 想了大半天终于意识到这是一道不可能手算出来的题目,于是他只好向你求助,希望你能编写一个程序帮他解决这个问题。
\(n \le 10^{18}, m \le 10^9\)

【分析】
考虑线性递推。
我们有:\(f_i = f_{i - 1} \times 10^k + i\),其中 \(k \in [1,18]\)
考虑怎么转化为矩阵递推式:

\[\begin{bmatrix}10^k&1&1\\0&1&1\\0&0&1\end{bmatrix} \begin{bmatrix}f_{i-1}\\i-1\\1\end{bmatrix} = \begin{bmatrix}f_{i}\\i\\1\end{bmatrix} \]

\(18\) 块处理,剩下的问题在于实现。

1.10 行列式

代数余子式:对于 \(1 \le i,j \le n\),在 \(n\) 阶行列式中所有不属于第 \(i\) 行也不属于第 \(j\) 列的元素按照原来的顺序组合成一个 \(n-1\) 阶余子式,它叫做 \(A_{i,j}\) 也就是原矩阵中元素 \(M_{i,j}\) 的代数余子式。
一个 \(n\) 阶行列式的值等于:

\[\sum \limits_{i/j=1}^n a_{i,j}(-1)^{i+j} A_{i,j} \]

其中 \(i,j\) 其中一个任选。也就是说,任意行(或者列)的元素与之对应的代数余子式乘积之和。

性质:

  • 交换某两行/列,行列式的值 *= -1。
  • 一行(列)加上另一行(列),行列式的值不变。
  • 一行/列乘上 \(k\),行列式的值 *= k。

1.11 二项式定理

\[(a + b)^n = \sum \limits_{i = 0}^n \dbinom{n}{i} a^ib^{n-i} \]

这玩意当 \(a=-1,b=1\) 的时候只有 \(n=0\) 的时候等于 \(1\),和容斥原理有关。

\(a=1,b=1\) 的时候,\(2^n = \sum \limits_{i = 0}^n \dbinom{n}{i}\)。组合数行求和就是这个东西。

就是这样:
image

列求和怎么办?考虑如下图,就知道 \(\sum \limits_{i = n}^m \dbinom{i}{n} = \dbinom{i+1}{n+1}\)
image

1.12 吸收恒等式

\(\dbinom{n}{m} \rightarrow \dbinom{n}{m+1}\)

\(\dbinom{n}{m+1} = \cfrac{n!}{(m+1)!(n-m+1)!}\)
\(\dbinom{n}{m} = \cfrac{n!}{m!(n-m)!}\)
\(\dbinom{n}{m} = k \dbinom{n+1}{m+1}\) 的话,\(k = \cfrac{n-m+1}{n-m}\)

也就是,一项可以推导它的邻项。(也可以是上下左右的邻项)

1.13 错排数

\(1 \sim n\) 的排列,满足 \(\forall i, p_i \neq i\) 的有几个?记它为 \(D_i\)。考虑最后一个数为 \(j\)。如果 \(D_j = i\),那么有 \(D_{i-2}\) 种情况。如果 \(D_j \neq i\),考虑 一个环的终点,有 \(D_{i-1}\) 种情况。因此 \(D_n = (n-1)D_{n-2} + (n-1)D_{n-1}\)
image

1.14 多项式定理

\[\dbinom{n}{k_1~k_2~...~k_m} \]

表示 \(n\) 个球,\(k_1\) 个红球,\(k_2\) 个蓝球……有多少种方案。

这个东西显然等于

\[\dbinom{n}{k_1}\dbinom{n-k_1}{k_2}... \]

等于

\[\cfrac{n!}{(n-k_1)!k_1!}\cfrac{(n-k1)!}{(n-k_1-k_2)!k_2!}...\cfrac{(n-k1-...-k_{m-1})!}{(n-k_1-k_2-...-k_m)!k_m!} \]

等于

\[\cfrac{n!}{(n-k_1-k_2-...-k_m)!k_1!k_2!...k_m!} \]

等于

\[\cfrac{n!}{k_1!k_2!...k_m!} \]

1.15 范德蒙德卷积

image

属于是需要找出来,但是证明很简单。

主要是变换组合数下标的 trick。

复杂一点的应用:

image

吸收恒等式也很有用。

ABC276G

【题意】
给定 \(n, m\),求出满足以下条件的数列的个数:

  • 数列长度为 \(n\)
  • 数列的每一个数都在 \([0,m]\) 之间。
  • 数列的相邻两个数模 \(3\) 不同余。

【思想】
组合计数问题,需要利用一些一一映射,将要算的东西改写成能算的东西,比如 \(\binom{n}{k}, \begin{Bmatrix}n\\k\end{Bmatrix},\begin{bmatrix}n\\k\end{bmatrix}\)(选择,子集,轮换)。

【分析】
“模 \(3\) 不同余”这个条件首先一看就不是很好直接算,我们考虑构造差分数组 \(b\),满足对于 \(i \in [2,n]\)\(3 \nmid b_i\)

然后显然考虑拆分成 \(b_i / 3\)\(b_i \% 3\)。记 \(x_i = b_i \% 3, y_i = b_i / 3\),则有对于 \(i = 1\)\(x_i \in \{0,1,2\}\);对于 \(i > 1\)\(x_i \in \{1,2\}\)

我们先考虑整块,也就是钦定了一套 \(x_i\) 之后,\(y_i\) 的个数怎么算?也就是计算使得 \(\sum \limits_{i = 1} ^ n y_i \le \lceil \cfrac{m - \sum x_i}{3} \rceil\) 的方案数。可以发现这个东西只和 \(\sum x_i\) 有关,那么如果计算上述式子的时间为 \(T\),那么我们可以枚举 \(x_i(i \le [2,n])\) 存在多少个 \(1\),从而得到存在多少个 \(2\)。相同的方案数可以二项式求出,那么可以 \(O(n) \times T\)

那么问题转化为:计算使得 \(\sum \limits_{i = 1} ^ n y_i \le t\) 的方案数。我们知道 \(\sum \limits_{i = 1} ^ n y_i = t\) 的方案数是插板法算的,那么一种方法是预处理出所有 \(t\) 的答案,那么每次询问可以 \(O(1)\) 得到解。总时间复杂度 \(O(n \log t_{\max} + n) = O(n \log t_{\max})\),其中 \(t_{\max} \sim m\)

还有一种方法:

引理:\(y_i\) 都是自然数,使得 \(\sum \limits_{i = 1}^n y_i \le t\) 的方案数为 \(\binom{n + t}{n}\)

证明:考虑插板法的过程。对于 \(\sum \limits_{i = 1}^n y_i = t\) 的方案数,也就相当于 \(t + n\) 个球,插 \(n - 1\) 个板,分成 \(n\) 个区域。

那么我们可以增加一个区域,这个区域表示把这些球扔掉,其他 \(n\) 个区域表示分成的 \(n\) 个区域。那么也就是把 \(\le t\) 个球分成 \(n\) 个区域。因此是一共 \(t + n + 1\) 个球,插 \(n\) 个板,分成 \(n + 1\) 个区域,答案为 \(\binom{n + t}{n}\)


2022.11.10 NOIP Monisai Round #1 A

【题意】
\(n\) 个景点和 \(m\) 个游客。这些景点横向一字排开。每个乘客都会选择一个景点到达并选择向左或者右行走。每一个景点都有一个小礼物。若一个人走到一个景点,满足这个人没有收到过礼物并且这个景点的小礼物没有被送出,那么这个景点发一份礼物给这个人。求每个人都拿到一个礼物的方案数。

【思想】
巧妙地增补方案构造一一对应,利用美妙的对称性寻找性质。
人为增设对称性!

【分析】
考虑增加一个景点,使得这些景点组成一个长度为 \(n + 1\) 的环。从环上任意一点可以向左或向右走。考虑这个模型和原题的差异:

  • \(n+1\) 景点的礼物被送出去,那么就失败。
  • 这个模型中,可以从 \(n+1\) 景点出发。
  • 这个模型中,增加了一些横跨过 \(n+1\) 的路线。

这三个差异其实是统一的。也就是当 \(n+1\) 景点的礼物没有被送出去时,才是成功的,并且方案与题目中一一对应。考虑选出这样的方案的概率

由于每个点都对称,所以概率为 \(\cfrac{\C_{m}^n}{\C_{m}^{n+1}} = \cfrac{n+1}{n-m+1}\)

所有方案的个数为 \((2(n+1))^m\)

答案为所有方案个数乘以概率。

#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

【题意】
\((a,b)\) 的个数,其中 \(0 \le a,b < 2^n\),并且 \(a + b\) 在二进制下加法的进位个数为 \(k\)
\(k < n \le 10^6\)

【分析】
考虑上一位如果进位了,那么这一位有三种选法也进位,一种选法不会进位。没进位的话,三种选法不进位,一种选法会进位。

然后 DP?不好意思,不太好转移。矩阵快速幂试过了不行。

这么优美的式子怎么不想想组合方法。我们从这个方向往下走。

考虑 \(d_i\) 表示该位有没有进位(假设二进制下分别为第 \(1 \sim n\) 位),特别地 \(d_0 = 0\),那么这总共 \(n + 1\) 位数,如果存在 \(i\) 个连续段,那么方案数就是 \(3^{n - i}\)。(连续段的开头只有一种选择方法,后面每一位都有三种)

于是变成了\(n + 1 - k\)\(0\)\(k\)\(1\),组成 \(i\) 个连续段,一共有几种情况?(数连续段模型)考虑第一段一定为 \(0\),那么 \(0\)\(1\) 的段数是确定的。然后考虑 \(p\)\(0\) 分成 \(x\) 段的个数(插板法)乘以 \(q\)\(1\) 分成 \(y\) 段的个数即可。

注意 \(0\) 个数分成 \(0\) 段的方案数是 \(1\)

#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 有没有标号?

做题时,一定要思考清楚我这个求的是有没有标号的。怎么考虑?例如有向图,\(\cfrac{n(n-1)}{2}\) 条边,一共有 \(2^{\frac{n(n-1)}{2}}\) 种生成图。这样就算同构的两张图,如果节点不一样也会被算两次。这就是有标号的。我们从 \(m\) 个点的图中,选 \(n\) 个点,有多少生成图?\(\dbinom{m}{n} \times 2^{\frac{n(n-1)}{2}}\),这还是有标号的。选出 \(n\) 个点,然后再赋值 \(1 \sim n\),发现这种情况下任意两张有标号下不同的图都是算上了的。\(\dbinom{m}{n}\) 中,选 \((1,2)\)\((1,3)\) 这两种都算上了,也是有标号的。所以做题的时候要分清楚,不要搞糊涂了。

posted @ 2022-11-12 16:53  OIer某罗  阅读(98)  评论(0编辑  收藏  举报