数据结构——康托展开与逆康托展开

含义

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

原理

  X = s1(n-1)! + s2(n-2)! + s3(n-3)! + …… + sn-1 * 1! + sn * 0!

  其中si表示在第i位右边比ai小的数的个数。

  我们现在用sl表示第i位左边比ai小的数的个数,sr表示第i位右边比ai小的数的个数,显然可以得到如下等式:
  ai = sl + sr + 1

  故公式中的si可以用上述等式计算:sr = ai - sl -1

  依照上述原理,则{1,2,3,4,5}的一种全排列{3,4,1,5,2}可以映射为{2,2,0,1,0}

  根据公式,该集合实际上表示的就是一个变进制数。

  简便计算:X = ((s1 * (n-1) + s2) * (n-3) + s3) * (n-3) + ……

康托展开

  暴力  O(n2):

  为了便于讲解下面的线段树优化,此处选择维护vis数组的方式(当然直接比大小也是一样的)。

  vis[j]用以记录j是否已出现,未出现为0,已出现为1。故a[i]左边比其小的数的个数就是vis[j]的和(j<a[i])。

  计算ans时,由于最后一位固定是0,故只需计算到n-1位即可。

  而所有全排列中比该序列小的有ans个,故该序列排在第ans+1位。

  代码如下:

int Power_Cantor()
{
    int ans=0;
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d",&a[i]);
        int sum=0;
        for(int j=1;j<=a[i];j++)sum+=vis[j];
        vis[a[i]]=1;
        a[i]-=sum+1;
        ans=(ans+a[i])*(n-i);
    }
    return ans+1;
}

  线段树优化  O(nlogn):

  我们用线段树结构存储vis数组,即可实现logn时间复杂度内求出结果。

  代码如下:

int Cantor()
{
    int ans=0;
    for(int i=1;i<=n-1;i++)
    {
        int now=a[i];
        now-=query(1,1,n,1,a[i])+1;
        update(1,1,n,a[i],1);
        ans=(ans+now)*(n-i);
    }
    return ans+1;
}

逆康托展开

  首先是将所给的排列位数转变为我们所需的变进制数:

  仍以{3,4,1,5,2}为例,其康托展开值为61:

  用 61 / 4! = 2余13,则a[1] = 2,即首位右边比首位小的数有2个,所以首位为3。

  用 13 / 3! = 2余1,则a[2] = 2,即在第二位之后小于第二位的数有2个,所以第二位为4。

  用 1 / 2! = 0余1,则a[3] = 0,即在第三位之后没有小于第三位的数,所以第三位为1。

  用 1 / 1! = 1余0,则a[4] = 1,即在第四位之后小于第四位的数有1个,所以第四位为5。

  最后一位自然就是剩下的数2。

  通过以上分析,所求排列组合为 34152。

  依然是用线段树维护vis数组,建树时每一位都先赋值为1,表示所有数均未出现。

  设对于当前这一位i,变进制数为a[i],要求的数为x。目标是要在未出现的数中找比x小的数的个数为a[i]个的数的位置,相当于在区间[1,x]中找a[i]+1。

  此处将a[i]+1记为s[i]。用二分查找x的位置:对于每一位s[i],先求出左子树比x小的数的个数为sum,再看s是否有s[i]个:若有,说明结果在左子树,则继续往左子树找s[i];若没有,则往右子树找s[i] - sum。

  查找完后将vis[x]的值修改为0。

  代码如下:

int search(int num)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        int find=query(1,1,n,l,mid);

        if(find>=num) r=mid;
        else{
            l=mid+1;
            num-=find;
        }
    }
    return r;
}

void R_Cantor(int num)
{
    num--;
    memset(a,0,sizeof a);
    build_tree(1,1,n);
    for(int i=1;i<=n;i++)
    {
        a[i]=num/(jc[n-i]);
        a[i]=search(a[i]+1);
        update(1,1,n,a[i],0);
        num%=jc[n-i];
    }

    for(int i=1;i<=n;i++)printf("%d ",a[i]);
    puts("");
}

