1. 程式人生 > 其它 >【組合數】Subpermutation-[2021中國大學生程式設計競賽(CCPC)- 網路選拔賽(重賽)]

【組合數】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) < k\),故這一情況的排列方式可以像上一種那樣算出來

首先\(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_,)ノ

*/