I am a teacher!

导航

C语言程序设计100例之(42):康托展开

例42   康托展开

问题描述

康托展开是一个全排列到一个自然数的双射,常用于构建hash表时的空间压缩。设有n个数(1,2,3,4,…,n),可以有组成n!种不同的排列组合,康托展开表示的就是当前排列组合在n个不同元素的全排列中的名次。

例如,3个数 {1,2,3} 按从小到大的排列一共6个,分别是123、132、213、231、312、321,将这6个排列分别用自然数1、2、3、4、5、6来表示。也就是把一个自然数与一个排列对应起来。它们之间的对应关系可由康托展开来得到。

给出n(n<10)和1~n构成的一个排列,求这是第几个排列。

输入

输入包括多组测试用例。每组测试用例占两行。

第1行是一个整数n;第2行是1~n构成的一个排列。

输出

对每个测试用例。在1行中输出一个整数,表示这是第几个排列。

输入样例

3

1 2 3

5

2 1 3 4 5

5

5 4 3 1 2

输出样例

1

25

119

        (1)编程思路。

        例如,求321是{1,2,3}中第几个排列可以这样考虑 :

        第1位是3,当第1位的数小于3时,其排列一定位于321的前面,如123、213。小于3的数有1、2,所以有2*2!=4个;第2位是2,小于2的数只有一个就是1,所以有1*1!=1个;因此,排在321之前的{1,2,3}排列有2*2!+1*1!=5个。这样,321是第6个排列。 计算式子:2*2!+1*1!+0*0! 就是所谓的康托展开式。

        再举个例子,1324是{1,2,3,4}排列中第几个排列。第1位是1,小于1的数没有,是0个, 0*3!;第2位是3,小于3的数有1和2,但1已经在第一位了,所以只有一个数2,1*2!;第3位是2,小于2的数只有1,但1已经在第1位,所以有0个数,0*1! ,所以,康托展开式为0*3!+1*2!+0*1!+0*0!=3,即1324是第3个排列。

        又例如,排列357412968是{1,2,3,4,5,6,7,8,9}排列中第98884个排列。因为康托展开式为:2*8!+3*7!+4*6!+2*5!+0*4!+0*3!+2*2!+0*1!+0*0!=98884。展开式解释如下:

        第1位是3,比3小的数有1和2这2个,以1、2这样的数开始的排列有8!个,即2*8!;第2位是5,比5小的数有1、2、3、4,由于3已经出现,因此有3个比5小的数,这样的数开始的排列有7!个,即3*7!;…以此类推,直至0*0!。

(2)源程序。

#include <stdio.h>

int fac[] = {1,1,2,6,24,120,720,5040,40320,362880};

int cantor(int *a, int n)   // 康托展开

{

    int i,j,cnt,num=0;

    for (i=0; i<n; i++) {

        cnt = 0;

        for (j=i+1; j<n; j++)

            if (a[j]<a[i])

                cnt++;

        num += fac[n-i-1]*cnt;

    }

    return num+1;

}

int main()

{

    int n,a[10];

    while (scanf("%d",&n)!=EOF)

    {

        int i;

        for (i=0;i<n;i++)

            scanf("%d",&a[i]);

        int ans=cantor(a,n);

        printf("%d\n",ans);

    }

    return 0;

}

习题42

42-1  逆康托展开

问题描述

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

"123" 、"132"、 "213"、 "231"、 "312"、 "321"。

给定 n 和 k,返回第 k 个排列。其中,n 的范围是 [1, 9],k 的范围是[1,  n!]。

输入

输入包括多组测试用例。每组测试用例占1行,在这1行中有两个整数n和k。

输出

对每个测试用例。在1行中输出1~n构成的1个排列。

输入样例

3 3

4 9

5 16

输出样例

213

2314

14352

        (1)编程思路。

        先以n=5,k=16进行说明。

        首先用16-1得到15

         1)用15去除4! ,得到0余15;有0个数比它小的数是1,所以最高位是1;

         2)用余数15去除3!,得到2余3;有2个数比它小的数是3,但1已经在之前出现过了,所以是4,即第2位为4;

         3)用余数3去除2! ,得到1余1;有1个数比它小的数是2,但1已经在之前出现过了,所以是3,即第3位是3;

         4)用余数1去除1! ,得到1余0。有1个数比它小的数是2,但1、3、4都出现过了,所以是5,即第4位是5;

        剩下的2作为最低位。

        故,k=16的排列为1 4 3 5 2。

      (2)源程序。

#include <stdio.h>

int fac[] = {1,1,2,6,24,120,720,5040,40320,362880};

void uncantor(int res[],int x, int n)  // 逆康托展开

{

    int i,j,cnt,t;

    int h[10]={0};

    for (i = 1; i <= n; i++)

    {

        t = x / fac[n-i];

        x -= t*fac[n-i];

        for (j = 1, cnt=0; cnt<=t; j++)

            if (!h[j]) cnt++;

        j--;

        h[j] = 1;

        res[i-1]=j;

    }

}

