zoukankan      html  css  js  c++  java
  • 「算法笔记」康托展开

    一、引入

    以前从来没听过“康托展开”的 myt 在做「NOIP2018 提高组」初赛卷的时候,碰到了一道需要用到康托展开的题,于是就有了以下内容。

    那么康托展开是什么呢?康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。(摘自百度百科

    通俗地说,康托展开可以用来求一个 (1sim n) 的任意排列的排名(把 (1sim n) 的所有排列按字典序排序,这个排列的位次就是它的排名)。

    康托展开可以在 (O(n^2)) 的复杂度内求出一个排列的排名,在用到树状数组优化时可以做到 (O(nlog n))

    二、康托展开

    1. 公式

    先给出康托展开的公式:

    (X=a_1(n−1)!+a_2(n−2)!+cdots+a_ncdot 0!)

    其中,(a_i) 为整数,并且 (0leq a_i<i,1leq ileq n)

    (X) 表示康托展开的结果。(a_i) 表示在当前未出现的元素中比第 (i) 个数小的数的个数,后面乘的是还剩下的数的个数的阶乘。

    2. 例子

    举个栗子:求长为 (5) 的排列 ([5,2,4,1,3]) 的排名。

    1. 首先,若第一个数已经确定,那么还有 (4) 个数没有确定,则共有 (4!) 种排列。而小于 (5) 的数开头的排列都会比 (5) 开头的排列的字典序要小。令 (1,2,3,4) 分别作为第一个数,也就是说第一个数有 (4) 种选择,所以共有 (4 imes 4!) 种排列。这 (4 imes 4!) 种排列都比 ([5,2,4,1,3]) 的字典序小。

    2. 再来看第二位。由于第一位 (5) 已经出现过了,所以还剩下 ({1,2,3,4})。第一位 (5) 已经确定,接下来小于 (2) 的数作为第二位的排列都会比 (2) 作为第二位的排列的字典序要小(第一位都是 (5))。({1,2,3,4}) 中只有 (1) 个数比 (2) 小。与上一步同理,这时候又有 (1 imes 3!) 种排列比 ([5,2,4,1,3]) 的字典序小。

    3. 然后看第三位。还剩下 ({1,3,4}),其中比 (4) 小的有 (2) 个。那么有 (2 imes 2!) 种排列比 ([5,2,4,1,3]) 的字典序小。

    4. 接下来看第四位。还剩下 ({1,3}),其中比 (1) 小的有 (0) 个。则有 (0 imes 1!) 种排列比 ([5,2,4,1,3]) 的字典序小。

    5. 最后再看第五位。此时有 (0 imes 0!) 种排列比 ([5,2,4,1,3]) 的字典序小。

    (4!)(3!)(2!)(1!)(0!)
    (24) (6) (2) (1) (0)

    综上所述,共有 (4 imes 4!+1 imes 3!+2 imes 2!+0 imes 1!+0 imes 0!=106) 种排列比 ([5,2,4,1,3]) 的字典序小。则 ([5,2,4,1,3]) 的排名为 (107)

    int solve(){
        int ans=0;
        for(int i=1;i<=n;i++){
            int cnt=0;    //在当前未出现的元素中比第 i 个数小的数的个数
            for(int j=i+1;j<=n;j++)    //当前未出现的元素:a[(i+1)~n] 
                if(a[j]<a[i]) cnt++;
            ans+=cnt*f[n-i];    //f[i]=i! 
        }
        return ans+1;    //当前算出的是,比给出排列字典序小的排列数,所以还要 +1 
    }

    三、逆康托展开

    逆康托展开可以计算这样的问题:给出一个 (1sim n) 的排列的排名,求这个排列。

    举个栗子:已知一个 (1sim 5) 的排列 (A) 的排名为 (107),求 (A)

    首先,(A) 的排名为 (107),那么共有 (106) 种排列比 (A) 的字典序小。

    1. (lfloor frac{106}{4!} floor=4),则有 (4) 个数比 (A) 的第一位小,则第一位为 (5)

    2. 还剩下 (106-4 imes 4!=10) 种排列比这个排列的字典序小((frac{106}{4!}=4 cdotscdots 10))。由于第一位 (5) 已经出现过了,所以还剩下 ({1,2,3,4})(lfloor frac{10}{3!} floor=1),则在 ({1,2,3,4}) 中有 (1) 个数比 (A) 的第二位小。则第二位为 (2)

    3. (10-1 imes 3!=4)(lfloorfrac{4}{2!} floor=2),则在 ({1,3,4}) 中有 (2) 个数比 (A) 的第三位小。则第三位为 (4)

    4. (4-2 imes 2!=0)(lfloorfrac{0}{1!} floor=0),则在 ({1,3}) 中有 (0) 个数比 (A) 的第四位小。则第三位为 (1)

    5. (0-0 imes 1!=0)(lfloorfrac{0}{0!} floor=0),可得第三位为 (3)

    所以 (A=[5,2,4,1,3])

    void solve(int n,int k){    //已知一个 1~n 的排列 A 的排名为 k,求 A
        memset(vis,0,sizeof(vis)),k--;    //有 k-1 种排列比 A 的字典序小 
        for(int i=1;i<=n;i++){
            int cnt=k/f[n-i];    //有 cnt 个数比 A 的第 i 位小 
            for(int j=1;j<=n;j++)
                if(!vis[j]){
                    if(!cnt){a[i]=j,vis[j]=1;break;}
                    cnt--; 
                }
            k%=f[n-i];    //还剩下几种排列比 A 的字典序小 
        }
    }

    四、树状数组优化

    1. 树状数组优化康托展开

    思考康托展开的过程。先不管乘上阶乘的部分,考虑优化求当前未出现的元素中比第 (i) 个数小的数的个数(当前有多少个小于它的数还没有出现)的做法。

    考虑用树状数组维护。具体来说,初始时我们把树状数组中的每个数设为 (1),表示这个数还没有出现。如果某个数出现过了,就把它变成 (0)。最后查询 (1sim a_{i-1}) 有多少个 (1) 就行了(因为比它小的数都在它的前面,并且出现过的数的值都为 (0))。

    Luogu P5367 【模板】康托展开 代码:

    #include<bits/stdc++.h>
    #define int long long
    #define lowbit(x) x&(-x)
    using namespace std;
    const int N=1e6+5,mod=998244353;
    int n,a[N],f[N],c[N],ans;
    void modify(int x,int y){
        for(int i=x;i<=n;i+=lowbit(i))
            c[i]=(c[i]+y)%mod;
    }
    int query(int x){
        int ans=0;
        for(int i=x;i;i-=lowbit(i))
            ans=(ans+c[i])%mod;
        return ans;
    }
    signed main(){
        scanf("%lld",&n),f[0]=1;
        for(int i=1;i<=n;i++)
            f[i]=(f[i-1]*i)%mod;
        for(int i=1;i<=n;i++)
            scanf("%lld",&a[i]),modify(a[i],1);    //初始时每个数都设为 1,表示这个数还没有出现 
        for(int i=1;i<=n;i++)
            modify(a[i],-1),ans=(ans+query(a[i]-1)*f[n-i]%mod)%mod;    //如果某个数出现过了,就把它变成 0。查询 1~a[i-1] 中 1 的个数。 
        printf("%lld
    ",ans+1);
        return 0;
    }

    2. 树状数组+二分优化逆康托展开

    把在未选数中找到第 cnt+1 大的数的过程改成二分+树状数组即可。

    #include<bits/stdc++.h>
    #define int long long
    #define lowbit(x) x&(-x)
    using namespace std; 
    const int N=1e6+5;
    int n,k,a[N],f[N],c[N];
    void modify(int x,int y){
        for(int i=x;i<=n;i+=lowbit(i))
            c[i]+=y;
    } 
    int query(int x){
        int ans=0;
        for(int i=x;i;i-=lowbit(i))
            ans+=c[i];
        return ans;
    }
    void solve(int n,int k){
        k--;
        for(int i=1;i<=n;i++) modify(i,1);    //初始时每个数都设为 1,表示这个数还没有出现 
        for(int i=1;i<=n;i++){
            int cnt=k/f[n-i],l=1,r=n,ans;
            while(l<=r){    //二分求,未出现过的数中,有 cnt 个数比它小的数
                int mid=(l+r)/2;
                if(query(mid)-1>=cnt) ans=mid,r=mid-1;    //query(mid)-1 即查询 mid 前面有几个数还没有出现。这里若用 query(mid-1) 会出锅,因为可能会取到之前出现过的数。 
                else l=mid+1;
            }
            a[i]=ans,k%=f[n-i],modify(a[i],-1);    //如果某个数出现过了,就把它变成 0
        }
    }
    signed main(){
        scanf("%lld%lld",&n,&k),f[0]=1;
        for(int i=1;i<=n;i++) f[i]=f[i-1]*i;
        solve(n,k);
        for(int i=1;i<=n;i++)
            printf("%lld%c",a[i],i==n?'
    ':' ');
        return 0;
    }
  • 相关阅读:
    Centos给php安装cassandra扩展
    树莓派配置文档 config.txt 说明(转)
    ubuntu远程桌面连接windows系统(转)
    Shell学习笔记
    linux终端terminal个性化配置(转)
    ubuntu14.04安装bodhi桌面系统后,unity启动界面改变,如何还原
    man curl_easy_perform(原创)
    man curl_easy_setopt(原创)
    树莓派 config.txt
    使用dd命令克隆整个系统(转)
  • 原文地址:https://www.cnblogs.com/maoyiting/p/13678641.html
Copyright © 2011-2022 走看看