• 康托展开(Cantor-Expansion)学习笔记


    康托展开(Cantor-Expansion)学习笔记

    参考资料

    何为康托展开

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

    实际上,康托展开就是给出一个全排列,求它是第几个全排列。

    而逆康托展开就是反过来,给出全排列长度和全排列排名,求这个全排列的每一个元素。

    实际上,康托展开除了在全排列方面有作用之外,还可以被用来当做一种数列hash,把一整个数列hash成一个值,这样可以大大的节省空间。

    如何康托展开

    暴力康托展开

    我们对于一个全排列,第i位数有n+1-i种选择,如果用变进制数表示,这一位的进制就是n+1-i

    换句话说,如果第i位选择了第k种情况,那么对应的变进制数就是k

    这里我们为了方便,以0为起点做下标。

    举个例子,排列:1 4 5 3 2转换成变进制数就是unknown-(02210).

    • 1是排列1 2 3 4 5中的第一项,变进制数为0.
    • 4是排列2 3 4 5中的第三项,变进制数为2.
    • 5是排列2 3 5中的第三项,变进制数为2.
    • 3是排列2 3中的第二项,变进制数为1.
    • 2是排列2中的第一项,变进制数为0.

    可以发现,变进制数的第i位的值就是(a_i - sum_{j=0}^{i-1} [a_j<a_i] +1).

    上面这句话的意思是变进制数的第i位的值就是这个数减去它左边比它小的数再+1.

    其实这就是康托展开的定义式。

    完整的式子:

    [ans = 1 + sum_{i=1}^n (sum_{j=i}^n [a_i < a_j]) imes (n - i)! ]

    然后我们暴力做就可以了,用一个used[i]数组来表示第i位是否已经被加入变进制序列,然后把变进制结果转换成十进制就可以了。

    代码

    这里我们以LuoguP5367 - 【模板】康托展开为例

    // luogu-judger-enable-o2
    /* Headers */
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<cctype>
    #include<algorithm>
    #include<vector>
    #include<queue>
    #include<stack>
    #include<climits>
    #include<iostream>
    #include<map>
    #define FOR(i,a,b,c) for(int i=(a);i<=(b);i+=(c))
    #define ROF(i,a,b,c) for(int i=(a);i>=(b);i-=(c))
    #define FORL(i,a,b,c) for(long long i=(a);i<=(b);i+=(c))
    #define ROFL(i,a,b,c) for(long long i=(a);i>=(b);i-=(c))
    #define FORR(i,a,b,c) for(register int i=(a);i<=(b);i+=(c))
    #define ROFR(i,a,b,c) for(register int i=(a);i>=(b);i-=(c))
    #define lowbit(x) x&(-x)
    #define LeftChild(x) x<<1
    #define RightChild(x) (x<<1)+1
    #define RevEdge(x) x^1
    #define FILE_IN(x) freopen(x,"r",stdin);
    #define FILE_OUT(x) freopen(x,"w",stdout);
    #define CLOSE_IN() fclose(stdin);
    #define CLOSE_OUT() fclose(stdout);
    #define IOS(x) std::ios::sync_with_stdio(x)
    #define Dividing() printf("-----------------------------------
    ");
    /* definitions */
    const int MAXN = 1e6 + 1;
    const int mod = 998244353;
    int n,a[MAXN],used[MAXN];
    long long ans;
    /* functions */
    int main(int argc,char *argv[]){
        scanf("%d",&n);
        FOR(i,1,n,1){
            scanf("%d",&a[i]);
            int tmp = a[i];
            FOR(j,1,a[i],1) tmp -= used[j];
            used[a[i]] = 1;
            a[i] = tmp - 1;//暴力计算变进制数
        }
        FOR(i,1,n-1,1) ans = (ans + a[i]) * (n - i) % mod;//转换成十进制
        printf("%lld",ans+1);//最后不要忘了+1
        return 0;
    }
    

    树状数组康托展开

    我们发现,之前的康托展开是两重循环,时间复杂度(O(n^2))

    (n geq 10^4)时,暴力康托展开就承受不起了。

    那么我们把used[]数组的求解过程用树状数组来维护前缀和,然后就可以用log的时间求出左侧小于自己的数的个数了。(没用线段树的原因是太难写了)

    代码

    // luogu-judger-enable-o2
    /* Headers */
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<cctype>
    #include<algorithm>
    #include<vector>
    #include<queue>
    #include<stack>
    #include<climits>
    #include<iostream>
    #include<map>
    #define FOR(i,a,b,c) for(int i=(a);i<=(b);i+=(c))
    #define ROF(i,a,b,c) for(int i=(a);i>=(b);i-=(c))
    #define FORL(i,a,b,c) for(long long i=(a);i<=(b);i+=(c))
    #define ROFL(i,a,b,c) for(long long i=(a);i>=(b);i-=(c))
    #define FORR(i,a,b,c) for(register int i=(a);i<=(b);i+=(c))
    #define ROFR(i,a,b,c) for(register int i=(a);i>=(b);i-=(c))
    #define lowbit(x) x&(-x)
    #define LeftChild(x) x<<1
    #define RightChild(x) (x<<1)+1
    #define RevEdge(x) x^1
    #define FILE_IN(x) freopen(x,"r",stdin);
    #define FILE_OUT(x) freopen(x,"w",stdout);
    #define CLOSE_IN() fclose(stdin);
    #define CLOSE_OUT() fclose(stdout);
    #define IOS(x) std::ios::sync_with_stdio(x)
    #define Dividing() printf("-----------------------------------
    ");
    /* definitions */
    const int MAXN = 1e6 + 1;
    const int mod = 998244353;
    long long n,a[MAXN],used[MAXN],Tree[MAXN];
    long long ans;
    /* functions */
    inline void Update(int x,int d){
        while(x <= n){
            Tree[x] += d;
            x += lowbit(x);
        }
    }
    inline long long Query(int x){
        long long ans = 0;
        while(x > 0){
            ans += Tree[x];
            x -= lowbit(x);
        }
        return ans;
    }
    int main(int argc,char *argv[]){
        used[0] = 1;
        FORL(i,1,MAXN,1) used[i] = (used[i-1] * i) % mod;
        scanf("%lld",&n);
        FOR(i,1,n,1) scanf("%d",&a[i]), Update(i,1);
        ans = 1;
        FOR(i,1,n,1){
            ans = (ans + (used[n - i] * Query(a[i] - 1)) % mod) % mod;
            Update(a[i],-1);
        }
        printf("%lld
    ",ans);
        return 0;
    }
    

    逆康托展开

    我们先把rank--作为第一轮的被除数。

    然后从第1位到第n位依次循环,去除以(n-1)!,用商找答案,用余数作为下一轮的被除数。

    举个例子,求5项数列中排名第5的全排列:

    (5-1=4)

    • 4 / (5 - 1)! = 0 …… 4,所以第一个数是当前未出现的第0个数:1
    • 4 / (4 - 1)! = 0 …… 4,所以第二个数是当前未出现的第0个数:2
    • 4 / (3 - 1)! = 2 …… 0,所以第三个数是当前未出现的第2个数:5
    • 0 / (2 - 1)! = 0 …… 0,所以第四个数是当前未出现的第0个数:4
    • 0 / (1 - 1)! = 0 …… 0,所以第五个数是当前未出现的第0个数:3

    所以答案就是:1,2,5,4,3

    代码

    /* Headers */
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<cctype>
    #include<algorithm>
    #include<vector>
    #include<queue>
    #include<stack>
    #include<climits>
    #include<iostream>
    #include<map>
    #define FOR(i,a,b,c) for(int i=(a);i<=(b);i+=(c))
    #define ROF(i,a,b,c) for(int i=(a);i>=(b);i-=(c))
    #define FORL(i,a,b,c) for(long long i=(a);i<=(b);i+=(c))
    #define ROFL(i,a,b,c) for(long long i=(a);i>=(b);i-=(c))
    #define FORR(i,a,b,c) for(register int i=(a);i<=(b);i+=(c))
    #define ROFR(i,a,b,c) for(register int i=(a);i>=(b);i-=(c))
    #define lowbit(x) x&(-x)
    #define LeftChild(x) x<<1
    #define RightChild(x) (x<<1)+1
    #define RevEdge(x) x^1
    #define FILE_IN(x) freopen(x,"r",stdin);
    #define FILE_OUT(x) freopen(x,"w",stdout);
    #define CLOSE_IN() fclose(stdin);
    #define CLOSE_OUT() fclose(stdout);
    #define IOS(x) std::ios::sync_with_stdio(x)
    #define Dividing() printf("-----------------------------------
    ");
    /* definitions */
    const int MAXN = 1e6 + 1;
    long long n,rank,ans[MAXN];
    /* functions */
    inline long long factor(int t){
        if(t == 0 || t == 1) return 1;
        else return factor(t - 1) * t;
    }
    inline int recantor(int rank,int len){
        rank--;
        int m = 1,ans = 0;
        bool *book = new bool [len + 1];
        FOR(i,1,len,1) book[i] = true;
        ROF(i,len,2,1){
            int t = factor(i - 1);
            m = rank/t + 1;
            while(book[m] == 0) m++;
            book[m] = 0; ans *= 10; ans += m; num %= t;
        }
        m = 1;
        while(book[m] == 0)m++;
        ans *= 10; ans += m;
        delete [] book;
        return ans;
    }
    int main(int argc,char *argv[]){
        scanf("%lld%lld",&n,&rank);
        printf("%d",recantor(rank,n));
        return 0;
    }
    

    康托展开的应用

    康托展开其实可以当做一种变形的hash,它可以把一整个数列映射成一个数,且不会出错。

    所以我们在遇到一些题目需要映射数列的时候,就可以利用康托展开来压缩空间。

    例题

    THE END

  • 相关阅读:
    在MyEclipse中设置Source folders和output folder
    在Myeclipse中设置源码和webroot目录
    将svn下载的工程转化为web工程
    位运算
    maxSequence
    krusual C++
    Dijkstra And Floyd C++
    Graph And Trave
    Set
    Tree
  • 原文地址:https://www.cnblogs.com/herself32-lyoi/p/11138374.html
Copyright © 2020-2023  润新知