int main()

{

    int n,x,a[10];

    while (scanf("%d",&n)!=EOF)

    {

        scanf("%d",&x);

        uncantor(a,x-1,n);

        int i;

        for (i=0;i<n;i++)

            printf("%d",a[i]);

        printf("\n");

    }

    return 0;

}

42-2  排第几?

问题描述

现在有"abcdefghijkl”12个字符,将其所有的排列按字典序排列,给出任意一种排列,说出这个排列在所有的排列中是第几小的?

输入

第一行有一个整数n(0<n<=10000);

随后有n行,每行是一个排列。

输出

对于每个排列,输出一个整数m,占一行,m表示排列是第几位。

输入样例

3

abcdefghijkl

hgebkflacdji

lkjihgfedcba

输出样例

1

302715242

479001600

         (1)编程思路。

         按例42所示的康托展开式展开即可。

        (2)源程序。

#include <stdio.h>

int main()

{

    long long fact[13];

    char str[15];

    fact[0]=1;

    int i,j;

    for (i=1;i<=12;i++)

    {

        fact[i]=fact[i-1]*i;

    }

    int t;

    scanf("%d",&t);

    getchar();

    while(t--)

    {

        gets(str);

        long long sum=0;

        int len=strlen(str);

        for (i=0; str[i]!='\0'; i++)

        {

            int cnt=0;

            for (j=i+1; str[j]!='\0'; j++)

            {

                if (str[i]>str[j])

                {

                    cnt++;

                }

            }

            sum+=cnt*fact[len-1-i];

        }

        printf("%lld\n",sum+1);

    }

    return 0;

}

42-3 奶牛排成行

        本题选自洛谷题库 (https://www.luogu.org/problem/ P3014)

问题描述

有N(1<=N<=20)头奶牛,编号为1…N,正在与FJ玩一个疯狂的游戏。奶牛会排成一行,问FJ此时的行号是多少。之后,FJ会给奶牛们一个行号,奶牛必须按照新行号排列成线。

行号是通过以字典序对行的所有排列进行编号来分配的。比如说:FJ有5头奶牛,让它们排为行号3,排列顺序为:

1:1 2 3 4 5

2:1 2 3 5 4

3:1 2 4 3 5

因此,奶牛将排列为1、2、4、3、5。

之后,奶牛排列为“1 2 5 3 4”,并向FJ问它们的行号。继续列表:

4:1 2 4 5 3

5:1 2 5 3 4

FJ可以看到这里的答案是5。

FJ和奶牛希望你帮助玩这个游戏。

输入

第1行输入两个整数N and K

之后的2*K行描述给出的查询。每个查询占两行。

查询有两个部分:C_i将是“P”或“Q”的命令。

如果C_i是“P”,则查询的第二部分将是一个整数Ai(1 <=Ai <= N!),它是行号。此时,你需要回答正确的奶牛排列顺序。

如果C_i是“Q”,则查询的第二部分将是N个不同的整数B_j(1 <= B_j <= N)。这将表示奶牛们的一个排列,此时你需要输出正确的行号。

输出

输出k行,每行为对应查询的答案。

输入样例

5 2

P

3

Q

1 2 5 3 4

输出样例

1 2 4 3 5

5

      (1)编程思路。

       P命令调用康托逆展开,Q命令调用康托展开。

      (2)源程序。

#include <stdio.h>

long long fact[21];

long long cantor(int *a, int n)           // 康托展开

{

    int i,j;

    long long cnt,num=0;

    for (i=0; i<n; i++) {

        cnt = 0;

        for (j=i+1; j<n; j++)

            if (a[j]<a[i])

                cnt++;

        num += fact[n-i-1]*cnt;

    }

    return num+1;

}

void uncantor(int res[],long long x, int n)  // 逆康托展开

{

    int i,j;

    long long t,cnt;

    int h[21]={0};

    for (i = 1; i <= n; i++)

    {

        t = x / fact[n-i];

        x -= t*fact[n-i];

        for (j = 1, cnt=0; cnt<=t; j++)

            if (!h[j]) cnt++;

        j--;

        h[j] = 1;

        res[i-1]=j;

    }

}

int main()

{

    fact[0]=1;

    int i;

    for (i=1;i<=20;i++)

    {

        fact[i]=fact[i-1]*i;

    }

    int n,k,a[21];

    long long x;

    scanf("%d%d",&n,&k);

    while (k--)

    {

        char op[2];

        scanf("%s",op);

        if (op[0]=='P'){

            scanf("%lld",&x);

            uncantor(a,x-1,n);

            for (i=0;i<n;i++)

               printf("%d ",a[i]);

            printf("\n");

        }

        else{

            for (i=0;i<n;i++)

                scanf("%d",&a[i]);

            x=cantor(a,n);

            printf("%lld\n",x);

        }

    }

    return 0;

}

posted on 2022-01-02 13:19  aTeacher  阅读(340)  评论(0编辑  收藏  举报