排列数,康托展开及其线段树优化

进入正题前,先介绍一个计算排列的超好用的工具:

next_permutation()和prev_permutation()

这两个函数定义在algorithm库中

参数和sort类似,传入三个参数,数组首位,数组末位的后一个,以及比较函数,当比较函数省略时,默认为less

功能是生成当前这个数组里的排列的下一个或上一个,“下一个/上一个”是基于由比较函数定义出的字典序。

允许元素重复,比如1,1,2,1,1,调用next_permutation()后生成的是1,2,1,1,1

这两个函数最优时间复杂度是O(1),最坏时间复杂度是O(n),平均时间复杂度是O(n)

康托展开,则是直接算出某一个排列是排第几个的。还可以直接计算出,排第几个的是哪个排列,叫做逆康托展开。

(注意康托展开对于有重复元素的情况不成立,直接套用下方式子显然不对,而笔者暂时也未找到相关文献或自己想出,证明含有重复元素的情况能用类似康托展开的方法或其他简单方法求解。如果您对此有想法,恳请在下方留言区告诉我,我会非常感激。)

(longlong最多能存下20!,21!就爆了,所以说逆康托展开只在20个元素以下的排列有用。但是康托展开的值可以取模,用处还是不小的。)

康托展开非常有意思,它巧妙地利用了排列的后缀也是一个排列的性质。对于一个排列,假设它的长度是len,那么对于第i位p[i],它去找第i+1~len位,所有比它小的数,假设有a个,那么,在这个排列前面比它小的一定有a*(len-i)!个。

为什么呢?因为第i+1~len位它们其实也构成了一个排列,这些元素共能构成(len-i)!个排列,又因为第i+1~len位中比p[i]小的数字,在所求的这个排列之前的排列中,各自充当了一次第i位,因此在所求排列前面比它小的一定有a*(len-i)!个。

再去求每一位,把每一位的答案加起来,即是比此排列小的排列共有多少个的答案。(升序排列的rank是0)

逆康托展开原理类似,只要每次用(len-i)!取模,构造出a,再一个一个构造,使得后面比这一位小的数正好是a个即可。

#include<iostream>
#define MAXN 20 
#define LL long long
using namespace std;
LL fact[MAXN+5];
void make_fact(){
    fact[0]=1;
    for(int i=1;i<=MAXN;i++){
        fact[i]=fact[i-1]*i;
    }
}
LL Cantor_exp(int *p,int len){
    LL rank=0;
    for(int i=1;i<len;i++){
        int a=0;
        for(int j=i+1;j<=len;j++){
            if(p[j]<p[i])a++;
        }
        rank+=a*fact[len-i];
    }
    return rank;
    
}
void Cantor_invexp(int *p,int len,LL rank){
    int temp[len];
    for(int i=0;i<len;i++){
        temp[i]=i+1;
    }
    for(int i=1;i<=len;i++){
        int a=rank/fact[len-i];
        rank%=fact[len-i];
        for(int j=0;j<len;j++){
            if(a==0 && temp[j]>0){
                p[i]=temp[j];
                temp[j]=0;
                break;
            }else if(temp[j]>0){
                a--;
            }
        }
    }
    return ;
}
int main(){
    make_fact();
    int p[6]={0,3,4,1,5,2};
    printf("%lld
",Cantor_exp(p,5)); 
    Cantor_invexp(p,5,1);
    for(int i=1;i<=5;i++){
        printf("%d ",p[i]);
    }
    printf("
");
    return 0;
} 

由于康托展开有找第i+1~len位,所有比它小的数的操作,因此如果暴力找的话时间复杂度是$O(n^2)$,用线段树优化可以使得其时间复杂度降到$O(nlogn)$,将这个查找操作由暴力改为在线段树上求和即可,线段树根节点存储的信息是此点下标对应的值是否在[i+1,len]这个区间出现过,每找一个就更新一次。

#include<iostream>
#define MAXN 20 
#define LL long long
using namespace std;
LL fact[MAXN+5];
struct Node{
    int l,r;
    LL sum;
}node[MAXN*4+10]; 
void build(int l,int r,int x){
    node[x].l=l;
    node[x].r=r;
    if(l==r){
        node[x].sum=1;
        return ;
    }else{
        int mid=(l+r)/2;
        build(l,mid,x*2);
        build(mid+1,r,x*2+1);
    }
    node[x].sum=node[2*x].sum+node[2*x+1].sum;
    return ;
}
LL query(int l,int r,int x){
    if(l<=node[x].l && node[x].r<=r)return node[x].sum;
    if(node[x].r<l || r<node[x].l)return 0;
    LL ans=0;
    if(l<=node[x*2].r)ans+=query(l,r,2*x);
    if(node[x*2+1].l<=r)ans+=query(l,r,2*x+1);
    return ans;
}
void change(int id,int num,int x){
    if(node[x].l==node[x].r){
        node[x].sum=num;
        return ;
    }
    if(id<=node[x*2].r){
        change(id,num,x*2);
    }else{
        change(id,num,x*2+1);
    }
    node[x].sum=node[x*2].sum+node[x*2+1].sum;
    return ;
}

void make_fact(){
    fact[0]=1;
    for(int i=1;i<=MAXN;i++){
        fact[i]=fact[i-1]*i;
    }
}
LL Cantor_exp(int *p,int len){
    LL ans=0;
    build(1,len,1);
    for(int i=1;i<len;i++){
        change(p[i],0,1);
        ans+=1LL*query(1,p[i]-1,1)*fact[len-i];
    }
    return ans;
}
int main(){
    make_fact();
    int p[6]={0,3,4,1,5,2};
    printf("%lld
",Cantor_exp(p,5)); 
//    Cantor_invexp(p,5,1);
//    for(int i=1;i<=5;i++){
//        printf("%d ",p[i]);
//    }
//    printf("
");
    return 0;
} 
原文地址:https://www.cnblogs.com/isakovsky/p/11294346.html