CCPC 2021 Online H - Subpermutation 复盘:排列边界与逆阶乘求和

题目中的两类连续段

2021 CCPC 网络选拔赛(重赛)H - Subpermutation 定义了 nn 的 full-permutation:把 11nn 的全部排列按字典序依次拼接,得到序列 pnp_n

题目给定 n,mn,m,要求统计 pnp_n 中有多少个长度为 mm 的连续子序列,本身恰好是 11mm 的一个排列。多组数据的答案对 109+710^9+7 取模,其中

1mn106,T105. 1\le m\le n\le 10^6,\qquad T\le 10^5.

每个原排列的长度为 nn,而待统计连续段的长度 mm 不超过 nn。因此,一个连续段至多经过两个原排列,所有情况可以完整地拆成两类:

  • 连续段完全位于一个排列内部;
  • 连续段取前一个排列的非空后缀,再取后一个排列的非空前缀。

两类之间互不重叠,分别计数后相加即可。

一个排列内部的贡献

长度为 mm 的连续段是 11mm 的排列,当且仅当这 mm 个数在当前排列中占据连续的 mm 个位置。

先确定这个连续块的起点,共有 nm+1n-m+1 种选择;块内的 11mm 可以任意排列,其余 nmn-m 个数也可以任意排列。因此,完全位于单个排列内部的合法连续段总数为

I=(nm+1)m!(nm)!. I=(n-m+1)m!(n-m)!.

这里是在全部 n!n! 个排列中整体计数:每个满足条件的排列恰好贡献一个合法位置,不会重复。

跨过排列边界

固定跨边界连续段从前一个排列末尾取出的长度为 kk,其中

1km1. 1\le k\le m-1.

d=mkd=m-k,并把前一个排列 PP 按位置拆成

P=A+B+C, P=A+B+C,

其中 A=d|A|=dB=nm|B|=n-mC=k|C|=k。跨边界的连续段由 CC 和后继排列 QQ 的长度为 dd 的前缀拼成。

先只要求 AACC 合起来恰好包含 11mm。这时:

  • 11mm 可以任意放入 A,CA,C 对应的共 mm 个位置,有 m!m! 种;
  • m+1m+1nn 可以任意放入中间的 BB,有 (nm)!(n-m)! 种。

候选排列共有

m!(nm)! m!(n-m)!

个。还需要判断从 PP 走到字典序后继 QQ 时,前缀 AA 是否保持不变。

最长下降后缀带来的扣除项

求一个排列的字典序后继时,需要找到它的最长下降后缀:交换后缀前的枢轴,再把后缀改成最小的升序状态。

如果 B+CB+C 不是下降序列,枢轴一定落在 B+CB+C 内,长度为 dd 的前缀 AA 不会改变。此时跨边界连续段就是 C+AC+A,其中每个 11mm 恰好出现一次,因此合法。

反过来,如果 B+CB+C 整体下降,字典序后继操作就会越过 AAB+CB+C 的分界。后继排列的前 dd 项不再恰好是原来的 AA:会有原分界右侧的元素进入前缀;若 PP 已是最后一个排列,则它本来也没有后继。无论哪种情况,CC 与新前缀都不能再恰好组成 11mm 的排列。

于是只需从候选数中扣除 B+CB+C 整体下降的情况。

由于 BB 中的数都大于 mmCC 中的数都不超过 mm,二者交界处天然满足下降关系。要让 B+CB+C 整体下降,只需让 BBCC 各自都按降序排列:

  • 11mm 中选择 kk 个数放进 CC,有 (mk)\binom{m}{k} 种;选定后,CC 的降序唯一;
  • 剩下的 dd 个数在 AA 中任意排列,有 d!d! 种;
  • BB 的降序唯一。

扣除项因此为

(mk)d!=(mk)(mk)!=m!k!. \binom{m}{k}d! =\binom{m}{k}(m-k)! =\frac{m!}{k!}.

