【组合数】Subpermutation-[2021中国大学生程序设计竞赛(CCPC)- 网络选拔赛(重赛)]
题目链接(http://acm.hdu.edu.cn/showproblem.php?pid=7133)
Time Limit: 2000/1000 MS (Java/Others)
Memory Limit: 262144/262144 K (Java/Others)
Problem Description
A permutation of \(n\) is a sequence of length \(n\) in which each number from \(1\) to \(n\) appears exactly once. A \(\textit{full-permutation}\) of \(n\) is a sequence that connects all permutations of \(n\) into one sequence in lexicographical order. Sequence \(p_1, p_2, \dots, p_n\) is lexicographically smaller than \(q_1, q_2, \dots, q_n\) if \(p_i \lt q_i\) where \(i\) is the minimum index satisfying \(p_i \neq q_i\).
Here are some symbols used in this problem:
- \(p_n\): the full-permutation of \(n\). For example, \(p_3 = \{1,2,3,1,3,2,2,1,3,2,3,1,3,1,2,3,2,1\}\) .
- \(S_n\): the set of all permutations of \(n\). For example, \(S_3=\{\{1,2,3\},\{1,3,2\},\{2,1,3\},\{2,3,1\},\{3,1,2\},\{3,2,1\}\}\).
- \(f(s,t)\): the number of contiguous subsequences in \(s\) that are equal to \(t\). For example, \(f(\{1,2,12,1,2\},\{1,2\})=2\).
Now given \(n\) and \(m\), please calculate \(\sum_{t\in{S_m}}f(p_n,t)\) modulo \(10^9+7\).
Input
The first line contains one integer \(T\ (1\le T\le 10^5)\), indicating the number of test cases.
The only line for each case contains two integers \(n\ (1\le n\le 10^6)\) and \(m\ (1\le m\le n)\), as described in the description.
Output
For each test case, output a single integer \(\sum_{t\in{S_m}}f(p_n,t)\) modulo \(10^9+7\).
Sample Input
4
2 1
2 2
3 2
4 3
Sample Output
2
2
4
15
Hint
For the third case in the sample, \(p_3 = \{1,2,3,1,3,2,2,1,3,2,3,1,3,1,2,3,2,1\}\), \(S_2=\{\{1,2\},\{2,1\}\}\). There are \(4\) contiguous subsequences in \(p_3\) that are equal to \(\{1,2\}\) or \(\{2,1\}\): \(\{\underline{1,2},3,1,3,2,\underline{2,1},3,2,3,1,3,\underline{1,2},3,\underline{2,1}\}\).
题意
把所有\(n\)元排列按字典序从小到大排成一列,求其中有多少子串为\(m\)元{1 ~ m}排列,模\(1e9+7\),\(t\)组询问,(\(T ≤ 10 ^ 5,1 ≤m≤n≤10^6\))
思路
代码很简短,思路清楚比较重要,补题怕自己忘了所以记录一下(来自emo老师讲题,自己做的笔记)
分解子问题:
子问题一:在一个完整的\(n\)元排列里面有\(m\)元排列为子串的个数
假设\(p_1,p_2……p_n\)为\(n\)元排列,里面有一个子串是\(m\)元排列。\(m\)元排列的元素紧凑在一起,所以它自己的元素的排列方式有\(m!\)种,把它看成一个小团。{1~m}共包含\(m\)个数,剩下来的数共\(n-m\)个,剩下的数各为小团,把所有小团进行一个排列,所以总排列数为 \(m\)元排列小团的排列数 * 所有小团的排列数,即 \(m! * (n - m + 1) !\)
子问题二(难点):\(m\)元排列有可能穿插在了两个相邻的\(n\)元排列中
\(e.g.\) \(n = 4,m = 4\)
排列{\(1,2,\underline{3,4,1,2,}4,3 ......\)} 或者{\(1,\underline{2,3,4,1,}2,4,3 ......\)} 中前两个\(n\)元排列中也出现了合法的\(m\)元排列
这时候就要思考一下两个相邻的\(n\)元排列有什么性质
随便列举几个相邻排列
{\(1,2,3,4\)}{\(1,2,4,3\)} {\(2,4,1,3\)}{\(2,4,3,1\)}
我们会发现对于\(n\)元排列 \(p_1,p_2 ... p_n\) 可以找到
\(p_1,p_2,...p_k < p_{k + 1} > p_{k + 2} > ... > p_n\)(\(k\)是最后一个满足\(p_k < p_{k + 1}\) 的下标)
它的下一个排列中的\(p_k\),会被原排列中的一个\(p_j\) (\(p_j > p_k\))替换掉,后面的元素倒着排列
那么下一个排列为
\(p_1,p_2,...,p_{k-1}, p_j,p_n,p_{n-1}, ...p_{j+1} ,p_k,p_{j-1},...p_{k+1}\)
它符合的性质是
\(p_1,p_2,...,p_{k-1}, p_j>p_n<p_{n-1}< ... <p_{j+1} <p_k<p_{j-1}<...<p_{k+1}\)
如果一个\(m\)元排列插在两个\(n\)元排列中间,从下标\(i\)开始,
①(\(i≤k\)) 该排列构造:\(p_i,...,p_k,p_{k + 1},p_{k + 2},...,p_j,...,p_n,p_1,p_2...p_{i + m - 1 - n}\)
思考这里有多少种排列方式
首先剩下的\(n-m\)个元素随便排,这部分是\((n - m)!\),\(m\)元序列里\(1\)到\(m\)本应随便排,但要求\(1\)到\(m\)在前一个\(n\)元排列中,一定要出现一个\(p_k<p_{k + 1}\)这样的数对才行,否则不合法,所以剩下的部分是\(m!\)减去不合法的情况,即减去\(p_i>p_{i + 1} > ... >p _ n\)这种情况,即\(m! - C^m_{n-i+1} * [m-(n-i+1)]!\)
所以方案数 = \((n - m)!*(m! - C^m_{n-i+1} * [m-(n-i+1)]!)\)
②(\(i ≥k+1\))该排列构造:\(p_i\)从\(p_i≥p_{k+1}\)的位置到下一个序列 (不敲了,荧光笔范围)

首先\(1\)到\(m\)一定是\(1\)到\(n\)里面最小的\(m\)个数,对这两个序列进行分析,发现\(p_j\)要大于\(p_k\),所以如果\(p_j\)在\(1\)到\(m\)的序列里那\(p_k\)必定在里面,这和\(p_i\)的范围不符,因而只有一种可能就是\(p_j\)不在\(1\)到\(m\)的序列里,则最后一位的下标\((i+m-1-n) < j\),故这一情况的排列方式可以像上一种那样算出来
首先\(1\)到\(m\)怎么选,第一个排列里,规则同上,为\(C^m_{n-i +1} * [m - (n - i + 1)] !\)
剩下的数里,由于一定要包含\(p_k < p_{k +1}\),所以剩下的数不能完全呈单调递减,需要减去单调递减一情况,为\(((n-m)!-1)\)
所以方案数 = \(C^m_{n-i +1} * [m - (n - i + 1)] ! * ((n-m)!-1)\)
总结来说,横跨两个\(n\)元排列的方案数为\(\sum_{n - m + 2≤i≤n} (n - m)! * (m ! - C_m^{n - i +1} * (m - (n - i + 1))!+ C_m^{n - i + 1} * (m - (n -i +1))! * ((n - m)! - 1)\)
再进行一些推导,发现

这两个颜色不同的地方是可以相互抵消的
那么化简就可得方案数 = \(\sum_{i = n - m + 2} ^ n m! * (n - m)! - C_m^{n - i + 1}*(m - (n - i + 1))!\)
= \(\sum_{i = n - m + 2} ^ n m! * (n - m)! - m!/(n - i + 1) !\)
= \((m-1) * m! * (n - m) !-m! *\sum_{i = n - m + 2} ^ n 1/(n - i + 1)!\)
= \((m-1) * m! * (n - m) !-m! *\sum_{i = 1} ^ {m - 1} 1/i!\)
这样,预处理之后,O(1)即可得横跨两个\(n\)元排列的方案数
而一个排列内的方案数,预处理下阶乘即可,两种排列方式的方法相加即得答案
AC代码
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define llinf 0x3f3f3f3f3f3f3f3f
#define int long long
#define ull unsigned long long
#define PII pair<int,int>
using namespace std;
typedef long long ll;
const int N = 1e6 + 100;
const int mod = 1e9 + 7;
int t, n, m;
int a[N];
int fact[N], invfact[N];
int quickpow(int a, int b) {//快速幂板子
int res = 1;
while (b > 0) {
if (b & 1) res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res;
}
void init() {//预处理
fact[0] = 1;
for (int i = 1; i < 1000001; i++) {
fact[i] = fact[i - 1] * i % mod;
}
invfact[1000000] = quickpow(fact[1000000], mod - 2);
for (int i = 1000000; i; i--) {
invfact[i - 1] = invfact[i] * i % mod;
}
for (int i = 1; i < 1000001; i++) {
invfact[i] = (invfact[i - 1] + invfact[i]) % mod;
}
return;
}
void solve() {
cin >> n >> m;//输入 n m
cout << (fact[m] * (n * fact[n - m] % mod - invfact[m - 1] + invfact[0] + mod) % mod) << endl;//直接输出公式求得答案
return;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
init();
while (cin >> t) {
while (t--) {
solve();
}
}
return 0;
}
/*
i raised a cute kitty in my code,
my friend who pass by can touch softly on her head:)
/l、
Meow~(゚、 。7
|、 ~ヽ
じしf_,)ノ
*/

浙公网安备 33010602011771号