附完整代码

#include <algorithm>
#include <cstring>
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 1e5+10;

int n;
int a[N],tree[3*N];
int jc[10]={1,1,2,6,24,120,720,5040,40320,362880}; 

void out()
{
    for(int i=1;i<=14;i++)
    {
        printf("tree[%d] = %d\n",i,tree[i]);
    }
    puts("");
}

void build_tree(int node,int start,int end)
{
    if(start==end)
    {
        tree[node]=1;
        return;
    }
    int mid=start+end>>1;
    int left=2*node;
    int right=2*node+1;
    build_tree(left,start,mid);
    build_tree(right,mid+1,end);
    tree[node]=tree[left]+tree[right];
}

void update(int node,int start,int end,int idx,int val)
{
    if(start==end)
    {
        tree[node]=val;
        return;
    }
    int mid=start+end>>1;
    int left=2*node;
    int right=2*node+1;
    if(idx<=mid)update(left,start,mid,idx,val);
    else update(right,mid+1,end,idx,val);
    tree[node]=tree[left]+tree[right];
}

int query(int node,int start,int end,int l,int r)
{
    if(end<l || start>r)return 0;
    else if(start>=l && end<=r)return tree[node];

    int mid=start+end>>1;
    int left=2*node;
    int right=2*node+1;

    return query(left,start,mid,l,r) + query(right,mid+1,end,l,r);
}

int Cantor()
{
    int ans=0;
    for(int i=1;i<=n-1;i++)
    {
        int now=a[i];
        now-=query(1,1,n,1,a[i])+1;
        update(1,1,n,a[i],1);
        ans=(ans+now)*(n-i);
    }
    return ans+1;  //所有全排列中比该序列小的有ans个,故该序列排在第ans+1位
}

int search(int num)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        int find=query(1,1,n,l,mid);

        // 先看左子树中未出现的比要求的数小的数的个数够不够num个
        // 若足够,则继续往左子树找num
        // 若不够,则继续往右子树找 (num-已找到的个数) 

        if(find>=num) r=mid;
        else{
            l=mid+1;
            num-=find;
        }
    }
    return r;
}

void R_Cantor(int num)
{
    num--; //该序列排在第num位,故比其小的全排列有num-1个。
    memset(a,0,sizeof a);
    build_tree(1,1,n);
    for(int i=1;i<=n;i++)
    {
        a[i]=num/(jc[n-i]);
        a[i]=search(a[i]+1);
        update(1,1,n,a[i],0);
        num%=jc[n-i];
    }

    for(int i=1;i<=n;i++)printf("%d ",a[i]);
    puts("");
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);

    memset(tree,0,sizeof tree);

    int ans=Cantor();

//    R_Cantor(62);

    printf("%d\n",ans);
    return 0;
}

原文地址:https://www.cnblogs.com/ninedream/p/11521141.html

时间: 2024-12-08 18:25:13

数据结构——康托展开与逆康托展开的相关文章

nyist 139 我排第几个&amp;&amp;143 第几是谁(康托展开和逆康托展开)

 我排第几个 时间限制:1000 ms  |  内存限制:65535 KB 难度:3 描述 现在有"abcdefghijkl"12个字符,将其所有的排列中按字典序排列,给出任意一种排列,说出这个排列在所有的排列中是第几小的? 输入 第一行有一个整数n(0<n<=10000); 随后有n行,每行是一个排列: 输出 输出一个整数m,占一行,m表示排列是第几位: 样例输入 3 abcdefghijkl hgebkflacdji gfkedhjblcia 样例输出 1 3027

nyoj 139——我排第几个|| nyoj 143——第几是谁? 康托展开与逆康托展开

讲解康托展开与逆康托展开.http://wenku.baidu.com/view/55ebccee4afe04a1b071deaf.html #include<bits/stdc++.h> using namespace std; int fac[20]; int fun(){ fac[0]=1; int i; for(i=1;i<=12;i++){ fac[i]=fac[i-1]*i; } } int main(){ int t,i,j,c,sum,num; char str[15];

