[NOI2018] 冒泡排序
CL.[NOI2018] 冒泡排序
结论1.交换次数压到下界,当且仅当不存在长度大于 的下降子序列。
证明很简单。众所周知的是,冒泡排序的交换次数等于序列逆序对数。要压到下界,与每个点有关的逆序对数都只能为 ,因为从 到 的过程中本身就要交换 次。如果存在长度大于 的下降子序列的话,则逆序对就不仅是两两间贡献了,隔着中间一个数也能贡献,因而就压不到下界。
不存在长度大于 的下降子序列,等价于序列可以被划分为不超过 条上升子序列——我们抽出所有前缀 构成一条上升子序列,则剩余的东西必然也递增,不然如果出现递减的两个数,从前面挑一个前缀 就能构成长度为 的下降子序列。
要求出所有字典序 的排列 个数,常见套路是枚举它们有长度为 的前缀相同,且第 位 更大。
首先,我们可以判一下 的长度为 的前缀是否合法:首先,要确保其本身的非前缀 元素递增;其次,要确保其未填入的元素全部 最后的非前缀 元素。这样的话,就可以设计DP状态: 表示有 个 前缀 的元素、 个 前缀 的元素未填入时的方案数。则,,其中前半部分表示填入一个 的元素,显然为了保证递增只能填最小的一个;后半部分是枚举填入 的数中第 大的数填入,则前 个数都会被划归 的数。两个转移式可以被合并为 。到这里 分已经到手了,但我们还可以考虑优化。
如果在矩阵上画出来的话,会发现这转移的位置是一条斜线,不好处理;但是我们可以转变DP状态:设新的 ,其中 是原本的 (总序列长度), 是原本的 ( 的数的数量)。则有 。考虑换成枚举 ,得到 。到这里我们就发现之前贸然设的 的下界不合法,可能会出现负数,于是便重新设了 的下界。但是,更好的方式是,我们直接令 时 即可直接使用 的转移式。
下面我们考虑用此DP值来求答案。枚举前缀长度 ,并设 表示后缀 中有多少个数 。则此部分的贡献就是 。现在考虑有哪些 是合法的。设若位置 老老实实填上了 ,剩余后缀中 的数量是 。
发现,就算 并非前缀 ,即可以填入 的数时, 的数也不能填,因为所有未填的 的数中只能填最小的,而 一定不会比最小数还小,因此只能填入一个前缀最大值。故若 是前缀 ,贡献为 ;否则,即 并非前缀 ,填入 位置的前缀 会自动将 也归入非前缀 类中,故 还是至少会加一。所以两部分综合起来看,都是 。
发现,无论是转移式还是求值式,我们都需要用到DP数组的后缀和。于是我们设 表示 。
好耶!是大家daisuki的推式子时间!
OK!这样我们就得出了简单的后缀和公式!
现在考虑其实际意义:从 出发,每次要么向 正方向各走一步,要么向 的负方向走一步。同时,因为在合法的转移式中可能会出现 ,但不会出现更负的数,因此我们便规定不能越过 这条界线。
因为这个“不越过 的界线”长得很像卡特兰数模型,因此我们考虑令 全体增加 再说。于是现在 (原本有 ,现在加一得到了 )。两个起始状态觉得也不好做,因此我们再强制令全体 增加 。于是现在 。假如我们此时令 的话,就会发现由上述操作刚好可以得出 ,因此我们这个操作是正确的。
发现只有一种操作在 方向上有移动不好做。考虑让后一种操作也在 轴正方向上走一步。因为原本方案中 轴上动了恰好 步,故 轴正方向实际上也动了 步,则 轴负方向实际上动了 步;因为 轴负方向的移动现在同时在 轴正方向上移动,因此就多移动了 步,因此终点现在是 。
发现现在是裸的卡特兰数模型。于是就直接得到 的卡特兰数。
因为我们之前强制令 各增加了 ,因此 中的 要比上式中的 各少一,所以此时要把它补回去,因此有 。
时间复杂度可以做到 。同时,在依次处理 的每个前缀的时候,要记得判断后缀中所有元素是否都 非前缀 元素构成的序列中的 。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int N=1200000;
int T,n,q[1201000],fac[1201000],inv[1201000],premax,secmax,res,k,sufmin[1201000];
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int binom(int x,int y){if(x<y||y<0)return 0;return 1ll*fac[x]*inv[y]%mod*inv[x-y]%mod;}
int S(int x,int y){if(y>x)return 0;return(binom(2*x-y+1,x-y)-binom(2*x-y+1,x-y-1)+mod)%mod;}
int main(){
fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1ll*fac[i-1]*i%mod;
inv[N]=ksm(fac[N]);for(int i=N;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
scanf("%d",&T);
while(T--){
scanf("%d",&n),premax=secmax=res=k=0;
for(int i=1;i<=n;i++)scanf("%d",&q[i]);sufmin[n+1]=0x3f3f3f3f;
for(int i=n;i;i--)sufmin[i]=min(sufmin[i+1],q[i]);
for(int i=1;i<=n;i++){
if(secmax>sufmin[i])break;
if(q[i]>premax)k+=q[i]-premax-1,premax=q[i];else secmax=max(secmax,q[i]),k--;
(res+=S(n-i,k+1))%=mod;
}
printf("%d\n",res);
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?