[提高组集训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);
}
}