康拓展开与逆康拓展开

1.康托展开的解释 康托展开就是一种特殊的哈希函数 把一个整数X展开成如下形式: X=a[n]*n!+a[n-1]*(n-1)!+...+a[2]*2!+a[1]*1! 其中,a为整数,并且0<=a<i,i=1,2,..,n {1,2,3,4,...,n}表示1,2,3,...,n的排列如 {1,2,3} 按从小到大排列一共6个.123 132 213 231 312 321 . 代表的数字 1 2 3 4 5 6 也就是把10进制数与一个排列对应起来. 他们间的对应关系可由康托展开来找到.

康拓展开和逆康拓展开

康拓展开和逆康拓展开 康拓展开模板题 复杂度O(\(n^2\))的会tle(看数据就知道了)(虽然某题解说可以,不知道是不是后期加强了数据 然而我还是写了O(\(n^2\))的 #include <cstdio> typedef long long LL; LL f[1000010]; const LL mod = 998244353; int a[1000010], b[1000010]; int main() { f[0] = 1; for(int i = 1; i < 100000

康托展开和逆康托展开

问题:给定的全排列,计算出它是第几个排列? 对于全排列,不清楚的可以参考全排列 方法:康托展开 对于一个长度为 n 的排列 num[1..n], 其序列号 X 为 X = a[1]*(n-1)! + a[2]*(n-2)! +...+ a[i]*(n-i)! +...+ a[n-1]*1! + a[n]*0! 其中a[i]表示在num[i+1..n]中比num[i]小的数的数量 写做伪代码为: Cantor(num[]) X = 0 For i = 1 .. n tp = 0 For j = i

康托展开 / 逆康托展开

先搬一下(戳)维基百科的康托展开(戳): 康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩. 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的. 由于是双射    所以可以求n的全排列里第k大的排列(逆康托展开) (伪)计算原理: 从某个元素找后面比这个元素小的数的个数,再乘以这个位置每一个数字能有的组合方法数(排列 / 阶乘),得出只考虑从这一位开始到末尾比当前小的排列数,然后加起来就是康托展开求的数(追求难懂的巅峰...........看不懂就看看维

LightOJ1060 nth Permutation(不重复全排列+逆康托展开)

一年多前遇到差不多的题目http://acm.fafu.edu.cn/problem.php?id=1427. 一开始我还用搜索..后来那时意外找到一个不重复全排列的计算公式:M!/(N1!*N2!*...*Nn!), 然后就靠自己YY出解法,搞了好几天,最后向学长要了数据,然后迷迷糊糊调了,终于AC了. 后来才知道当时想的解法类似于逆康托展开,只是逆康托展开是对于没有重复元素全排列而言,不过有没有重复元素都一个样. 而现在做这题很顺,因为思路很清晰了,另外这做法和数论DP的统计部分有相似之处.

NYOJ143 第几是谁? 【逆康托展开】

第几是谁? 时间限制:3000 ms  |  内存限制:65535 KB 难度:3 描述 现在有"abcdefghijkl"12个字符,将其按字典序排列,如果给出任意一种排列,我们能说出这个排列在所有的排列中是第几小的.但是现在我们给出它是第几小,需要你求出它所代表的序列. 输入 第一行有一个整数n(0<n<=10000); 随后有n行,每行是一个整数m,它代表着序列的第几小: 输出 输出一个序列,占一行,代表着第m小的序列. 样例输入 3 1 302715242 2607

康托和逆康托展开(转)

1.康托展开的解释 康托展开就是一种特殊的哈希函数 把一个整数X展开成如下形式: X=a[n]*n!+a[n-1]*(n-1)!+...+a[2]*2!+a[1]*1! 其中,a为整数,并且0<=a<i,i=1,2,..,n {1,2,3,4,...,n}表示1,2,3,...,n的排列如 {1,2,3} 按从小到大排列一共6个.123 132 213 231 312 321 . 代表的数字 1 2 3 4 5 6 也就是把10进制数与一个排列对应起来. 他们间的对应关系可由康托展开来找到.