【组合数】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_,)ノ

*/
posted @ 2021-10-25 20:47  TomiokapEace  阅读(439)  评论(2)    收藏  举报