[2021-09-09 T2] 就差⼀点——冒泡排序和反序表之间不为人知的秘密

就差一点解题报告

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 n1的上升子序列

小明想知道,对于给定的 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 1n,k,T5000,108mod109+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=1j1dpi1,k+jdpi,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=1i1[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 00,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) isimin(i1,k)

    将这些值域限制乘起来就是不同的反序表个数,也就是不同的排列个数

    即: ∏ i = 1 n ( min ⁡ ( i − 1 , k ) + 1 ) \prod_{i=1}^n\Big(\min(i-1,k)+1\Big) i=1n(min(i1,k)+1)

  • 考虑冒泡排序后,最后的序列是一个长为 n − 1 n-1 n1的上升子序列(只差一点)

    • 这时的反序表形如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=nk1

      (不要忘记 s i < i s_i<i si<i的约束)

      考虑枚举这段 1 1 1的长度 l e n len len

      这段长度的选择方案相当于在总长为 n − k − 1 n-k-1 nk1中摆下 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 nk1len+1=nklen

      剩下的 n − k − 1 − l e n n-k-1-len nk1len个数反序表都不超过 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)nk1len,再乘上前 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=1nk1(k+1)nk1len(nklen)

    • 这时的反序表有且仅有一个位置,其 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 i1(k+2)+1=ik2

      即: ∑ 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+3nj=1n[j=i](min(j1,k)+1)[j=i](ik1)

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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值