浅析排列组合、斯特林数、贝尔数、二项式定理与推论及其反演、子集反演、广义容斥
浅析排列组合、斯特林数、贝尔数、二项式定理与推论及其反演、子集反演、广义容斥
更好的阅读体验戳此进入
写在前面
内容较多,不保证正确性,不保证证明一定严谨,欢迎纠错。
题目编号可能并未顺序编号,仅按照做题顺序编号。
排列数
定义显然,从 $ n $ 个里选择 $ m $ 个的不同排列。
有:
证明:感性证明即可,第一个位置可以选择 $ n $,后面每一个位置都会少一个元素,即 $ n \times (n - 1) \times (n - 2) \times \cdots \times (n - m + 1) $。
组合数
定义显然,从 $ n $ 个里选择 $ m $ 个的本质不同方案数。
有:
证明从意义上想即可,对于排列数 $ A_n^m $,其中的重复个数即为在 $ m $ 里选择 $ m $ 个的不同排列,即 $ \dfrac{A_nm}{A_mm} $。
8种球盒问题(插板法)
这是一类十分经典的问题。首先为什么一共有八种呢,即我们考虑,球是否全部相同或不同(部分相同就是类似多重集相关问题了,后文有写),盒子之间是否完全相同,盒子是否允许空。不难发现共 $ 2^3 = 8 $ 种。
同时我们规定球的数量为 $ n $,盒的数量为 $ m $。
球完全相同 盒子完全不同 不允许空盒
最经典的问题,我们考虑在 $ n $ 个球的 $ n - 1 $ 个间隔种插入 $ m - 1 $ 个板,显然会使球被分成 $ m $ 段,其中第 $ i $ 段即为第 $ i $ 个盒子里的球,答案为:
球完全相同 盒子完全不同 允许空盒
考虑额外添加 $ m $ 个虚球,且球之间是完全相同的。则问题转换为前者,被划分为一个球的则包含了被划分为虚球即为空的情况:
球完全不同 盒子完全不同 允许空盒
每个球都可能被放进 $ m $ 个盒子里,则结论显然:
球完全不同 盒子完全相同 不允许空盒
第二类斯特林数,类似集合划分,但是限制必须划分为 $ m $ 段,即不能为空集。
考虑令 $ dp(i, j) $ 表示 $ i $ 个球放入 $ j $ 个盒子的方案数。
有转移:
即考虑第 $ i $ 个球如果放入前 $ j $ 个盒子,那么有 $ j $ 个方案,或自己独立放进一个新的盒子。
其为第二类斯特林数,记作 $ S(n, m) $ 或 $ n \brace m $。
则答案为:
球完全不同 盒子完全相同 允许空盒
答案较为显然,类似前者,为:
意义上理解一下即可,这个东西是贝尔数。其对应的是可以划分为空集的集合划分,或者说划分为 $ \le m $ 段。
球完全不同 盒子完全不同 不允许空盒
在斯特林数的基础上乘上盒子之间的排列即可。
球完全相同 盒子完全相同 允许空盒
令 $ dp(i, j) $ 表示 $ i $ 个相同小球放入 $ j $ 个可空的箱子中的方案数。
转移显然:
即考虑若我们盒子的数量多于球的数量,那么球的放置方式都是本质相同的,直接转移即可。反之则考虑第 $ j $ 个盒子空着,或考虑将 $ j $ 个盒子各放一个放满,则剩下的 $ i - j $ 可以任意放到 $ j $ 个里面。
则答案为:
值得注意的是,该状态也可以认为是将整数 $ n $ 可空地划分为 $ m $ 部分的方案数。
球完全相同 盒子完全相同 不允许空盒
类比于前者,答案为:
即考虑先在每个盒子里放 $ m $ 个球,即可保证不空。
斯特林数
斯特林数(Stirling Number)共有两类,称作第一类斯特林数和第二类斯特林数。
第二类斯特林数
我们先从第二类开始,因其会更常见常用很多。
第二类斯特林数,也叫斯特林子集数,可以记作 $ n \brace m $ 或 $ S(n, m) $,其意义在球盒问题中提到过。
边界为 $ S(n, 0) = [n = 0] $。
一般的递推式上文提到过,即:
当然这样的复杂度我们肯定是不能接受的,于是考虑通向式:
考虑证明:
令 $ f(i) $ 表示 $ n $ 个元素可空地放入 $ i $ 个集合的方案数,$ g(i) $ 表示对应非空的地放入的方案数。
显然有:
由二项式定理有:
当然此时我们不难想到,这并不是第二类斯特林数,因为第二类斯特林数要求集合之间是完全相同的,即不区分顺序,所以我们需要在 $ g(i) $ 中去掉 $ i $ 个集合的排列,则最终:
$ \texttt{QED} $。
同行同列等如何求以及第一类斯特林数及其求法等之后补完多项式和生成函数再回来补。
多重集的排列组合数
多重集的排列
全排列较为简单,考虑 $ n_1, n_2, \cdots, n_k $,且 $ \sum_{i = 1}^k n_i = n $,记作:
则显然多重集全排列数为从总排列数去除每个元素本身造成的重复排列的贡献,用乘法原理合起来即可,最终答案为:
而若是多重集的 $ m $ 排列,如何统计并没有什么思路,可能需要用到多项式?
多重集的组合
考虑若选择的 $ m $ 不大于任意一个集合的大小,那么我们便可以不考虑个数限制,则令第 $ i $ 个选了 $ x_i $ 个,我们要对每个 $ x_i $ 赋值并使得 $ \sum x_i = m $。这就变成了插板法,即将 $ m $ 可空地放入 $ k $ 个盒子里,方案数即为:
而若其大于了,那么我们就需要考虑到 $ n_i $ 的限制了,于是我们可以考虑通过容斥在所有方案中去掉 $ x_i \gt n_i $ 的方案。
若我们令不满足第 $ i $ 个限制的方案的集合为 $ F_i $,那么答案即为:
对于后者,不难想到对于 $ F_i $,我们考虑固定选 $ n_i + 1 $ 个而使得方案一定不合法,然后在剩下的中插板,最终有:
然后取并集的过程不难想到就是一个显然的容斥:
我们可以尝试将答案记录的更优美显然一些,即:
其中 $ A $ 的限制为枚举子集,需满足 $ \lvert A \rvert = p, A_i \lt A_{i + 1} $。意义上即为从限制中任选 $ k $ 个且升序排列。
圆排列
这个没什么可说的,就是去掉普通排列中的 $ n $ 种循环同构的,即:
亿些公式
(1)
从意义上或从公式上易证。
(2)
从公式上易证。
(3)
即杨辉三角。从组合意义上尝试证明:即从 $ n $ 个里选 $ m $ 个的方案数即为,从 $ n - 1 $ 个里选 $ m $ 个而不选第 $ n $,或从 $ n - 1 $ 选 $ m - 1 $ 并选 $ n $,类似于 DP。
(4)
考虑二项式定理:$ (a + b)^n = \sum_{i = 0}^n {n \choose i} a^{n - i}b^i $,令 $ a = 1, b = 1 $,易证。
(5)
考虑二项式定理中令 $ a = 1, b = -1 $,易证。特殊情况为 $ n = 0 $ 时原式显然为 $ 1 $。
(6)
从意义上即可证明,从 $ m + n $ 里选 $ m $ 个可以转换为枚举从 $ n $ 里选了多少用乘法原理和 $ m $ 里选多少组合即可。
(7)
令 (6) 中的 $ n = m $ 则易证。
(8)
证明待补。
(9)
证明待补。
(10)
证明待补。
(11)
考虑从组合意义上证明:从 $ n $ 个里选择 $ r $ 然后再在 $ r $ 选择 $ k $,最终选择了 $ k $,于是考虑线从 $ n $ 里选择 $ k $,再从剩下的 $ n - k $ 中选择 $ r $ 与 $ k $ 之间差异的 $ r - k $。
(12)
其中 $ F_n $ 为斐波那契数列。
证明待补。
反演
关于反演大概就是我们已知和式 $ f = \sum g $,但是 $ g $ 难以求出,而 $ f $ 容易得出,故通过一些推导得出新的 $ g = \sum f $ 以求解。
二项式定理
二项式较为显然,即形如 $ (a + b)^n $ 的式子。
二项式定理即考虑组合意义将上式展开:
证明:
考虑归纳法,对于 $ n = 1 $,显然成立。
对于 $ n \neq 1 $,若满足上式,则对于 $ n $ 与 $ n - 1 $,有:
我们考虑对下式乘上 $ a + b $,则下式的 $ \texttt{LHS} = (a + b)^n $,对下式的 $ \texttt{RHS} $ 进行推导:
于是此时下式 $ \texttt{RHS} $ 与上式 $ \texttt{RHS} $ 相同,$ \texttt{QED} $。
多项式定理
类比一下不难得到多项式定理:
其中:
不难发现这东西本质就是个多重集的排列。
广义二项式定理
二项式定理可推广到对于任意实数 $ \alpha $,满足:
这里简单介绍一下下降幂,即:
实数意义上的下降幂(我也不知道这玩意是不是存在)定义应该差不多,就是每次 $ -1 $ 然后乘 $ k $ 个。
然后对于实数的组合数,意义与一般的组合数相同,即:
这个东西的证明十分高妙,显然我不会,然后一种用处可以是去求指数函数值,在生成函数等里面还有很多重要用处,这里不再过多赘述。
二项式反演
首先写一下结论:
令:
那么:
也就是我们将 $ g \rightarrow f $ 转换成了 $ f \rightarrow g $。
证明:
首先考虑感性证明:对于二项式反演,我们一般认为 $ f(i) $ 代表 “钦定”,$ g(i) $ 代表 “恰好”。举个例子,如果我们要在 $ n $ 个球里选择至多 $ i $ 个,这个可以认为是钦定 $ i $ 个,剩下的随意。而若我们要求选且仅选 $ i $ 个,那么就是恰好。
所以说对于前者,即至多选 $ n $ 个,那么方案数就是对于任意一个恰好选 $ i $ 个,我们要从 $ n $ 里任选 $ i $ 作为恰好选定的,即组合数。
对于后者,我们可以认为对于恰好选 $ n $ 个,应该为至多选 $ n $ 个的方案数减去至多选 $ n - 1 $ 个加上至多选 $ n - 2 $ 个,以此类推,发现这东西是个容斥。
所以从意义上来讲这个一定是正确的。
考虑严谨证明:
不难发现可以将下式 $ f(i) $ 用上式带入而推导:
令 $ k = i - j $,则 $ i = k + j $,继续转换:
套一下公式即可得到最终结果:
$ \texttt{QED} $。
子集反演
这个东西实际上应该写在二项式反演之前,因为其比二项式反演更简单一些。
证明易证,考虑一般容斥原理即可。
例题 #1
求将 $ n $ 个信件装进 $ n $ 个信封,每个信件都没放进对应信封的方案数。
一般的 线性DP 显然,即:$ dp(i) = (i - 1) \times (dp(i - 1) + dp(i - 2)) $。
然后这种简单递推式是可以通过特征方程等转成通向式的,不过这里我们考虑一些二项式反演的方法。
考虑令 $ f(i) $ 为钦定 $ i $ 个信件放进了对应的信封,那么显然 $ f(i) = {n \choose i} (n - i)! = \dfrac{n!}{i!} $。
然后再令 $ g(i) $ 为恰好 $ i $ 个,有 $ f(i) = \sum_{j = i}^n {j \choose i} g(i) $,二项式反演得:
$ g(i) = \sum_{j = i}^n (-1)^{j - i} {j \choose i} f(j) $。
答案为 $ g(0) $。
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
template < typename T = int >
inline T read(void);
int N;
ll fact(int i){ll ret(1); for(int j = 1; j <= i; ++j)ret *= j; return ret;}
ll g(int i){return fact(N) / fact(i);}
ll ans(0);
int main(){
N = read();
for(int i = 0, mul(1); i <= N; ++i, mul *= -1)ans += g(i) * mul;
printf("%lld\n", ans);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
例题 #2
一个有 $ n $ 个元素的集合共有 $ 2^n $ 个包括空集的不同子集,现要在这其中取出不少于一个的若干个集合,使得它们的交集元素个数为 $ k $,求方案数。
考虑令 $ f(i) $ 表示钦定交集元素个数为 $ i $ 的方案数,有 $ f(i) = {n \choose i} (2{2{n - i}} - 1) $,也就是先枚举固定哪几个,然后再枚举剩下元素的所有方案。特别地,需要去掉取出 $ 0 $ 个的方案。
令 $ g(i) $ 为恰好 $ i $ 的方案数,有 $ f(i) = \sum_{j = i}^n {j \choose i} g(i) \(,二项式反演后有:\) g(i) = \sum_{j = i}^n (-1)^{j - i} {j \choose i}f(j) $。
但是需要注意,直接这样算显然无法计算,于是考虑扩展欧拉定理,即若 $ a \bot p $,则满足:
本题模数为 $ 1e9 + 7 $,那么 $ \varphi(p) = 1e9 + 6 $,所以我们直接算 $ 2{2\bmod{(1e9 + 6)}} \bmod{(1e9 + 7)} $ 即可。
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
#define MOD (ll)(1e9 + 7)
template < typename T = int >
inline T read(void);
ll qpow(ll a, ll b, ll mod = MOD){
ll ret(1), mul(a);
while(b){
if(b & 1)ret = ret * mul % mod;
b >>= 1;
mul = mul * mul % mod;
}return ret;
}
int N, K;
ll ans(0);
ll fact[1100000], inv[1100000];
void Init(void){fact[0] = 1;
for(int i = 1; i <= 1010000; ++i)fact[i] = fact[i - 1] * i % MOD;
inv[1010000] = qpow(fact[1010000], MOD - 2);
for(int i = 1009999; i >= 0; --i)inv[i] = inv[i + 1] * (i + 1) % MOD;
}
ll C(int n, int m){if(n < m || n < 0 || m < 0)return 0; return fact[n] * inv[m] % MOD * inv[n - m] % MOD;}
ll F(ll i){return (qpow(2, qpow(2, N - i, MOD - 1)) - 1 + MOD) % MOD * C(N, i) % MOD;}
int main(){
Init();
N = read(), K = read();
for(int i = K, mul(1); i <= N; ++i, mul *= -1)(ans += mul * C(i, K) * F(i) % MOD) %= MOD;
printf("%lld\n", (ans + MOD) % MOD);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
例题 #3
有 $ n $ 个不同盒子和 $ m $ 种球,每种球有 $ c_i $ 个,将球不可空地放进盒子里,求方案数。
不难想到令 $ f(x) $ 表示钦定 $ x $ 个盒子为空的方案数,那么不难想到对于每个 $ c_i $ 问题转化为可空地放入 $ n - x $ 个盒子,方案数即为 $ {n - x - 1 + c_i \choose n - x - 1} $。
总方案数即为乘起来,然后再乘一个枚举删除的 $ {n \choose x} $。
令 $ g(x) $ 为恰好 $ x $ 个为空,有:$ f(i) = \sum_{j = i}^n {i \choose j} g(j) $,于是二项式反演有:
$ g(i) = \sum_{j = i}^n (-1)^{j - i} {i \choose j} f(j) $。
答案即为 $ g(0) $。
复杂度是 $ O(nm) $ 的。
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
#define MOD (ll)(1e9 + 7)
template < typename T = int >
inline T read(void);
ll N, M;
ll fact[2100], inv[2100];
ll c[1100];
ll C(int n, int m){if(n < m || n < 0 || m < 0)return 0; return fact[n] * inv[m] % MOD * inv[n - m] % MOD;}
ll F(int x){
ll ret(1);
for(int i = 1; i <= M; ++i)(ret *= C(N - x - 1 + c[i], N - x - 1)) %= MOD;
ll mul = C(N, x);
return ret * mul % MOD;
}
ll qpow(ll a, ll b){
ll ret(1), mul(a);
while(b){
if(b & 1)ret = ret * mul % MOD;
b >>= 1;
mul = mul * mul % MOD;
}return ret;
}
void Init(void){fact[0] = 1;
for(int i = 1; i <= 2010; ++i)fact[i] = fact[i - 1] * i % MOD;
inv[2010] = qpow(fact[2010], MOD - 2);
for(int i = 2009; i >= 0; --i)inv[i] = inv[i + 1] * (i + 1) % MOD;
}
int main(){
Init();
N = read(), M = read();
for(int i = 1; i <= M; ++i)c[i] = read();
ll ans(0);
for(int i = 0, mul(1); i <= N - 1; ++i, mul *= -1)(ans += mul * F(i)) %= MOD;
printf("%lld\n", (ans + MOD) % MOD);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
例题 #4
存在 $ n $ 对情侣,坐在 $ n $ 对座位上,若一对情侣坐在了一对座位上则称其为和睦的,求恰好有 $ k $ 对和睦的情侣的方案数。
看到恰好就不难想到二项式反演了。
令 $ f(k) $ 表示钦定 $ k $ 对情侣和睦的方案数,则有 $ f(k) = 2^k \times {n \choose k}^2 \times k! \times (2n - 2k)! $。
从意义上即考虑枚举是哪 $ k $ 对座位和哪 $ k $ 对情侣,乘上枚举 $ k $ 对情侣坐在 $ k $ 对座位的顺序,然后还要乘上枚举每一对坐在座位上的顺序,最后再将剩下的 $ 2n - 2k $ 个人任意排列即可。
同时还存在 $ f(i) = \sum_{j = i}^n {j \choose i} g(j) $。
于是令 $ g(i) $ 表示恰好 $ k $ 对,二项式反演有:$ g(i) = \sum_{j = i}^n (-1)^{j - i}{j \choose i} f(j) $。
然后需要注意求的是 $ k \in [0, n] $,于是还需要优化,尝试展开 $ g(i) $。
大致的思路就是将求和转换一下变成类似前缀和,然后将与 $ j $ 无关的提出来,就可以发现后面项中与 $ j $ 部分相关的仅剩 $ n - i $ 了,而 $ n - i \le n $,所以我们对于每个 $ n - i $ 对应的所有 $ j $ 做一个前缀和,这样就是 $ O(n^2) $ 预处理并 $ O(1) $ 查询。
令 $ d = n - i $,则仅考虑和式,可将和式转化为:
显然可以对每个 $ d $ 进行预处理。
预处理后复杂度为:$ O(n^2 + Tn) $。
注意答案可能存在复数,需要对答案再次转正并取模。
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
#define MOD (998244353ll)
template < typename T = int >
inline T read(void);
int N;
ll ans[1100];
ll fact[2100], inv[2100], pow2[2100];
// ll C(int n, int m){if(n < m || n < 0 || m < 0)return 0; return fact[n] * inv[m] % MOD * inv[n - m] % MOD;}
// ll F(int k){
// return pow2[k] * C(N, k) % MOD * C(N, k) % MOD * fact[2 * N - 2 * k] % MOD;
// }
ll qpow(ll a, ll b){
ll ret(1), mul(a);
while(b){
if(b & 1)ret = ret * mul % MOD;
b >>= 1;
mul = mul * mul % MOD;
}return ret;
}
void Init(void){fact[0] = pow2[0] = 1;
for(int i = 1; i <= 2010; ++i)fact[i] = fact[i - 1] * i % MOD;
inv[2010] = qpow(fact[2010], MOD - 2);
for(int i = 2009; i >= 0; --i)inv[i] = inv[i + 1] * (i + 1) % MOD;
for(int i = 1; i <= 2010; ++i)pow2[i] = pow2[i - 1] * 2 % MOD;
}
int main(){
// freopen("out.txt", "w", stdout);
Init();
for(int d = 0; d <= 1010; ++d)
for(int j = 0, mul(1); j <= d; ++j, mul *= -1)
(ans[d] += mul * pow2[j] % MOD * fact[2 * (d - j)] % MOD * inv[j] % MOD * inv[d - j] % MOD * inv[d - j] % MOD) %= MOD;
int T = read();
while(T--){
int N = read();
for(int i = 0; i <= N; ++i){
int d = N - i;
printf("%lld\n", ((ans[d] * pow2[i] % MOD * fact[N] % MOD * fact[N] % MOD * inv[i] % MOD) + MOD) % MOD);
}
}
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
例题 #5
存在 $ n \times n $ 的国际象棋棋盘,你需要在其中放 $ n $ 个车(能上下左右四向移动任意)以满足所有空格子都可以被至少一个车攻击到,且恰好有 $ k $ 对车可以互相攻击。
这道题虽然有着 “恰好” 在,但是本质上其并非二项式反演,或者说二项式反演推一半就会发现已经有答案了。
首先存在性质,即要么每一行都有一个车,要么每一列都有一个车。以行为例,若存在一行没有车,那么这一行显然都空着,如果想要不在这一行放任何车并覆盖这一行就只能在非本行的每一列放一辆车,即又符合后者了,$ \mathrm{QED} $。
于是可以推导下一个性质,即 $ k \ge n $ 时答案为无解,这个不难想到互相攻击最多的合法情况仅能为同一行或同一列,此时因为车不能跨车攻击车,所以最多只能有 $ n - 1 $ 对。
此时我们可以考虑钦定每行都有一个车,然后考虑其所在列的位置,对于每一列都有车的情况显然与前者严格对称,故 $ \times 2 $ 即可。
此时不难发现,若共有 $ m $ 列,那么可以互相攻击的车的对数即为 $ n - m $。即 $ k = n - m $,那么我们便可以将对 $ k $ 的限制通过 $ m = n - k $ 转化为对列数的限制。
这里我们自然可以通过经典套路讨论钦定 $ i $ 列和恰好 $ i $ 列,但是没必要,我们不难发现此时问题即为不可空地将 $ n $ 个不同的球放入 $ n - k $ 个不同的盒子中,此即为经典球盒问题中的第二类斯特林数,则最终发现答案由如下部分组成:
枚举哪 $ n - k $ 个盒子,第二类斯特林数计算不可空地放入相同盒子,枚举不同盒子之间的排列:
当然特别地,对于 $ k = 0 $ 的,答案应直接为 $ n! $,因为此时每行不同与每列不同是等效的。
考虑用二项式反演优化求第二类斯特林数的式子即可,最终复杂度 $ O(n \log n) \(,\) \log $ 是快速幂的复杂度。
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
#define MOD (998244353ll)
template < typename T = int >
inline T read(void);
ll N, K;
ll fact[210000], inv[210000];
ll C(int n, int m){if(n < m || n < 0 || m < 0)return 0; return fact[n] * inv[m] % MOD * inv[n - m] % MOD;}
ll qpow(ll a, ll b){
ll ret(1), mul(a);
while(b){
if(b & 1)ret = ret * mul % MOD;
b >>= 1;
mul = mul * mul % MOD;
}return ret;
}
void Init(void){fact[0] = 1;
for(int i = 1; i <= 201000; ++i)fact[i] = fact[i - 1] * i % MOD;
inv[201000] = qpow(fact[201000], MOD - 2);
for(int i = 200999; i >= 0; --i)inv[i] = inv[i + 1] * (i + 1) % MOD;
}
ll Sn(int m){
ll ret(0);
for(int i = 0; i <= m; ++i)(ret += ((m - i) & 1 ? -1 : 1) * qpow(i, N) % MOD * inv[i] % MOD * inv[m - i] % MOD) %= MOD;
return (ret + MOD) % MOD;
}
int main(){
Init();
N = read(), K = read < ll >();
if(K >= N)printf("0\n"), exit(0);
if(!K)printf("%lld\n", fact[N]), exit(0);
printf("%lld\n", 2ll * C(N, N - K) % MOD * fact[N - K] % MOD * Sn(N - K) % MOD);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
例题 #6
给定两个序列 $ a_n $ 和 $ b_n $,保证没有相同的数,匹配 $ a_i $ 与 $ b_i $,使得 $ a_i \gt b_i $ 的对数比 $ a_i \lt b_i $ 的对数多 $ k $ 对。
显然我们需要先将序列 $ a_n $ 和 $ b_n $ 排序,显然这不会对方案数造成影响。
显然总对数为 $ n $,两者的差又为 $ k $,那么显然 $ a_i \gt b_i $ 的对数为 $ \dfrac{n + k}{2} $,同时显然若不能整除则无解。
令 $ cnt_i $ 表示比 $ a_i $ 小的 $ b_i $ 数量,于是考虑到一种 DP,即令 $ dp(i, j) $ 表示考虑匹配前 $ i $ 个 $ a $ 中钦定 $ j $ 对 $ a_i \gt b_i $ 的方案数。
对于新的一个数,显然其可以从 $ dp(i - 1, j) $ 转移而来,即令 $ i $ 匹配一个更大的,但是此时如何匹配讨论十分复杂,所以我们直接放弃这一部分,认为其为任意的,后续再讨论。而若其从 $ dp(i - 1, j - 1) $ 转移而来,那么其应能够匹配 $ cnt_i $ 中去掉 $ j - 1 $ 个后剩下的任意一个,所以转移为:
然后现在我们还需要讨论对于 $ dp(i, j) $ 剩下的 $ i - j $ 个如何分配,此时不难想到二项式反演。
令 $ f(i) = (n - i)! \times dp(n, i) $ 表示钦定 $ i $ 对 $ a_i \gt b_i $ 的方案数,令 $ g(i) $ 表示恰好的方案数,则又有 $ f(i) = \sum_{j = i}^n {j \choose i} g(j) $,则进行二项式反演,有 $ g(i) = \sum_{j = i}^n (-1)^{j - i} {j \choose i} f(j) $,答案即为 $ g(\dfrac{n + k}{2}) $。
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
#define MOD (ll)(1e9 + 9)
template < typename T = int >
inline T read(void);
int N, K;
int A[2100], B[2100];
int cnt[2100];
ll ans(0);
ll dp[2100][2100];
ll fact[2100], inv[2100];
ll C(int n, int m){if(n < m || n < 0 || m < 0)return 0; return fact[n] * inv[m] % MOD * inv[n - m] % MOD;}
ll F(int i){return fact[N - i] * dp[N][i] % MOD;}
ll qpow(ll a, ll b){
ll ret(1), mul(a);
while(b){
if(b & 1)ret = ret * mul % MOD;
b >>= 1;
mul = mul * mul % MOD;
}return ret;
}
void Init(void){fact[0] = 1;
for(int i = 1; i <= 2010; ++i)fact[i] = fact[i - 1] * i % MOD;
inv[2010] = qpow(fact[2010], MOD - 2);
for(int i = 2009; i >= 0; --i)inv[i] = inv[i + 1] * (i + 1) % MOD;
}
int main(){
Init();
N = read(), K = read();
if((N + K) & 1)printf("0\n"), exit(0);
for(int i = 1; i <= N; ++i)A[i] = read();
for(int i = 1; i <= N; ++i)B[i] = read();
sort(A + 1, A + N + 1), sort(B + 1, B + N + 1);
for(int a = 1, b = 0; a <= N; ++a){
while(b + 1 <= N && B[b + 1] < A[a])++b;
cnt[a] = b;
}dp[0][0] = 1;
for(int i = 1; i <= N; ++i)for(int j = 0; j <= i; ++j)
dp[i][j] = (dp[i - 1][j] + (j ? (cnt[i] - (j - 1)) * dp[i - 1][j - 1] % MOD : 0)) % MOD;
int lim = (N + K) >> 1;
for(int j = lim, mul(1); j <= N; ++j, mul *= -1)
(ans += mul * C(j, lim) % MOD * F(j) % MOD) %= MOD;
printf("%lld\n", (ans + MOD) % MOD);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
例题 #7
LG-P6478 [NOI Online #2 提高组] 游戏。
//TODO
例题 #8
CF1228E Another Filling the Grid。
//TODO
例题 #9
//TODo
例题 #10
//TODO
例题 #11
//TODO
广义容斥
//TODO
UPD
update-2023_01_13 初稿 添加排列组合数 8种球盒问题 第二类斯特林数简介 多重集的排列组合 圆排列 排列组合相关公式 二项式定理推论及反演 多项式定理 广义二项式定理
update-2023_01_16 添加子集反演 例题 #1 #2 #3 #4 #5 #6