zoukankan      html  css  js  c++  java
  • [NOI2018] 冒泡排序

    CL.[NOI2018] 冒泡排序

    结论1.交换次数压到下界,当且仅当不存在长度大于 \(2\) 的下降子序列。

    证明很简单。众所周知的是,冒泡排序的交换次数等于序列逆序对数。要压到下界,与每个点有关的逆序对数都只能为 \(|i-p_i|\),因为从 \(i\)\(p_i\) 的过程中本身就要交换 \(|i-p_i|\) 次。如果存在长度大于 \(2\) 的下降子序列的话,则逆序对就不仅是两两间贡献了,隔着中间一个数也能贡献,因而就压不到下界。

    不存在长度大于 \(2\) 的下降子序列,等价于序列可以被划分为不超过 \(2\) 条上升子序列——我们抽出所有前缀 $ \max$ 构成一条上升子序列,则剩余的东西必然也递增,不然如果出现递减的两个数,从前面挑一个前缀 $ \max $ 就能构成长度为 \(3\) 的下降子序列。

    要求出所有字典序 \(>q\) 的排列 \(p\) 个数,常见套路是枚举它们有长度为 \(i-1\) 的前缀相同,且第 \(i\)\(p\) 更大。

    首先,我们可以判一下 \(q\) 的长度为 \(i-1\) 的前缀是否合法:首先,要确保其本身的非前缀 $ \max$ 元素递增;其次,要确保其未填入的元素全部 \(>\) 最后的非前缀 $ \max$ 元素。这样的话,就可以设计DP状态:\(f_{i,j}\) 表示有 \(i\)\(<\) 前缀 $ \max$ 的元素、\(j\)\(>\) 前缀 $ \max $ 的元素未填入时的方案数。则,\(f_{i,j}=f_{i-1,j}+\sum\limits_{k=1}^jf_{i+k-1,j-k}\),其中前半部分表示填入一个 \(<\max\) 的元素,显然为了保证递增只能填最小的一个;后半部分是枚举填入 \(>\max\) 的数中第 \(k\) 大的数填入,则前 \(k-1\) 个数都会被划归 \(<\max\) 的数。两个转移式可以被合并为 \(f_{i,j}=\sum\limits_{k=0}^jf_{i+k-1,j-k}\)。到这里 \(80\) 分已经到手了,但我们还可以考虑优化。

    如果在矩阵上画出来的话,会发现这转移的位置是一条斜线,不好处理;但是我们可以转变DP状态:设新的 \(f_{i,j}\),其中 \(i\) 是原本的 \(i+j\)(总序列长度),\(j\) 是原本的 \(i\)\(< \max\) 的数的数量)。则有 \(f_{i,j}=\sum\limits_{k=0}^{i-j}f_{i-1,j+k-1}\)。考虑换成枚举 \(j+k-1\),得到 \(f_{i,j}=\sum\limits_{k=\max(0,j-1)}^{i-1}f_{i-1,k}\)。到这里我们就发现之前贸然设的 \(k=0\) 的下界不合法,可能会出现负数,于是便重新设了 \(\max(0,j-1)\) 的下界。但是,更好的方式是,我们直接令 \(j<0\)\(f_{i,j}=0\) 即可直接使用 \(f_{i,j}=\sum\limits_{k=j-1}^{i-1}f_{i-1,k}\) 的转移式。

    下面我们考虑用此DP值来求答案。枚举前缀长度 \(i\),并设 \(j\) 表示后缀 \([i+1,n]\) 中有多少个数 \(< \max\)。则此部分的贡献就是 \(\sum\limits_j f_{n-i,j}\)。现在考虑有哪些 \(j\) 是合法的。设若位置 \(i\) 老老实实填上了 \(q_i\),剩余后缀中 \(<\max\) 的数量是 \(k\)

    发现,就算 \(q_i\) 并非前缀 $ \max $,即可以填入 \(<\max\) 的数时,\(<\max\) 的数也不能填,因为所有未填的 \(<\max\) 的数中只能填最小的,而 \(q_i\) 一定不会比最小数还小,因此只能填入一个前缀最大值。故若 \(q_i\) 是前缀 $ \max $,贡献为 \(\sum\limits_{j=k+1}^{n-i}f_{n-i,j}\);否则,即 \(q_i\) 并非前缀 $ \max $,填入 \(i\) 位置的前缀 $ \max $ 会自动将 \(q_i\) 也归入非前缀 $ \max $ 类中,故 \(k\) 还是至少会加一。所以两部分综合起来看,都是 \(\sum\limits_{j=k+1}^{n-i}f_{n-i,j}\)

    发现,无论是转移式还是求值式,我们都需要用到DP数组的后缀和。于是我们设 \(S_{n,m}\) 表示 \(\sum\limits_{i=m}^nf_{n,i}\)

    好耶!是大家daisuki的推式子时间!

    \[\begin{aligned}S_{n,m}&=\sum\limits_{i=m}^nf_{n,i}\\&=\sum\limits_{i=m}^n\sum\limits_{j=i-1}^{n-1}f_{n-1,j}\\&=\sum\limits_{j=m-1}^{n-1}\sum\limits_{i=m}^{j+1}f_{n-1,j}\\&=\sum\limits_{j=m-1}^{n-1}f_{n-1,j}+\sum\limits_{j=m-1}^{n-1}\sum\limits_{i=m+1}^{j-1}f_{n-1,j}\\&=S_{n-1,m-1}+\sum\limits_{i=m+1}^j\sum\limits_{j=i-1}^{n-1}f_{n-1,j}\\&=S_{n-1,m-1}+\sum\limits_{i=m+1}^jf_{n,i}\\&=S_{n-1,m-1}+S_{n,m+1}\end{aligned} \]

    OK!这样我们就得出了简单的后缀和公式!

    现在考虑其实际意义:从 \(S_{0,0}=1\) 出发,每次要么向 \(n,m\) 正方向各走一步,要么向 \(m\) 的负方向走一步。同时,因为在合法的转移式中可能会出现 \(S_{n,-1}\),但不会出现更负的数,因此我们便规定不能越过 \(m=-1\) 这条界线。

    因为这个“不越过 \(-1\) 的界线”长得很像卡特兰数模型,因此我们考虑令 \(m\) 全体增加 \(1\) 再说。于是现在 \(S_{0,0}=S_{0,1}=1\)(原本有 \(S_{0,-1}=1\),现在加一得到了 \(S_{0,0}=1\))。两个起始状态觉得也不好做,因此我们再强制令全体 \(n\) 增加 \(1\)。于是现在 \(S_{1,0}=S_{1,1}=1\)。假如我们此时令 \(S_{0,0}=1\) 的话,就会发现由上述操作刚好可以得出 \(S_{1,0}=S_{1,1}=1\),因此我们这个操作是正确的。

    发现只有一种操作在 \(n\) 方向上有移动不好做。考虑让后一种操作也在 \(n\) 轴正方向上走一步。因为原本方案中 \(n\) 轴上动了恰好 \(n\) 步,故 \(m\) 轴正方向实际上也动了 \(n\) 步,则 \(m\) 轴负方向实际上动了 \(n-m\) 步;因为 \(m\) 轴负方向的移动现在同时在 \(n\) 轴正方向上移动,因此就多移动了 \(n-m\) 步,因此终点现在是 \((2n-m,m)\)

    发现现在是裸的卡特兰数模型。于是就直接得到 \(\dbinom{2n-m}{n-m}-\dbinom{2n-m}{n-m-1}\) 的卡特兰数。

    因为我们之前强制令 \(n,m\) 各增加了 \(1\),因此 \(S_{n,m}\) 中的 \(n,m\) 要比上式中的 \(n,m\) 各少一,所以此时要把它补回去,因此有 \(S_{n,m}=\dbinom{2n-m+1}{n-m}-\dbinom{2n-m+1}{n-m-1}\)

    时间复杂度可以做到 \(O(n)\)。同时,在依次处理 \(q\) 的每个前缀的时候,要记得判断后缀中所有元素是否都 \(>\) 非前缀 $ \max $ 元素构成的序列中的 $ \max $。

    代码:

    #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;
    }
    
  • 相关阅读:
    MySql多表循环遍历更新
    GridView控件的选择功能,代码实现CheckBox控件的全选、反选以及取消
    使用HTTP POST请求12306网站接口查询火车车次API
    GridView控件的绑定分页功能
    使用HTTP GET请求12306网站接口获取车站名和车站Code
    浅谈从Oracle数据库中取出10条数据的Select语句与Sql Server、MySql的区别
    2022 程序员口语提升指南
    R语言与java整合
    新浪的股票接口 c# (收藏)
    摘记
  • 原文地址:https://www.cnblogs.com/Troverld/p/14601744.html
Copyright © 2011-2022 走看看