就差一点解题报告
description
题目描述
冒泡排序是⼀个简单的排序算法,其时间复杂度为 O ( n 2 ) O(n^2) O(n2)
有⼀个大小为 n n n的排列 p 1 , . . . , p n p_1,...,p_n p1,...,pn,⼩明想对这个排列进⾏冒泡排序,于是写了下⾯这份代码:
for(int i=1;i<=k;i++)
for(int j=1;j<n;j++)
if(p[j]>p[j+1]) swap(p[j],p[j+1]);
细⼼的选手不难发现小明手抖把第⼀行中的 n n n打成了 k k k,所以当 k k k比较小时,这份代码可能会出错
⼩明发现当这份代码出错时,可能就差⼀点就能把这个排列排序,他定义⼀个排列就差⼀点能被排序,当且仅当这个排列存在⼀个大小为 n − 1 n-1 n−1的上升子序列
小明想知道,对于给定的 n , k n,k n,k,有多少种不同的排列满⾜对这个排列运⾏上述代码后,这个排列就差⼀点能被排序
由于答案可能很⼤,⼩明只需要知道答案对质数 m o d mod mod取模的结果
输入格式
本题⼀个测试点含有多组测试数据,第⼀行⼀个整数 T T T,表示数据组数
接下来 T T T行,每行 3 3 3个整数, n , k , m o d n,k,mod n,k,mod,意义同题意
输出格式
共 T T T行,对于每组测试数据,输出一行一个整数表示答案
样例
5
5 1 998244353
5 2 998244353
5 3 998244353
5 4 998244353
5 5 998244353
74
114
120
120
120
数据范围
1 ≤ n , k , T ≤ 5000 , 1 0 8 ≤ m o d ≤ 1 0 9 + 7 1\le n,k,T\le5000,10^8\le mod\le 10^9+7 1≤n,k,T≤5000,108≤mod≤109+7
solution
-
DDG
有个神奇 D P DP DP,正确倒是正确,只是这是怎么想到的呢?d p i , j = ∑ k = 1 j − 1 d p i − 1 , k + j ∗ d p i , j dp_{i,j}=\sum_{k=1}^{j-1}dp_{i-1,k}+j*dp_{i,j} dpi,j=∑k=1j−1dpi−1,k+j∗dpi,j (前 i i i个数,在 x x x前面比 x x x大的数的个数最大值为 j j j 的序列)
因为一次冒泡排序,相当于处理了 在 i i i前面比 i i i大的数个数最多 的 i i i
-
卷爷
找了三个强大的性质最重要的性质就是,对于值属于 [ i , n ] [i,n] [i,n]的所有下标,将这些下标抽出来,然后根据值离散化
如果离散化后的序列需要 t t t次变成单调上升,那么回到原序列,也只需要 t t t次,单看这些下标,就会发现是单调上升的
结合这两种思路,就来到了小儿子
的通俗易懂的解法——反序表
反序表对于一个排列的定义为 s i = ∑ j = 1 i − 1 [ p j > p i ] s_i=\sum_{j=1}^{i-1}[p_j>p_i] si=∑j=1i−1[pj>pi]
- 即反序表的第 i i i位上的数值表示:在原排列中,第 i i i位以前的比第 i i i位值大的个数
e.g.
原序列3 2 4 1 5
,反序表为0 1 0 3 0
反序表具有很多非常好的性质
-
显然对于 i i i, s i < i s_i<i si<i(严格小于);换言之,对于 i i i,其 s i s_i si的取值为 [ 0 , i ) [0,i) [0,i)共 i i i个
-
反序表与排列是一一对应的,那么原题要求排列个数,就转化成了求反序表的个数
-
冒泡一轮排序会将 最大的 还没在应在位置的值 放置在 其应在位置,然后这区间中的每个数位置都会前移一位,其在反序表的变化则为下标,值均减一(如果已经是 0 0 0就不减)
换言之,一次冒泡排序后,每个数至多只会减少一
e.g.
原序列
3 2 5 1 4
,反序表为0 1 0 3 1
一次冒泡排序后
原序列
3 2 1 4 5
,反序表为0 1 2 0 0
5 5 5由位置 3 3 3变到 5 5 5,反序表改变的区间为 [ 3 , 5 ] [3,5] [3,5]
-
反序表中 i i i位置上的值如果为 s i s_i si,意味着至少需要 s i s_i si次冒泡排序才能将原序列 i i i有序
这里的有序定义为,其前面的数全小于ta,其后面的数全大于ta
了解完反序表的性质后,就可以解决这道题了
-
考虑冒泡排序后,最后的序列是一个长为 n n n的上升子序列(不差一点)
这时的反序表全是 0 0 0,
0,0,0,...,0
一次排序,反序表只能减一或者不减, k k k次排序最多减少 k k k
也就是说想要最后反序表为 0 0 0,其初始值不能超过 k k k
即: ∀ i s i ≤ min ( i − 1 , k ) \forall_i\quad s_i\le \min(i-1,k) ∀isi≤min(i−1,k)
将这些值域限制乘起来就是不同的反序表个数,也就是不同的排列个数
即: ∏ i = 1 n ( min ( i − 1 , k ) + 1 ) \prod_{i=1}^n\Big(\min(i-1,k)+1\Big) ∏i=1n(min(i−1,k)+1)
-
考虑冒泡排序后,最后的序列是一个长为 n − 1 n-1 n−1的上升子序列(只差一点)
-
这时的反序表形如
0,0,...,1,1,...,1,0,0,...,0
e.g.
最后序列为4 1 2 3 5
,反序表为0 1 1 1 0
最后为 0 0 0说明初始反序表的值不超过 k k k
最后为 1 1 1说明初始反序表的值不超过 k + 1 k+1 k+1
注意: s i s_i si能取到 k + 1 k+1 k+1的 i i i是有限制的,仅为 [ k + 2 , n ] [k+2,n] [k+2,n],共 n − ( k + 2 ) + 1 = n − k − 1 n-(k+2)+1=n-k-1 n−(k+2)+1=n−k−1个
(不要忘记 s i < i s_i<i si<i的约束)
考虑枚举这段 1 1 1的长度 l e n len len
这段长度的选择方案相当于在总长为 n − k − 1 n-k-1 n−k−1中摆下 l e n len len的放置方案,显然为 n − k − 1 − l e n + 1 = n − k − l e n n-k-1-len+1=n-k-len n−k−1−len+1=n−k−len种
剩下的 n − k − 1 − l e n n-k-1-len n−k−1−len个数反序表都不超过 k k k,可选为 [ 0 , k ] [0,k] [0,k]共 k + 1 k+1 k+1个
这些数生成的反序表组合为 ( k + 1 ) n − k − 1 − l e n (k+1)^{n-k-1-len} (k+1)n−k−1−len,再乘上前 k k k个数的组合
即: ( k + 1 ) ! ∗ ∑ i = 1 n − k − 1 ( k + 1 ) n − k − 1 − l e n ∗ ( n − k − l e n ) (k+1)!*\sum_{i=1}^{n-k-1}(k+1)^{n-k-1-len}*(n-k-len) (k+1)!∗∑i=1n−k−1(k+1)n−k−1−len∗(n−k−len)
-
这时的反序表有且仅有一个位置,其 s i > 1 s_i>1 si>1(严格大于)
e.g.
最后序列为2 3 1 4 5
,反序表为0 0 2 0 0
相当于原始 s i > k + 1 s_i>k+1 si>k+1,这个 i i i同样有范围限制,为 [ k + 3 , n ] [k+3,n] [k+3,n]
对于 i i i其选择方案为 i − 1 − ( k + 2 ) + 1 = i − k − 2 i-1-(k+2)+1=i-k-2 i−1−(k+2)+1=i−k−2
即: ∑ i = k + 3 n ∏ j = 1 n [ j ≠ i ] ( min ( j − 1 , k ) + 1 ) ⋅ [ j = i ] ( i − k − 1 ) \sum_{i=k+3}^n\prod_{j=1}^n[j≠i]\big(\min(j-1,k)+1\big)·[j=i](i-k-1) ∑i=k+3n∏j=1n[j=i](min(j−1,k)+1)⋅[j=i](i−k−1)
-
code
#include <cstdio>
#include <iostream>
using namespace std;
#define maxn 5005
#define int long long
int T, n, k, mod, fac;
int inv[maxn], mi[maxn];
int qkpow( int x, int y ) {
int ans = 1;
while( y ) {
if( y & 1 ) ans = ans * x % mod;
x = x * x % mod;
y >>= 1;
}
return ans;
}
signed main() {
scanf( "%lld", &T );
while( T -- ) {
scanf( "%lld %lld %lld", &n, &k, &mod );
fac = inv[1] = mi[0] = 1;
if( k >= n ) {
for( int i = 1;i <= n;i ++ )
fac = fac * i % mod;
printf( "%lld\n", fac );
continue;
}
for( int i = 1;i <= k + 1;i ++ )
fac = fac * i % mod;
for( int i = 2;i <= k + 1;i ++ )
inv[i] = ( mod - mod / i ) * inv[mod % i] % mod;
for( int i = 1;i <= n;i ++ )
mi[i] = mi[i - 1] * ( k + 1 ) % mod;
int ans = fac * mi[n - k - 1] % mod;
for( int i = 1;i <= n - k - 1;i ++ )
ans = ( ans + fac * ( n - k - i ) % mod * mi[n - k - 1 - i] ) % mod;
int mul = 1;
for( int i = 1;i <= n;i ++ )
mul = mul * ( min( i - 1, k ) + 1 ) % mod;
for( int i = k + 3;i <= n;i ++ )
ans = ( ans + mul * inv[min( i - 1, k ) + 1] % mod * ( i - k - 2 ) ) % mod;
printf( "%lld\n", ans );
}
return 0;
}