固定 kk 时,合法的跨边界连续段数量为

Bk=m!(nm)!m!k!=m!((nm)!1k!). B_k=m!(n-m)!-\frac{m!}{k!} =m!\left((n-m)!-\frac{1}{k!}\right).

对所有 kk 求和:

B=k=1m1Bk=m!((m1)(nm)!k=1m11k!). \begin{aligned} B &=\sum_{k=1}^{m-1}B_k \\ &=m!\left((m-1)(n-m)!-\sum_{k=1}^{m-1}\frac{1}{k!}\right). \end{aligned}

合并为闭式

把排列内部与排列边界的贡献相加:

Ans=I+B=(nm+1)m!(nm)!+m!((m1)(nm)!k=1m11k!)=m!(n(nm)!k=1m11k!). \begin{aligned} Ans &=I+B \\ &=(n-m+1)m!(n-m)! \\ &\quad +m!\left((m-1)(n-m)!-\sum_{k=1}^{m-1}\frac{1}{k!}\right) \\ &=m!\left(n(n-m)!-\sum_{k=1}^{m-1}\frac{1}{k!}\right). \end{aligned}

最终只需要阶乘、逆阶乘以及逆阶乘的前缀和。

Si=k=1i1k!, S_i=\sum_{k=1}^{i}\frac{1}{k!},

实现中的 fac[i]ifac[i]sum[i] 分别维护 i!i!(i!)1(i!)^{-1}SiS_i。单次询问直接计算

1
return (1ll * n * fac[n - m] % mod - sum[m - 1] + mod) % mod * fac[m] % mod;

其中的除法都在模 109+710^9+7 意义下用逆元完成。预处理范围小于模数,所以每个阶乘都有逆元。

m=1m=1 时,求和为空,sum[0]=0,公式给出 n(n1)!=n!n(n-1)!=n!,正好等于完整串中数字 1 的出现次数。当 n=mn=m 时,fac[n-m]=fac[0]=1,同一套公式也无需额外分支。

复杂度与实现边界

阶乘、逆阶乘和前缀和预处理到 10610^6,时间复杂度为 O(106+log(109+7))O(10^6+\log(10^9+7)),空间复杂度为 O(106)O(10^6)。完成预处理后,每组询问只进行常数次模运算,单次复杂度为 O(1)O(1),全部询问为 O(T)O(T)

实现中还有三个边界需要保持一致:

  • fac[0]=1,保证 n=mn=m 时可以直接取 0!0!
  • 逆阶乘前缀和从 ifac[1] 开始,保证 m=1m=1 时扣除项为空;
  • 模减法先加 mod 再取模,避免中间结果为负数。

最终 AC 代码

点击展开/折叠 最终 AC 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr int mod = 1e9 + 7, maxn = 1e6 + 5;

ll t, fac[maxn], ifac[maxn], sum[maxn];

ll qpow(ll a, ll b) {
ll res = 1;
a %= mod;
while (b) {
if (b & 1) {
res = res * a % mod;
}
a = a * a % mod;
b >>= 1;
}
return res;
}

void init() {
fac[0] = 1;
for (int i = 1; i < maxn; ++i) {
fac[i] = fac[i - 1] * i % mod;
}
ifac[maxn - 1] = qpow(fac[maxn - 1], mod - 2) % mod;
for (int i = maxn - 2; i >= 0; --i) {
ifac[i] = ifac[i + 1] * (i + 1) % mod;
}
for (int i = 1; i < maxn; ++i) {
sum[i] = (sum[i - 1] + ifac[i]) % mod;
}
}

ll ans(int n, int m) {
return (1ll * n * fac[n - m] % mod - sum[m - 1] % mod + mod) % mod * fac[m] % mod;
}

int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
init();
cin >> t;
while (t--) {
int n, m;
cin >> n >> m;
cout << ans(n, m) << '\n';
}
return 0;
}