组合数学:从入门到入坟
paulpao
·
·
算法·理论
前言
本文难度跨度较大,从绿题到黑题不等,既有对基本定义的介绍,也有对较综合习题的讲解,如果你对基础知识足够了解,可以直接跳到最后一章。
一. 排列与组合
1. 定义
从一个 n 元集合中选出 m 个数,按顺序排成一个有序序列,这样的方案数称为排列,记作 P_{n}^{m}。
序列的顺序性是关键,不同顺序视为不同排列。如 1,2,3 与 1,3,2 是不同排列。
排列的计算很简单,第一项有 n 种选法,第二项有 n-1 种,第三项有 n-2 种,依此类推,第 m 项有 n-m+1 种选法。
因此 P_{n}^{m}=n(n-1)(n-2)\cdots(n-m+1)=\frac{n!}{(n-m)!}。
从 n 个元素中选出 m 个元素构成一个子集,子集不区分顺序,这类方案数称为组合数,记作 C_{n}^{m} 或 \binom{n}{m},由于后者看起来比较高级,本文采用后者。
同一组元素的不同排列在组合中被视为同一种情况。如 1,2,3 与 1,3,2 是同种组合。
组合数可通过排列数去除重复计数得到:由于 m 个元素有 m! 种排列,在组合中均视为相同。
因此,\binom{n}{m}=\frac{P_{n}^{m}}{m!}=\frac{n!}{m!(n-m)!}。
2. 预处理
方法 1:帕斯卡恒等式(O(n^2)-O(1))。
首先有 \binom{n}{m}=\binom{n-1}{m}+\binom{n-1}{m-1}。
:::info[证明]
分类讨论,使用组合意义证明。
- 第 n 个元素如果被选择了,那么有 \binom{n-1}{m-1} 种情况,因为要在前 n-1 个元素里选 m-1 个。
- 第 n 个元素如果没被选择,那么有 \binom{n-1}{m} 种情况,因为要在前 n-1 个元素里选 m 个。
其次我们知道 \binom{n}{0}=1 \ (n \in \mathbb N),因为从 n 元集中选 0 个只有空集一种选法。
那么我们就可以利用这两个式子做 DP 了,时间复杂度预处理 O(n^2),查询 O(1)。
这种方法好实现,但效率低下,下面介绍一种更高效的方法。
方法 2:逆元预处理(O(n)-O(1))。
首先,前文已经提到了组合数公式,即 \binom{n}{m}=\frac{n!}{m!(n-m)!}。
如果范围较小,我们可以直接用这个公式计算出结果,都不用预处理。
但是这个公式增长起来是很快的,分子随便爆 long long。OI 不会搞狗屎高精度,所以题目里一般会有素数模数 P,让你对这个数取模,我们引入有理数取模方法。
我们认为 \frac{p}{q}\equiv a\pmod P 当且仅当 p\times q^{-1}\equiv a\pmod P,其中 q^{-1}\times q\equiv1\pmod P 且 q^{-1}\le P,一般 P 是素数,所以大部分情况下 q^{-1} 存在且唯一,称为 P 的逆元,在模意义下,除以一个数等于乘其逆元。
那么我们可以预处理出所有阶乘和阶乘逆元,就可以 O(1) 求组合数了。阶乘逆元直接递推即可,下面讨论怎么预处理阶乘逆元。
由费马小定理,1 \equiv a^{P-1}\pmod{P}\Rightarrow a^{-1} \equiv a^{P-2}\pmod{P}。我们可以使用快速幂 O(\log P) 求 n! 逆元 (n!)^{-1}。
我们利用递推关系反向求解:((n-1)!)^{-1} \equiv (n!)^{-1} \times n\pmod P,两边同乘 n! 易知该式成立。这样我们就可以在 O(n) 复杂度内预处理出所有阶乘逆元。
注意要处理 0!=1 以及 (0!)^{-1}=1。
参考代码:
#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>
#define int long long
using namespace std;
const int N=3e6+5;
const int mod=1e9+7;
int ksm(int a,int b){
int ans=1;
while(b){
if(b%2){
b--;
ans*=a;
ans%=mod;
}
b/=2;
a*=a;
a%=mod;
}
return ans;
}
int jc[N],jcn[N];
void ini(){
jc[0]=1;
jcn[0]=1;
for(int i=1;i<=N-2;i++){
jc[i]=jc[i-1]*i;
jc[i]%=mod;
jcn[i]=ksm(jc[i],mod-2);
}
}
int C(int a,int b){
if(b>a) return 0;
return jc[a]*jcn[b]%mod*jcn[a-b]%mod;
}
int A(int a,int b){// 这种方法同样适用于求排列数
if(b>a) return 0;
return jc[a]*jcn[a-b]%mod;
}
3. 例题
P3223 排队
这题原题要高精度,现代的 OI 基本不会考高精,甚至作者校内 OJ 直接加了个模数,因此参考代码对 10^9+7 取模了。
:::info[Hint1]
如果只有男生女生,没有老师,怎么搞?
:::
:::success[Hint1答案]
组合数学常用技巧:插空法。
先把男生排成一排,一共 n! 种排法。由于女生不能相邻,我们可以视作男生分隔开了女生。比如 xoxoxox,其中 o 代表男生,女生只能放在 x 的位置,共 n+1 个,所以总方案数是 A_{n+1}^{m}(注意这里顺序是需要考虑的),总方案数 n!\times A_{n+1}^{m}。 |
| :::info[Hint2] |
| 老师会隔开女生,女生也会隔开老师,怎么处理? |
| ::: |
| :::success[Hint2答案] |
| 分类讨论,老师的数量很少,老师隔开女生和女生隔开老师的情况数都很少,可以先讨论老师与女生之间的隔开情况,再把一个由老师和女生组成的连续部分看作一个“女生”,去使用插空法,这种“把很多人视作一个整体”的方法也被称作捆绑法。 |
| ::: |
| :::success[题解] |
先考虑女生分隔老师在一起构成的“女生”的可能合法情况数,x 是女生,o 是老师。 |
- x,单个女生显然可以,最多 m 个。 |
- o,单个老师同理,最多 2 个。 |
- oxo,女生分隔开了老师,最多 1 组。 |
显然男生直接排成一排,共 n! 种方法。下面我们讨论插空的方法,我们把老师也带上,分两类讨论。
- 老师插在不同的空,先把老师放好,共 A_{n+1}^{2},然后老师就和男生没区别了,相当于有 n+2 个男生,女生有 A_{n+3}^{m} 种排列方法。这类情况共 A_{n+3}^{m}\times A_{n+1}^{2} 种。
- 老师插在同一个空,中间一定要有一个女生,这个
oxo 单元组共 2m 种可能性,可以放在 n+1 个空里,然后这个单元组等价于一个男生,剩下的女生共 A_{n+2}^{m-1} 种排法。
| 综合一下,共 n!\times(A_{n+2}^{m-1}\times2m\times(n+1)+A_{n+3}^{m}\times A_{n+1}^{2}) 种情况。 |
:::success[code]
#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>
#define int long long
using namespace std;
const int N=3e6+5;
const int mod=1e9+7;
int ksm(int a,int b){
int ans=1;
while(b){
if(b%2){
b--;
ans*=a;
ans%=mod;
}
b/=2;
a*=a;
a%=mod;
}
return ans;
}
int jc[N],jcn[N];
void ini(){
jc[0]=1;
jcn[0]=1;
for(int i=1;i<=N-2;i++){
jc[i]=jc[i-1]*i;
jc[i]%=mod;
jcn[i]=ksm(jc[i],mod-2);
}
}
int C(int a,int b){
if(b>a) return 0;
return jc[a]*jcn[b]%mod*jcn[a-b]%mod;
}
int A(int a,int b){
if(b>a) return 0;
return jc[a]*jcn[a-b]%mod;
}
signed main(){
int n,m;
cin>>n>>m;
ini();
cout<<jc[n]*(C(n+1,1)*m%mod*2%mod*A(n+2,m-1)%mod+A(n+1,2)*A(n+3,m)%mod)%mod;
return 0;
}
:::
CF1420D Rescue Nibel!
:::info[形式化题意]
给定 n 个区间 [l_i,r_i],你需要找出 k 个区间,使其交集不为空。
:::
:::info[Hint1]
两个区间 [l_i,r_i] 与 [l_j,r_j] 交集不为空等价于 l_j\le r_i 或 l_i\le r_j,可以根据 l 排序消掉一维。
:::
:::info[Hint2]
固定选择的左端点最大的区间,找出所有与其有交集的区间。
:::
:::success[Solution]
先把区间根据左端点排序,现在两个区间有交等价于 r_j\ge l_i,我们考虑固定每个区间作为左端点最靠右的选择区间时的方案数,那就是在和他有交的区间中选 k-1 个,即方案数为 \binom{cnt}{k-1},cnt 为与其有交区间个数。
时间复杂度 $O(n\log n)$。
:::
:::success[Code]
```cpp
#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>
#include<algorithm>
#define int long long
using namespace std;
const int N=1e6+5;
const int mod=998244353;
int ksm(int a,int b){
int ans=1;
while(b){
if(b%2){
b--;
ans*=a;
ans%=mod;
}
b/=2;
a*=a;
a%=mod;
}
return ans;
}
int jc[N],jcn[N];
void ini(){
jc[0]=1;
jcn[0]=1;
for(int i=1;i<=N-2;i++){
jc[i]=jc[i-1]*i;
jc[i]%=mod;
jcn[i]=ksm(jc[i],mod-2);
}
}
int C(int a,int b){
if(b>a) return 0;
return jc[a]*jcn[b]%mod*jcn[a-b]%mod;
}
int A(int a,int b){
if(b>a) return 0;
return jc[a]*jcn[a-b]%mod;
}
int n,k;
struct ran{
int l,r;
}a[N];
bool cmp(ran r1,ran r2){
return r1.l<r2.l;
}
int tr[N];
int lowbit(int x){
return x&(-x);
}
void modify(int x,int k){
for(int i=x;i<=3*n;i+=lowbit(i)){
tr[i]+=k;
}
}
int query(int x){
int ans=0;
for(int i=x;i>=1;i-=lowbit(i)){
ans+=tr[i];
}
return ans;
}
int b[2*N];
signed main(){
ini();
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i].l>>a[i].r;
b[2*i-1]=a[i].l;
b[2*i]=a[i].r;
}
sort(b+1,b+2*n+1);
int m=unique(b+1,b+2*n+1)-b-1;
for(int i=1;i<=n;i++){
a[i].l=lower_bound(b+1,b+m+1,a[i].l)-b;
a[i].r=lower_bound(b+1,b+m+1,a[i].r)-b;
}
sort(a+1,a+n+1,cmp);
int ans=0;
for(int i=1;i<=n;i++){
ans+=C(query(a[i].l),k-1);
ans%=mod;
modify(a[i].l,1);
modify(a[i].r+1,-1);
}
cout<<ans;
return 0;
}
```
:::
在计数时,为了防止重算或漏算,我们一般先钦定一项,这样计数更好做,逻辑也更清晰,下例也是这种思想的一种体现。
[[ARC156B] Mex on Blackboard](https://www.luogu.com.cn/problem/AT_arc156_b)
:::info[Hint1]
我们不用考虑计数整个多重集,只需要考虑计数新生成的多重集。
:::
:::info[Hint2]
钦定新增多重集中的最大值 $x$。
:::
:::info[Hint3]
什么情况下 $x$ 能出现?
:::
:::success[Hint3答案]
$0,1,2,\dots,x-1$ 都在原多重集中出现。
:::
:::success[Solution]
钦定 $x$ 是新增的多重集中的最大值,那么由于 $\operatorname{mex}$ 的定义,$0,1,\dots,x-1$ 都必须在原多重集中出现,我们设在新多重集中,$i$ 的出现次数为 $cnt_i$,那么 $\sum_{i=0}^{x}cnt_i=k$,并且对于在原多重集中没出现的元素 $k$,$cnt_k\ge1$。
这个方程解的组数是个很经典的问题,我们令 $b_i=cnt_i+[i\in S]$,这样所有 $b_i\ge1$,然后我们求解 $\sum_{i=0}^{x}b_i=k+c$,其中 $c$ 为 $\sum_{i=0}^{x}[i\in S]$,即 $\le x$ 且在原多重集中出现的元素数,显然上方程每个解都与 $\sum_{i=0}^{x}cnt_i=k$ 的一个解一一对应。
这个方程的解的数量显然是 $\binom{k+c-1}{x}$,钦定每个数做最大值,同时同步计算 $c$,时间复杂度 $O(n)$。
::::info[如果你看不懂上面的显然]
这种方程组解的数量等价于把 $k+c$ 个苹果分成 $x+1$ 组(注意 $0$),那就相当于在 $k+c-1$ 个“苹果间的间隙”中插 $x$ 个隔板。
::::
:::
## 二. 卡特兰数
#### 1. 定义
卡特兰数是一个特殊的数列,第 $i$ 项记作 $C_i$。
卡特兰数的定义:$C_0=1$,$C_n=\sum_{i=0}^{n-1}C_iC_{n-1-i}$。
这时候可能有人要问了:这有什么用呢?参见下一节。
#### 2. 组合意义
卡特兰数拥有极多的组合意义,参见 [OI wiki](https://oi-wiki.org/math/combinatorics/catalan/)。
几个经典问题就是长度为 $n$ 的合法括号序列个数,长度为 $n$ 的入栈顺序对应的合法出栈顺序个数,不碰到 $y=x$ 的从 $(1,1)$ 走到 $(n,n)$ 的方案数,这些问题的答案都是 $C_n$。
神奇的是,上面几个问题是等价的,对于一个长度为 $n$ 的合法括号序列,我们把 `(` 视作一次入栈操作,`)` 视作一次出栈操作,即可一一对应一个合法出栈序列个数。我们又可以把路径计数问题中每次往右走视作写一个 `(`,向上走视作写一个 `)`,这样在任意时刻 `(` 数量多于 `)` 数量,括号序列合法。
#### 3. 计算
原始的卷积递推形式求解是 $O(n)$ 的,太慢了,我们有高贵的 $O(1)$ 通项:$C_n=\binom{2n}{n}-\binom{2n}{n+1}$。具体证明方法不是本文重点,感兴趣的读者可以看 OI wiki。
#### 4. 例题
[P3200 [HNOI2009] 有趣的数列](https://www.luogu.com.cn/problem/P3200)
这道题取模有点阴间,主要讨论计数过程,取模细节详见代码。
:::info[Hint1]
~~都放在这个板块了答案肯定是卡特兰数对吧。~~
考虑将本题对应到哪个卡特兰数经典问题?
:::
:::success[Hint1答案]
路径计数问题。
接着考虑怎么解释三个约束,把他往路径计数上靠。
:::
:::success[Solution]
首先从 $(0,0)$ 走到 $(n,n)$ 一共会走 $2n$ 步,那么我们给每一步分配一个走的“时间”,把 $a_{2i-1}$ 视为第 $i$ 次向右走是第几步,把 $a_{2i}$ 视作第 $i$ 次向上走是第几步,这样约束 $1,2$ 自动满足了,对于约束 $3$,由于不碰对角线,所以第 $i$ 次向上肯定在第 $i$ 次向右之后,那么约束 $3$ 也满足了。因此答案即为 $C_n$。
:::
:::success[Code]
```cpp
#include<iostream>
#define int long long
using namespace std;
int n,p;
int cnt=0;
const int N=1e7+5;
int pr[N];
int ksm(int a,int b){
int ans=1;
while(b){
if(b%2){
b--;
ans*=a;
ans%=p;
}
b/=2;
a*=a;
a%=p;
}
return ans;
}
bool flag[N];
int c[N],minp[N];
void sss(){
for(int i=2;i<=2*n;i++){
if(flag[i]==0){
minp[i]=cnt+1;
pr[++cnt]=i;
for(int j=i;j*i<=2*n;j++){
flag[j*i]=1;
if(minp[j*i]==0) minp[j*i]=cnt;
}
}
}
}
signed main(){
cin>>n>>p;
sss();
for(int i=1;i<=2*n;i++){
int tmp=i;
if(i<=n){
while(tmp!=1){
c[minp[tmp]]--;
tmp/=pr[minp[tmp]];
}
}
if(i>=n+2){
while(tmp!=1){
c[minp[tmp]]++;
tmp/=pr[minp[tmp]];
}
}
}
int ans=1;
for(int i=1;i<=cnt;i++){
ans*=ksm(pr[i],c[i]);
ans%=p;
}
cout<<ans;
return 0;
}
```
:::
## 三. 第二类斯特林数
#### 1. 定义
将一个 $n$ 元集合划分成 $m$ 个互不区分**非空**子集的方案数称为**第二类斯特林数**,记作 $n\brace m$。
比如 ${5\brace 1}=1$,因为只能全部分配在一个集合里,${3\brace 2}=3$,因为必定有一个集合只有一元,有三种可能性。
#### 2. 预处理
斯特林数有一个优美的递推。
$$
{n\brace m}={n-1\brace m-1}+m{n-1\brace m}
$$
::::info[证明]
考虑组合意义:
- 如果最后一个元素单开一个新的集合,由于集合互不区分,那么情况数是 $n-1\brace m-1$,把前面的 $n-1$ 个元素分配到剩下的 $m-1$ 个集合中即可。
- 如果最后一个元素加入已有的集合,这个元素可以加入 $m$ 个集合中的任意一个,因此有 $m{n-1\brace m}$ 种情况数。
注意:这里乘 $m$ 并不违反“集合不区分”的前提。
所谓“不区分”,是指我们不把集合排成顺序、不给它们编号。
但在一个具体的划分中,$m$ 个集合是通过它们所包含的元素来自然区分的——因为它们是互不相等的子集。他们被“后天的”区分了。
把第 $n$ 个元素放进不同的集合,就会形成不同的新集合,进而产生不同的新划分。所以 $m$ 种选择都合法,且互不重复。
::::
我们可以用这个递推式 DP 预处理出每个斯特林数,时间复杂度 $O(n^2)$,基本上已经够用了,~~听说可以 NTT 做到 $O(n\log n)$ 但我不会。~~
DP 边界条件:${n \brace 1}=1 \ (n \in \mathbb N^*)$。
参考实现:
```cpp
#include<iostream>
#define int long long
using namespace std;
const int N=105;
int S[N][N];
const int mod=998244353;
int n,q;
signed main(){
n=100;// 预处理上界
for(int i=1;i<=n;i++){
S[i][1]=1;
}
for(int i=2;i<=n;i++){
for(int j=1;j<=n;j++){
S[i][j]=S[i-1][j-1]+j*S[i-1][j]%mod;
S[i][j]%=mod;
}
}
int a,b;
while(cin>>a>>b){
cout<<S[a][b]<<endl;
}
return 0;
}
```
#### 3. 例题
[HDU2643 Rank](https://acm.hdu.edu.cn/showproblem.php?pid=2643)
:::info[Hint]
枚举排名数 $m$,等价于把 $n$ 个元素划分成 $m$ 个**互相区分**的集合。
:::
:::success[Solution]
几乎板题。
互不区分的划分方案数是 $n\brace m$,那么互相区分只要再乘上一个 $m!$ 钦定顺序即可。
答案为 $\sum_{i=1}^{n}{n\brace i}i!$。
:::
:::success[Code]
```cpp
#include<iostream>
#define int long long
using namespace std;
const int N=1e3+5;
const int mod=20090126;
int C[N][N];
int jc[N];
int T;
signed main(){
C[1][1]=1;
for(int i=2;i<=1000;i++){
for(int j=1;j<=i;j++){
C[i][j]=C[i-1][j]*j+C[i-1][j-1];
C[i][j]%=mod;
}
}
jc[1]=1;
for(int i=2;i<=1000;i++){
jc[i]=jc[i-1]*i%mod;
}
cin>>T;
while(T--){
int n;
cin>>n;
int ans=0;
for(int i=1;i<=n;i++){
ans+=C[n][i]*jc[i];
ans%=mod;
}
cout<<ans<<endl;
}
return 0;
}
```
:::
[HDU4045 Machine scheduling](https://acm.hdu.edu.cn/showproblem.php?pid=4045)
:::info[Hint1]
原题计数可分为两部分:
- 从 $1,2,\dots,n$ 中选出 $r$ 个数,要求这 $r$ 个数之间每对数的差值大于等于 $k$,求方案数。
- 把 $r$ 个数分成至多 $m$ 组的方案数。
答案就是两者方案数之积。
:::
:::success[第二部分 Solution]
直接用预处理好的斯特林数,答案为 $\sum_{i=1}^{m}{r\brace i}$。
这题实际上和斯特林数关系不大。
:::
:::info[Hint2]
难点在于处理差值约束,我们可以先删去一些数,转化成简单的组合数,选完以后再插进去。
:::
:::success[第一部分 Solution]
对于每个数,如果它不是选择的最后一个数,我们就删去它后面的 $r-1$ 个数,来保证约束条件,那么一共删去了 $(r-1)(k-1)$ 个数,剩下的选 $r$ 个,答案即为 $\binom{n-(r-1)(k-1)}{r}$。
:::
:::success[code]
```cpp
#include<iostream>
#define int long long
using namespace std;
const int N=1e3+5;
const int mod=1e9+7;
int C[N][N];
int sum[N][N];
int T;
int n,r,k,m;
int dp[N][N];
int ksm(int a,int b){
int ans=1;
while(b){
if(b%2){
b--;
ans*=a;
ans%=mod;
}
b/=2;
a*=a;
a%=mod;
}
return ans;
}
int jc[N],jcn[N];
void ini(){
jc[0]=1;
jcn[0]=1;
for(int i=1;i<=N-2;i++){
jc[i]=jc[i-1]*i;
jc[i]%=mod;
jcn[i]=ksm(jc[i],mod-2);
}
}
int CC(int a,int b){
if(b>a) return 0;
return jc[a]*jcn[b]%mod*jcn[a-b]%mod;
}
int A(int a,int b){
if(b>a) return 0;
return jc[a]*jcn[a-b]%mod;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
C[1][1]=1;
for(int i=2;i<=1000;i++){
for(int j=1;j<=i;j++){
C[i][j]=C[i-1][j]*j+C[i-1][j-1];
C[i][j]%=mod;
}
}
for(int i=1;i<=1000;i++){
for(int kk=1;kk<=1000;kk++){
sum[i][kk]=sum[i][kk-1]+C[i][kk];
sum[i][kk]%=mod;
}
}
ini();
while(cin>>n>>r>>k>>m){
int ans=CC(n-(r-1)*(k-1),r);
int t2=sum[r][m];
cout<<ans*t2%mod<<endl;
}
return 0;
}
```
:::
## 四. 第一类斯特林数
第一类斯特林数整体和第二类斯特林数差不多,但第二类斯特林数更实用,第一类主要用于解决轮换或圆排列有关问题。
#### 1. 定义
将一个 $n$ 元集合划分成 $m$ 个互不区分**非空圆排列**的方案数称为**第一类斯特林数**,记作 $n\brack m$。
#### 2. 预处理
第一类斯特林数有一个优美的递推。
$$
{n\brack m}={n-1\brack m-1}+(n-1){n-1\brack m}
$$
::::info[证明]
考虑组合意义:
- 如果最后一个元素单开一个新的圆排列,由于圆排列互不区分,那么情况数是 $n-1\brack m-1$,把前面的 $n-1$ 个元素分配到剩下的 $m-1$ 个圆排列中即可。
- 如果最后一个元素加入已有的圆排列,由于是圆排列,所以我们只关心其相对位置,他可以插入在前 $n-1$ 个数中任意一个的后面,因此有 $(n-1){n-1\brack m}$ 种情况数。
::::
我们可以用这个递推式 DP 预处理出每个第一类斯特林数,时间复杂度 $O(n^2)$,基本上已经够用了。
DP 边界条件:${0 \brack 0}=1$。其实这是一个约定俗成的规定,方便 DP,当然也可以用 ${n \brack 1}=(n-1)! \ (n \in \mathbb N^*)$。
#### 3. 例题
[HDU 3625 Examining the Room](https://acm.hdu.edu.cn/showproblem.php?pid=3625)
:::info[Hint1]
暴力破解一扇门以后,什么情况下需要暴力破解另一扇门?
:::
:::success[Hint1答案]
打开一扇门以后里面装着暴力破解门的钥匙。
:::
:::info[Hint2]
如果你破坏一扇门以后,打开的门的集合是 $S$,那么对于任意 $a\in S$,破坏 $a$ 后打开的门也是 $S$。
:::
:::info[Hint3]
可以视作把原来的 $n$ 个钥匙分成 $k$ 个圆排列,但是需要解决不合法情况。
:::
:::success[Solution]
先概率转计数,考虑 $n!$ 种情况中有多少合法情况。
先分析一下这个问题大致的结构:我们把房门 $a$ 中有房门 $b$ 中的钥匙视作 $a\to b$ 这条边,那么整个图就是很多环,因为每个点的入度和出度都是 $1$。(这里每个环其实就是一个圆排列)
要满足条件,必须把 $1,2,\dots,n$ 分成 $\le k$ 个环(圆排列),并且排除掉 $1$ 自环的情况。
那么合法方案数就是 $\sum_{i=1}^{k}{n\brack i}-\sum_{i=1}^{k-1}{n-1\brack i}$,第一项是任意分,第二项是去除 $1$ 自环的情况。
时间复杂度 $O(n^2+nT)$。
:::
## 五. 综合应用
看到这里,恭喜你走出了新手村!
前面的这些都只是开胃菜,接下来的题就相当逆天了。
本章默认读者掌握提高级算法与大部分常见组合技巧。
[CF886E Maximum Element](https://www.luogu.com.cn/problem/CF886E)
这题纯推式子,感觉没什么好提示的。
:::success[Solution]
定义 $dp_i$ 表示一个长度为 $i$ 的排列,出现错误的情况数,$f_i$ 表示一个长度为 $i$ 的排列,不“提前返回”的情况数。
考虑如何转移,我们钦定在第 $j$ 个位置上首次出错,并且 $a_j=l \ (l \ne n)$,那么显然 $a_j$ 是前缀最大值,否则上一个比 $l$ 大的数肯定满足后面 $k$ 个比他小,那么我们从所有小于 $l$ 的数里面选 $j+k-1$ 个,然后前 $j-1$ 个内部不能有重复,共 $(j-1)!-dp_{j-1}$ 种选择,后面有 $k!$ 种,剩下的 $i-j-k$ 个数随便排,总结一下:
$$
dp_i=\sum_{j=1}^{i-k-1}\sum_{l=j}^{i-1}\binom{l-1}{j+k-1}\binom{j+k-1}{k}k!f_j(i-j-k)!
$$
$f_i$ 的计算留到最后讲。
这个式子是 $O(n^3)$ 的,太慢了,我们来推一推式子:
$$
\begin{aligned}
dp_i&=\sum_{j=1}^{i-k-1}\sum_{l=j}^{i-1}\binom{l-1}{j+k-1}\binom{j+k-1}{k}k!f_{j-1}(i-j-k)!\\
&=k!\sum_{j=1}^{i-k-1}\binom{j+k-1}{k}f_{j-1}(i-j-k)!\sum_{l=j}^{i-1}\binom{l-1}{j+k-1}\\
&=k!\sum_{j=1}^{i-k-1}\binom{j+k-1}{k}f_{j-1}(i-j-k)!\binom{i-1}{j+k}
\end{aligned}
$$
最后一步是组合数的列求和。
这个式子有一些项和 $i,j$ 都有关,不好处理,继续拆
$$
\begin{aligned}
&k!\sum_{j=1}^{i-k-1}\binom{j+k-1}{k}f_{j-1}(i-j-k)!\binom{i-1}{j+k}\\
&=k!\sum_{j=1}^{i-k-1}\binom{j+k-1}{k}f_{j-1}(i-j-k)!\frac{(i-1)!}{(i-1-j-k)!(j+k)!}\\
&=k!\sum_{j=1}^{i-k-1}\binom{j+k-1}{k}f_{j-1}(i-j-k)\frac{(i-1)!}{(j+k)!}
\end{aligned}
$$
现在只有一项是混在一起的了,乘法分配律拆开来做。对于只和 $j$ 有关的,前缀和每次更新,只和 $i$ 有关的部分直接算。
然后来讲怎么算 $f_j$,方法类似。
最大值出现位置需要在 $[j-k+1,j]$ 内。我们钦定最大值出现位置,那么前半部分就是要选 $i-1$ 个数,并且不提前返回,后半部分任意排。即:
$$
\begin{aligned}
f_j&=\sum_{i=\min(1,j-k+1)}^{j}f_{i-1}\binom{j-1}{i-1}(j-i)!\\
&=\sum_{i=\min(1,j-k+1)}^{j}f_{i-1}\frac{(j-1)!}{(i-1)!(j-i)!}(j-i)!\\
&=(j-1)!\sum_{i=j-k}^{j-1}\frac{f_i}{i!}
\end{aligned}
$$
式子最后偏移了一下下标,这个式子就很优美了,前缀和随便维护一下就可以了。
:::
:::success[Code]
```cpp
#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>
#define int long long
using namespace std;
const int N=3e6+5;
const int mod=1e9+7;
int ksm(int a,int b){
int ans=1;
while(b){
if(b%2){
b--;
ans*=a;
ans%=mod;
}
b/=2;
a*=a;
a%=mod;
}
return ans;
}
int jc[N],jcn[N];
void ini(){
jc[0]=1;
jcn[0]=1;
for(int i=1;i<=N-2;i++){
jc[i]=jc[i-1]*i;
jc[i]%=mod;
jcn[i]=ksm(jc[i],mod-2);
}
}
int C(int a,int b){
if(b>a) return 0;
return jc[a]*jcn[b]%mod*jcn[a-b]%mod;
}
__int128 dp[N],sum=0,sum2=0,f[N],summ[N];
int n,k;
signed main(){
ini();
cin>>n>>k;
f[0]=1,summ[0]=1;
for(int i=1;i<=n;i++){
f[i]=jc[i-1]*(summ[i-1]-((i-k-1>=0)?summ[i-k-1]:0))%mod;
f[i]+=mod;
f[i]%=mod;
summ[i]=summ[i-1]+f[i]*jcn[i];
summ[i]%=mod;
}
for(int i=k+2;i<=n;i++){
int j=i-k-1;
sum+=C(j+k-1,k)*f[j-1]%mod*jcn[j+k]%mod;
sum%=mod;
sum+=mod;
sum%=mod;
sum2+=C(j+k-1,k)*f[j-1]%mod*jcn[j+k]%mod*(j+k)%mod;
sum2%=mod;
sum2+=mod;
sum2%=mod;
dp[i]=jc[k]*jc[i-1]%mod*((i*sum-sum2)%mod)%mod+mod;
dp[i]%=mod;
}
int ans=dp[n]%mod;
cout<<ans;
return 0;
}
```
:::
[P2481 [SDOI2010] 代码拍卖会](https://www.luogu.com.cn/problem/P2481)
:::info[Hint1]
注意到原数必定是一堆($\le 9$ 个)形如 $11111\ldots1$ 的数的和。
:::
:::info[Hint2]
背包。
定义 $dp_{i,j,k}$ 为考虑到模 $P$ 余 $i$ 的 $111\ldots1$,现在选了 $j$ 个后缀 $1$,模 $P$ 余 $k$ 的方案数。
目标:$\sum_{j=1}^{9}dp_{P-1,j,0}$。
:::
:::info[Hint3]
观察到 $1111\ldots1$ 对 $P$ 取模的结果是有周期性的。
:::
:::success[Solution]
首先可以利用周期性处理出对 $P$ 取模为 $i$ 的 $111\ldots1$ 数量 $cnt_i$。
考虑转移方法:
$$
dp_{i,j,k}=\sum_{s=0}^{9-j}dp_{i-1,j-s,(k-si\bmod p+p)\bmod p}\binom{cnt_i+s-1}{s}
$$
最后一项是经典方程 $\sum_{i=1}^{cnt_j}x_i=s$,$x_i \in \mathbb N$ 的解。
:::
[P9896 [ICPC 2018 Qingdao R] Sub-cycle Graph](https://www.luogu.com.cn/problem/P9896)
:::info[Hint1]
原题要求的“半环图”本质上就是一堆链。本题 $O(\sum n)$ 可以接受,所以我们枚举链的数量。
:::
:::info[Hint2]
如果有 $k$ 条链,那么会有 $2k$ 个点度数为 $1$,由于 $\sum d(u)=2m$,因此有 $m-k$ 个点度数为 $2$,剩下的点度数为 $0$。
:::
:::info[Hint3]
分三步走。
1. 钦定 $2k$ 个点作为链的两端,方法数为 $\binom{n}{2k}$。再选 $m-k$ 个中间点,方法数为 $\binom{n-2k}{m-k}$。
2. 计算其配对方法数。
3. 计算度数为 $1$ 的点的分配方法。
:::
:::success[Solution]
第二部分算法:我们先确定一个顺序,选 $k$ 个点出来作为链头 $\binom{2k}{k}$,然后生成一个全排列 $k!$ 作为链尾的排列,那么让链头和链尾排列对应上,就构造出了一种方案。
但是请注意!链头链尾的顺序是我们自己定出来的,实际上并不存在,例如 $u$ 链头 $v$ 链尾和 $v$ 链头 $u$ 链尾其实是一种情况,但是被我们视作了两种情况。这样重算了 $k$ 次。我们要把 $2^k$ 个重算情况去掉。
方案数是 $\frac{\binom{2k}{k}k!}{2^k}$。
第三部分算法:如果点之间互不区分,那么就是经典的不定方程 $\sum_{i=1}^{k}a_i=m-k$。方案数是 $\binom{k+m-k-1}{k-1}$ 即 $\binom{m-1}{k-1}$。
但是点之间是互相区分的,那么乘上排列 $(m-k)!$ 即可。
综合一下,答案即为 $\sum_{k=1}^{\lfloor\frac{n}{2}\rfloor}\binom{n}{2k}\binom{n-2k}{m-k}\frac{\binom{2k}{k}k!}{2^k}\binom{m-1}{k-1}(m-k)!$。
:::
:::success[Code]
```cpp
#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>
#define int long long
using namespace std;
const int N=1e5+5;
const int mod=1e9+7,inv2=(mod+1)/2;
int ksm(int a,int b){
int ans=1;
while(b){
if(b%2){
b--;
ans*=a;
ans%=mod;
}
b/=2;
a*=a;
a%=mod;
}
return ans;
}
int jc[N],jcn[N];
void ini(){
jc[0]=1;
jcn[0]=1;
for(int i=1;i<=N-2;i++){
jc[i]=jc[i-1]*i;
jc[i]%=mod;
jcn[i]=ksm(jc[i],mod-2);
}
}
int C(int a,int b){
if(b<0) return 0;
if(b>a) return 0;
return jc[a]*jcn[b]%mod*jcn[a-b]%mod;
}
signed main(){
ini();
int T;
cin>>T;
while(T--){
int n,m;
cin>>n>>m;
if(m>n) cout<<0<<endl;
else if(m==0) cout<<1<<endl;
else if(m==n) cout<<jc[n-1]*inv2%mod<<endl;
else{
int ans=0;
for(int k=1;k<=min(n/2,min(m,n-m));k++){
ans+=C(n,2*k)*C(n-2*k,m-k)%mod*jc[k]%mod*C(2*k,k)%mod*ksm(inv2,k)%mod*C(m-1,k-1)%mod*jc[m-k]%mod;
ans%=mod;
}
cout<<ans<<endl;
}
}
return 0;
}
```
:::
[[ABC136F] Enclosed Points](https://www.luogu.com.cn/problem/AT_abc136_f)
::::info[Hint1]
考虑点的贡献。
::::
::::info[Hint2]
点的贡献分成三类:
- 自身在被选择集合 $S$ 里。
- 自身不在 $S$ 里,但左上角,右下角都有点在 $S$ 里。
- 自身不在 $S$ 里,但左下角,右上角都有点在 $S$ 里。
::::
::::success[Solution]
首先我们不关注具体数值只关注大小关系,所以先离散化。
如果我们以点集或者矩阵的视角去入手的话,会发现无法做。这时候我们需要换个视角,考虑每个点的贡献。
对于每个点 $P$,它有两种情况会有贡献,假设点集为 $T$。
- $P\in T$,则对于其余 $n-1$ 个点可以任选,贡献共 $2^{n-1}$。
- $P\notin T$,则其余 $n-1$ 个点构成的矩形必须包含 $P$,这时候必定存在两个点 $A,B$ 满足 $X_A<X_P<X_B$ 且 $Y_A<Y_P<Y_B$ 或 $Y_B<Y_P<Y_A$。
记满足 $X_A>X_P,Y_A>Y_P$ 的点数为 $c_1$,满足 $X_A<X_P,Y_A>Y_P$ 的点数为 $c_2$,满足 $X_A<X_P,Y_A<Y_P$ 的点数为 $c_3$,满足 $X_A>X_P,Y_A<Y_P$ 的点数为 $c_4$。
则贡献为 $2^{c_2}2^{c_4}(2^{c_1}-1)(2^{c_3}-1)+2^{c_1}2^{c_3}(2^{c_2}-1)(2^{c_4}-1)-(2^{c_1}-1)(2^{c_2}-1)(2^{c_3}-1)(2^{c_4}-1)$,第一项的意义是满足 $X_A<X_P<X_B$ 且 $Y_A<Y_P<Y_B$ 的贡献,第二项为满足 $X_A<X_P<X_B$ 且 $Y_B<Y_P<Y_A$ 的贡献,第三项是容斥,因为有些时候前两项的限制条件都满足。
用树状数组计算每个点对应的 $c_1,c_2,c_3,c_4$ 即可,转化成了经典问题 [二维数点](https://www.luogu.com.cn/problem/P10814),时间复杂度 $O(n\log n)$。
::::
[[ABC301F] Anti-DDoS](https://www.luogu.com.cn/problem/AT_abc301_f)
::::info[Hint1]
直接求解这个问题是困难的,我们考虑分段求解。
$dp1_i$:前 $i$ 个数不出现 `DD` 型字串的填法。
$dp2_i$:前 $i$ 个数不出现 `DDo` 型字串的填法。
$dp3_i$:前 $i$ 个数不出现 `DDoS` 型字串的填法。
那么我们要求 $dp3_n$。
::::
::::success[Solution]
状态定义见提示。
首先考虑 $dp1_i$,定义 $q_i$ 表示前 $i$ 个字符中问号的个数,$t_{i,j}$ 表示前 $i$ 个字符中第 $j$ 个大写字母出现次数,若存在 $t_{i,j}\ge2$,则有 $dp1_i=0$。否则 $dp1_i=\sum_{j=0}^{\min(q_i,num)}C_{q_i}^{j}\times P_{num}^{j}\times26^{q_i-j}$,其中 $num$ 表示满足 $t_{i,j}=0$ 的 $j$ 的数量。组合意义为把 $j$ 个问号变大写,那么就是先选 $j$ 个问号,再选 $j$ 个可以变的大写字母(注意这个是有序的,`AB` 与 `BA` 是两种填法),剩下的没有约束小写字母随便填。组合数可以预处理,求和最多 $26$ 项,可以接受。
接着考虑 $dp2_i$ 与 $dp3_i$,对于 $dp2_i$,若 $S_i$ 为小写,则 $dp2_i=dp1_{i-1}$,当且仅当前面没有 `DD` 型才行。若为大写,则 $dp2_i=dp2_{i-1}$,不会影响。若为问号,则 $dp2_i=26\times(dp2_{i-1}+dp1_{i-1})$,就是讨论问号具体是什么,然后合并前两种情况。
$dp3_i$ 同理。
::::
接下来来几道黑题!
懒得写提示了,但是题解会分步写。
[[ARC154E] Reverse and Inversion](https://www.luogu.com.cn/problem/AT_arc154_e)
::::success[Step 1:拆贡献]
首先,由定义可知 $f(p)=\sum_{\substack{1\le i<j\le n\\p_i>p_j}}(j-i)$。
这个式子含有 $i,j$ 两个变量,不好处理,考虑对 $i,j$ 这两项的贡献进行单独处理。
先考虑第 $j$ 项产生贡献的系数,每存在一个 $p_j<p_i$ 且 $i<j$,就会让答案增加 $j$,即第 $j$ 项产生的贡献为 $j\sum_{i=1}^{j-1}[p_i>p_j]$。
同理可得第 $i$ 项产生的贡献为 $-i\sum_{j=i+1}^{n}[p_i<p_j]$。
综上所述,$f(p)=\sum_{i=1}^{n}(i\sum_{j=1}^{i-1}[p_i>p_j]-i\sum_{j=i+1}^{n}[p_i<p_j])$。
::::
::::success[Step 2:化简式子]
其实括号里这个式子是有性质的,我们继续化简。
$$
\begin{aligned}
&i\sum_{j=1}^{i-1}[p_i>p_j]-i\sum_{j=i+1}^{n}[p_i<p_j]\\
&=i\sum_{j=1}^{i}[p_i>p_j]-i\sum_{j=i+1}^{n}[p_i\le p_j]\\
&=i^2-i\left(\sum_{j=1}^{i}[p_i\le p_j]+\sum_{j=i+1}^{n}[p_i\le p_j]\right)\\
&=i(i-p_i)
\end{aligned}
$$
因此 $f(p)=\sum_{i=1}^{n}i(i-p_i)$。
::::
::::success[Step 3:计数转期望]
考虑每个位置的数的贡献,发现每个位置的数的贡献仅与其操作后的位置有关,我们可以考虑每个位置的数操作后的期望位置,设第 $i$ 个数的期望位置为 $e_i$,则答案为 $(C_{n+1}^{2})^m\sum_{i=1}^{n}i^2-e_i\times p_i$。
这一步有些费解(如果你觉得不费解请跳过此段),我们来证一下。假设操作完 $m$ 次后,第 $i$ 个数出现在第 $j$ 个位置的情况数为 $q_{i,j}$,则答案为 $(C_{n+1}^{2})^m\sum_{i=1}^{n}i^2-\sum_{i=1}^{n}\sum_{j=1}^{n}q_{i,j}\times j\times p_i$,即为 $(C_{n+1}^{2})^m\sum_{i=1}^{n}i^2-\sum_{i=1}^{n}p_i\times\sum_{j=1}^{n}q_{i,j}\times j$。这就是期望的形式!把 $\sum_{j=1}^{n}q_{i,j}\times j=e_i\times(C_{n+1}^{2})^m$ 代入原式即可。
::::
::::success[Step 4:计算期望]
现在唯一的问题就是如何快速计算 $e_i$。
我们先计算第 $i$ 个数在进行操作后的期望位置。考虑第 $i$ 个数在操作以后位置变为 $j$ 的方案数,不妨设 $i\le j$。显然 $l=i,r=j$ 是一组可行的方案。其余方案必须满足 $l=i-a,r=j+a \ (a\in \mathbb Z^+)$,$i>j$ 时同理。因此方案数为 $\min(i,j,n-j+1,n-i+1)$,令 $j'=n-j+1$,发现 $\min(i,j,n-j+1,n-i+1)=\min(i,j',n-j'+1,n-i+1)$,即第 $i$ 个数在操作以后位置变为 $j$ 的方案数与变为 $n-j+1$ 的方案数相等,因此当 $i$ 被操作时,他的期望位置即为 $\frac{n+1}{2}$。
当第 $i$ 个数未被操作时,他的位置期望显然为 $i$。
包含第 $i$ 个数的操作数量为 $i(n-i+1)$,而操作总数为 $\frac{n(n+1)}{2}$,因此 $m$ 次操作后第 $i$ 个数的期望位置 $e_i$ 为 $ti+(1-t)\frac{n+1}{2}$,其中 $t=(1-\frac{2i(n-i+1)}{n(n+1)})^m$,快速幂计算即可。
每个 $e_i$ 可以 $O(\log m)$ 计算,求答案是线性的,总复杂度为 $O(n\log m)$。
::::
[[AGC056B] Range Argmax](https://www.luogu.com.cn/problem/AT_agc056_b)
::::success[Step 1:构造双射]
我们的目标是计数 $x$,而不同的 $p$ 可能对应同样的 $x$,不好计数。我们希望建立一个 $p$ 与 $x$ 之间的一一对应,我们从 $x$ 出发,构造以下“典型排列”:
> 我们考虑排列中每个数 $i$,将其放置在 $p$ 中最前面的满足所有 $x$ 中约束条件的位置。对于其满足的约束,将该约束所对应的区间删去。
:::info[证明]
一方面,不妨设一个 $x$ 对应了两个“典型排列”$p_1,p_2$。设 $k$ 是最大的在 $p_1,p_2$ 中位置不同的数,不妨设其在 $p_1$ 中的位置在 $p_2$ 的位置前边,则与“放在最前面矛盾”。
另一方面,如果一个“典型排列”$p$ 对应了两个序列 $x$,显然与 $x$ 的定义矛盾。
:::
举个例子,样例 $1$ 中 $x=(1,1)$ 显然是一种可行情况,则它对应的 $p$ 即为 $p=(3,2,1)$。
因此我们只需计数“典型排列”$p$ 的数量。
::::
::::success[Step 2:DP 计数]
我们定义 $dp_{i,j,k}$ 表示在区间 $[i,j]$ 中,区间最大值位置 $\ge k$ 的“典型排列”数量,则答案即为 $dp_{1,n,1}$。
这里这个 $k$ 可能看起来意义不明,但是在转移中很有用。
边界条件:若 $i>j$,则 $dp_{i,j,k}=1$。
若 $k>j$,则 $dp_{i,j,k}=0$。
我们分两类进行转移。
- 最大值位置 $>k$,这种情况的数量即为 $dp_{i,j,k+1}$。
- 最大值位置即为 $k$,此时区间被分为了两部分 $[i,k-1]$ 与 $[k+1,r]$。
> 左右两部分区间是独立的,但是由于“典型排列”的定义,左区间有额外约束:假设左区间的最大值位置为 $k'$,则必须存在一个约束区间同时包含 $k$ 与 $k'$。
:::info[证明]
我们交换 $p_k$ 与 $p_{k'}$。
如果所有原来包含 $k$ 的区间中的次大值 $\le p_{k'}$,那么其他区间的最大值位置也不会变。
否则显然是 $k$ 右边的数影响了答案,我们把交换后位置 $\ge k$ 的所有数重新根据原有偏序关系排序。即假设原来 $k$ 右边的数离散化以后原来第 $i$ 个数的排名是 $r_i$,交换后重新排序,使得第 $i$ 个数的排名依旧是 $r_i$。
这样的话所有区间的最大位置都不会改变。
:::
所以左区间最大值位置 $k'$ 应当与 $k$ 在同一个限制区间中,预处理一个数组 $l_{i,j,k}$ 表示在 $[i,j]$ 内且包含 $k$ 的区间的左端点最小值,即 $k'$ 的最左位置。
综上 $dp_{i,j,k}\gets dp_{i,j,k+1}+dp_{i,k-1,l_{i,j,k}}\times dp_{k+1,j,k+1}$,提前预处理 $l_{i,j,k}$,然后区间 DP 即可。
:::info[预处理相关]
首先显然对于一段限制区间 $[i,j]$,对于任意 $k\in[i,j]$ 有 $l_{i,j,k}\gets\min(l_{i,j,k},i)$。
考虑完每个区间后,再重新更新一下:
- 当 $k\in[i+1,j-1]$ 时 $l_{i,j,k}\gets\min(l_{i,j,k},l_{i,j-1,k},l_{i+1,j,k})
- 当 k=j 时,l_{i,j,k}\gets\min(l_{i,j,k},l_{i+1,j,k})
-
| 当 k=i 时,l_{i,j,k}\gets\min(l_{i,j,k},l_{i,j-1,k}) |
| 预处理复杂度和 DP 复杂度都是 O(n^3) 的,可以接受。 |
CF1770F Koxia and Sequence
::::success[Step 1:对称性]
下文中的 a 指的是一个好数组。
由题意得,答案为:
\bigoplus_{a}\bigoplus_{i=1}^{n}a_i
交换异或顺序,得答案为:
\bigoplus_{i=1}^{n}\bigoplus_{a}a_i
也就是说,我们可以分别考虑每个位置对答案的贡献。
由于每个位置的数有对称性,因此对于每个 i,\bigoplus_{a}a_i 都相等,设为 v。
| 那么答案就是 \bigoplus_{i=1}^{n}v,显然当 n 为偶数时,答案为 0,n 为奇数时,答案为 v。下面考虑怎么求 v=\bigoplus_{a}a_1。 |
| ::::success[Step 2:拆位] |
| 我们现在考虑 a_1 每一位的贡献,定义 c_i 为在多少个“好数组”中,a_1 的第 i 位为 1。 |
下文中用集合形式表示数,即 i\in a_1 表示 a_1 第 i 位为 1。
| 由于异或的性质,我们只需要计算每个 c_i 的奇偶性。 |
| ::::success[Step 3:容斥与子集反演] |
| 按位或的条件很难处理,我们尝试把他消掉。 |
定义 H(S) 表示和为 x 且按位或为 S 的好数组方案数,G(S) 表示和为 x 且按位或结果 \subseteq S 的方案数,那么 G(S) 很好用 H(S) 表示。
G(S)=\sum_{T\subseteq S}H(T)
然后我们可以子集反演,得:
H(S)=\sum_{T\subseteq S}(-1)^{|S|-|T|}G(T)
这个公式感性理解一下吧,我也不会证。
由于我们只关心 H(S) 奇偶性,那么两边 \bmod 2 由于 -1\equiv1\pmod 2,所以神奇的是,-1 全消掉了!
而且模 2 意义下,求和就是异或,那么可知:
H(S)\equiv\bigoplus_{T\subseteq S}G(T)\pmod 2
我们再定义 H_i(S) 是 i\in a_1 且和为 x 且按位或为 S 的好数组方案数,G_i(S) 是 i\in a_1 且和为 x 且按位或 \subseteq S 的好数组方案数。那么有:
c_i=H_i(y)\equiv\bigoplus_{T\subseteq y}G_i(T)\pmod 2
如果 i\notin T 则显然 G_i(T)=0。a_1 有限制,其他没限制,我们把 T\subseteq y 和 i\in a_1 转化为 a_1-2^i\subseteq T-2^i。
那么:
G_i(T)=[i\in T]\sum_{\sum_{k=1}^{n}a_k=x}[a_1-2^i\subseteq T-2^i]\prod_{j=2}^{n}[a_j\subseteq T]
::::
::::success[Step 4:逆用 Lucas 定理]
首先,由 Lucas 定理 \binom{n}{m}\equiv[m\subseteq n]\pmod 2。
那么
G_i(T)\equiv\sum_{\sum_{k=1}^{n}a_k=x}\binom{T-2^i}{a_1-2^i}\prod_{j=2}^{n}\binom{T}{a_j}\pmod 2
::::
::::success[Step 5:范德蒙德卷积]
由范德蒙德卷积,G_i(T)\equiv\binom{nT-2^i}{x-2^i}\equiv[i\in T][(x-2^i)\subseteq(nT-2^i)],然后我们枚举每一位再枚举 T 分别计算。
CF1603E A Perfect Problem
:::success[引理 1]
一个序列是否完美与其顺序无关,因此我们先将原序列排序后处理。
:::
:::success[引理 2]
一个排好序的序列完美当且仅当其所有子区间是好的。
::::info[证明]
必要性显然。
| 对于一个子序列,设其和为 s,如果其中的数出现位置最靠前为 l,最靠后为 r,那么由于 [l,r] 合法,a_la_r\ge\sum_{i=1}^{n}a_i\ge s。因此任意子序列合法。 |
| :::success[引理 3] |
| 一个排序好的序列完美当且仅当其所有前缀是好的。 |
| ::::info[证明] |
| 必要性显然。 |
| 充分性:对于一个子区间 [l,r],由于 a_la_r\ge a_r a_1\ge\sum_{i=1}^{n}a_i\ge\sum_{i=l}^{r}a_i,知其是好的。由引理 2 得证。 |
| :::success[引理 4] |
| 一个完美的序列满足 i\le a_i。 |
::::info[证明]
由于 a_1a_i\ge\sum_{j=1}^{i}a_j\ge ia_1。
| 知 a_i\ge i。 |
| :::success[引理 5] |
| 令 b_i=a_i-a_1,则 \sum b_i\le a_1。 |
| ::::info[证明] |
| 由于 (n+1)a_1\ge a_1(x+b_n)\ge\sum b_i+na_1。 |
| 得 a_1\ge\sum b_i。 |
| ::::success[引理 6] |
| 在合法情况下,a_1\ge n-2\sqrt n。 |
::::info[证明]
由引理 4,b_i=a_i-a_1\ge i-a_1。
从而 \sum_{i=1}^{n}b_i\ge\sum_{i=a_1+1}^{n}(i-a_1)=\frac{(n-a_1)(n-a_1+1)}{2}。
但是又由引理 5 可得,a_1\ge\sum_{i=1}^{n}b_i。
从而可得 a_1\ge\frac{(n-a_1)(n-a_1+1)}{2}。
::::success[Solution]
枚举最小值 a_1,定义 dp_{i,j} 表示当前选了 i 个数,前缀和为 j 的方案数,从小到大依次枚举每个数以及其选的个数。DP 即可。
::::