组合数学:从入门到入坟

· · 算法·理论

前言

本文难度跨度较大,从绿题到黑题不等,既有对基本定义的介绍,也有对较综合习题的讲解,如果你对基础知识足够了解,可以直接跳到最后一章。

一. 排列与组合

1. 定义

从一个 n 元集合中选出 m 个数,按顺序排成一个有序序列,这样的方案数称为排列,记作 P_{n}^{m}

序列的顺序性是关键,不同顺序视为不同排列。如 1,2,31,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,31,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[证明]
分类讨论,使用组合意义证明。

根据加法原理,上式成立。

其次我们知道 \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 Pq^{-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! 种方法。下面我们讨论插空的方法,我们把老师也带上,分两类讨论。

综合一下,共 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_il_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})
预处理复杂度和 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 为偶数时,答案为 0n 为奇数时,答案为 v。下面考虑怎么求 v=\bigoplus_{a}a_1
::::success[Step 2:拆位]
我们现在考虑 a_1 每一位的贡献,定义 c_i 为在多少个“好数组”中,a_1 的第 i 位为 1

下文中用集合形式表示数,即 i\in a_1 表示 a_1i 位为 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)=0a_1 有限制,其他没限制,我们把 T\subseteq yi\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 分别计算。

时间复杂度 O(y\log y)

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 即可。 ::::