lyin场切黑题太强了
首先康托展开是用来求一个全排列的排名的东西。复杂度\(O(n^2)\),树状数组可以到\(O(n\log n)\)。板子
简单说一下原理:首先一个长为\(n\)的排列数是\(n!\)没什么问题。所以我们可以对于每一位考虑有当前位之后有多少排列要比该排列小。
举个例子:\(3,1,4,2,5\)这个排列,第一位\(3\),比它小的有\(1,2,\)所以当\(1,2\)做开头的排列有\(2\times 4!\)种,累加进答案,顺便顺序考虑下一位。这是外层的扫描。
为什么这个是\(O(n^2)\)的呢?考虑第三位\(4\)。我们发现,前面两位是\(3,1,\)已经固定了,所以第三位比\(4\)小的就只有\(2\)。所以我们要顺序扫描每个比当前数小的有没有用过。
形式的一个公式是\(\sum_{i=1}^nsum_{a_i}\times(n-i)!\),其中\(sum_{a_i}=\sum_{j>i}a_j<a_i\)。也就是说,\(sum\)就是我们之前说的“还没用过的数” 的个数,而后面那个阶乘就是排列数。
我们发现这个可以用树状数组优化。初始化树状数组全为\(1\),每用过一个数更新\(-1\),每次查找\(sum\)直接查找比它小的数的个数,也就是还没用过的数的个数。
最后我们求出了比当前排列小的排列数,再加1就是排名。
上个代码。
int main(){ scanf("%d",&n);jc[0]=1; for(int i=1;i<=n;i++){ scanf("%d",&a[i]);jc[i]=1ll*jc[i-1]*i%mod;//预处理阶乘 update(i,1);//初始化 } for(int i=1;i<=n;i++){ ans=(ans+1ll*query(a[i]-1)*jc[n-i]%mod)%mod;//直接按照定义累计答案 update(a[i],-1);//把a[i]删掉 } printf("%d",ans+1); }
另一个东西:逆康托展开,就是知道排名求原排列。
首先显然排名\(-1\)。然后设这是第\(i\)个数,当前排名为\(x\),则比它小的排列数就是\(\lfloor\frac x{(n-i)!}\rfloor\),由此我们可以知道第\(i\)位是什么数字,然后让\(x\)减去这个排名,在下一位重复这个操作。
和原来差不多,我们建一棵权值线段树,每个点存储区间内未用过的数的个数,然后就可以直接查询了。
void pushup(int rt){ tree[rt].num=tree[lson].num+tree[rson].num; } void build(int rt,int l,int r){ tree[rt].l=l;tree[rt].r=r; if(l==r){ tree[rt].num=1;return; } int mid=(l+r)>>1; build(lson,l,mid);build(rson,mid+1,r); pushup(rt); } int query(int rt,int pos){ if(tree[rt].l==tree[rt].r){ tree[rt].cnt=0;return tree[rt].l;//顺便更新 把这个数去掉 } int mid=(tree[rt].l+tree[rt].r)>>1,val=0; if(pos<=mid)val=query(lson,pos); else val=query(rson,pos); pushup(rt); return val; } int main(){ scanf("%d",&x); for(int i=1;i<=n;i++){ int ret=x/jc[n-i];//当前排名 printf("%d",query(1,ret+1));//我们找到了比它小的个数所以+1就是原数 x-=ret; } }