[提高组集训2021] 就差⼀点

一、题目

对一个长度为 \(n\) 的排列冒泡排序,给定参数 \(k\),问有多少排列排序后存在一个大小为 \(n-1\) 的上升子序列。

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\leq 5000\)

二、解法

首先介绍一下反序表,解决排列问题的又一利器 \(\tt get\)

定义:定义数组 \(d_i=\sum_{j=1}^{i}[p_j<p_i]\) 表示排列 \(p\) 的反序表,当然也可以定义成 \(d_{p_i}=\sum_{j=1}^i[p_j<p_i]\)

性质1:反序表和排列构成双射,因为反序表可以通过插入法构造出对应的排列。

性质2:一个排列被冒泡排序的次数是 \(\max d_i\)每次冒泡把 \(d_i\) 中的非 \(0\) 元素减 \(1\) 然后前移一位。


本题套用上面的结论即可,首先我们考虑能被排好序的排列个数怎么求,其实就是反序表前 \(k\) 个元素任意取值,后 \(n-k\) 个元素只能取 \([0,k]\),方案是 \(k!\times(k+1)^{n-k}\),我们把没被排好序的方案分成两种情况:

情况1:排序后反序表为:\(00..011110..00\),也就是中间出现了连续的一段 \(1\),那么这段 \(1\) 初始的元素值一定是 \(k+1\),我们枚举这一段 \(1\) 的长度,可以得到初始反序表的方案数是:

\[k!\sum_{i=1}^{n-k-1}(k+1)^{n-k-i}\times(n-k-i) \]

情况2:排序后反序表为:\(00..00x00..00\),其中 \(x>1\),我们枚举它初始的位置是 \(i+k\),那么初始反序表的方案数是:

\[k!\sum_{i=3}^{n-k}(i-2)\times(k+1)^{n-k-1} \]

把所有情况的方案加起来即可。

#include <cstdio>
#include <iostream>
using namespace std;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,m,k,pw[5005];
signed main()
{
	//freopen("almostthere.in","r",stdin);
	//freopen("almostthere.out","w",stdout);
	T=read();
	while(T--)
	{
		n=read();k=read();m=read();
		pw[0]=1;k=min(k,n);
		for(int i=1;i<=n;i++)
			pw[i]=pw[i-1]*(k+1)%m;
		int ans=pw[n-k];
		for(int i=1;i<n-k;i++)
			ans=(ans+(n-k-i)*pw[n-k-i])%m;
		for(int i=3;i<=n-k;i++)
			ans=(ans+(i-2)*pw[n-k-1])%m;
		for(int i=1;i<=k;i++)
			ans=ans*i%m;
		printf("%lld\n",ans);
	}
}
posted @ 2021-09-10 08:47  C202044zxy  阅读(50)  评论(0编辑  收藏  举报