一个老生常谈的问题,给定一个两两不同字符的排列x,问其在所有排列情况中的排名。
很容易得到第一种暴力思路,先找到其所有字符集合的最小排列,再利用c++的next_permutation()函数不断寻找,直到找到原排列。思路清晰,代码简单,时间复杂度却是高达 O(n!) ,可以解决n<=11的情况(如蓝桥杯某年国赛的一道题),但是n一大就会暴死。
于是引入优化。设排列长度为n,先考虑原排列的第一个字符x1,设x在原排列中的排名为a1,显然,其存在使得原排序大于所有第一个字符小于x1的排列,这些排列的总数为(a1-1)*(n-1)! 。接下去考虑第二个字符x2,其排名为a2,故而其对答案的贡献为x1在第一位且第二位小于x2的所有排列总数。依此类推,第 i 位字符对答案的贡献为第 i 位右侧小于第 i 位字符个数乘以(n-i)! 。阶乘可以通过预处理得到,问题在于如何寻找第 i 位右侧小于第 i 位字符个数。考虑暴力求解,直接循环求解,时间复杂度为O(n^2),依然不够快。
继续优化,考虑使用线段树或者树状数组。首先明确问题的优化方向为加速寻找第 i 位右侧小于第 i 位字符个数。考虑离散化建立原排序对应出现次数的数组c,明显初始时c的值全部为1,查询第 i 位右侧小于第 i 位字符个数即为查询c数组下标1到x[i]-1的区间和,每次修改时将c[x[i]]置为0即可,这明显可使用线段树或者树状数组进行维护,因为其本质即单点修改,区间查询。这样,总时间复杂度为O(nlogn)。
代码如下:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=(1e6)+5; const int modd=998244353; ll c[maxn]; ll a[maxn]; ll tree[maxn]; int n; int lowbit(int x){ return x&(-x); } void update(int i,int x){ while(i<=n){ tree[i]=(tree[i]+x)%modd; i+=lowbit(i); } } ll query(int i){ ll res=0; while(i>0){ res=(res+tree[i])%modd; i-=lowbit(i); } return res; } ll f[maxn]; int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>c[i]; update(i,1); } ll ans=1; f[1]=1; for(int i=2;i<n;i++)f[i]=f[i-1]*i%modd; for(int i=1;i<=n;i++){ ans=(ans+query(c[i]-1)*f[n-i]%modd)%modd; update(c[i],-1); } cout<<ans<<endl; return 0; }
标签:字符,排列,小于,int,ll,maxn,展开,康托 From: https://www.cnblogs.com/yokino/p/17284345.html