【BZOJ5416】冒泡排序(NOI2018)-组合数学+树状数组
测试地址:冒泡排序
做法:本题需要用到组合数学+树状数组。
一道神题,用到的数学知识并没有难到哪里去,但成功把我这种弱菜区分掉了。
首先,交换次数能达到题目中给的下界的充要条件是,排列中不存在长度的下降子序列。因为要达到下界,每次交换都应该要“达到效果”,即两边的元素都往该去的方向移动。而一旦出现长度为的下降子序列,就一定存在一次交换,使得对一个元素没达到效果,因此也就达不到下界了。
转换了问题之后,我们需要找到构造出合法序列的方法。假设目前构造到第位,之前所填的数的最大值为,那么对于当前填的数,我们当然可以填任意一个大于的数,而对于小于的数,它必须是还没填的数中最小的数,否则令这个最小的数为,当前填的数为,排列中一定会存在子序列,也就不满足条件了。
先考虑没有任何限制的情况,令为还有个数要填,有个数比当前已填数中的最大值大(后面简称这种数为非限制元素),这样的总的方案数。由上面的分析可得:
边界条件为。
注意到是一个前缀和的形式,因此有:
边界条件为。
有了这个递推式,是不是就等于,从每次向右或向上走一个单位长度到的路径条数:呢?不是的,因为我们还限制了,所以路径不能跨越这条直线。这个就有点像卡特兰数了,事实上,没有任何限制的方案数的确就是卡特兰数,但是对于更一般的,我们也可以用类似的证明方法得到。
于是我们在预处理阶乘及逆元的情况下,可以计算一个了。
接下来我们考虑字典序的限制。不难看出,我们可以枚举前缀,然后求最长公共前缀为该前缀的合法排列数量,显然此时要填的数应该比给出的排列中的数大。我们又知道,填入的数应该同时比前面已填的最大数要大,因此这一位可填的数就一定要比这一位的前缀最大值大。前缀最大值很明显是可以预处理的,而要求某一个后缀中比某数大的数的数量显然用树状数组就可以了。
那么此时,如果前缀中已经出现了,即最大的数值,则后面剩下的数显然只能从小到大填了,而这种情况填出的排列显然字典序不会比给出的排列大,因此直接退出即可。
否则,假设当前在填第位,如果后面有个非限制元素可以填,那么就对答案有的贡献。根据的定义,这显然等于。前面已经介绍了计算这个的方法,因此直接累加即可。
上面就是对答案贡献的讨论,然而我们还需要判断一个前缀合不合法,这个就很容易了,有很多种方法判断,判断的根据在上面讲构造算法的时候已经说明了,利用树状数组可以做到判断。
于是我们就解决了这一题,时间复杂度为。
以下是本人代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=998244353;
int T,tot,n,a[600010],b[600010],c[600010],sum[600010];
ll fac[1200010],inv[1200010],invfac[1200010];
void extend(int limit)
{
for(ll i=tot+1;i<=limit;i++)
{
fac[i]=fac[i-1]*i%mod;
inv[i]=(mod-mod/i)*inv[mod%i]%mod;
invfac[i]=invfac[i-1]*inv[i]%mod;
}
}
int lowbit(int i)
{
return i&(-i);
}
void add(int x)
{
for(int i=x;i<=n;i+=lowbit(i))
sum[i]++;
}
int calc(int x)
{
int ans=0;
for(int i=x;i;i-=lowbit(i))
ans+=sum[i];
return ans;
}
ll C(int a,int b)
{
return fac[a]*invfac[b]%mod*invfac[a-b]%mod;
}
ll f(int a,int b)
{
return (C(a+b,b)-C(a+b,b-1)+mod)%mod;
}
int main()
{
scanf("%d",&T);
tot=1;
fac[0]=invfac[0]=fac[1]=invfac[1]=inv[1]=1;
for(int i=1;i<=T;i++)
{
scanf("%d",&n);
extend(n<<1);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
sum[i]=0;
for(int i=1;i<=n;i++)
{
b[i]=calc(a[i]);
add(a[i]);
}
for(int i=1;i<=n;i++)
sum[i]=0;
for(int i=n;i>=1;i--)
{
c[i]=n-i-calc(a[i]);
add(a[i]);
}
int now=n;
ll ans=0;
for(int i=1;i<=n;i++)
{
bool flag=0;
if (c[i]<now) now=c[i],flag=1;
if (now==0) break;
ans=(ans+f(n-i+1,now-1))%mod;
if (flag) continue;
if (b[i]==a[i]-1) continue;
break;
}
printf("%lld\n",ans);
}
return 0;
}