康托展开与康托逆展开,细节决定成败!

来源:互联网 发布:多线程实例 java 编辑:程序博客网 时间:2024/05/19 05:32

     先来几个题目链接吧,不管您看会没看会,看会了可以直接去做,没看会可以带着问题再看本篇;

    NYOJ:http://acm.nyist.net/JudgeOnline/problem.php?pid=139(比较水的裸康托展开)

    NYOJ:http://acm.nyist.net/JudgeOnline/problem.php?pid=143(上一题的逆过程,也是比较裸的逆康托)

    HDU:http://acm.hdu.edu.cn/showproblem.php?pid=1430(有点小复杂)

    HDu:http://acm.hdu.edu.cn/showproblem.php?pid=1027(逆康托展开)


                                                               上篇

 实在闲的无聊了,好多不会却不知道干嘛。下午开始在NYOJ找没做过的题,无意中就找到了这道通过率高却还没A的康托展开题(其实还有蛮多我不会的难度比较高的,直接跳过T_T),于是。。。。。。我就去百度百科、博客上学了一下,公式挺简单易懂,也挺好理解的;不过对于逆康托展开我还是有点懵逼(其实是不会);

    以下就我对今天学的这个康托展开简单介绍一下,博主水平有限,尽量用简单的方式来介绍吧;

    首先,接触这个的时候得明白这个康托展开是干嘛的。正如我们学算法一样,建议先碰到问题再去学相关的算法,不然不久就会忘了;就我的理解,给定一个排列,求在所有可能的排列中,此排列排第几个.搜狗百科直接给出了一个公式并指出”这就是康托展开hehe “,不过看多了博客、wiki、nocow也就明白了这个公式的含义;按普通思路来想,我们用next_permutation()函数将所有排列存起来再比较就可以了,我想,在小范围内是可以的,不过全排列n个元素就有n!种,超时毫无悬念。那么有没有一种快速算的方法呢,嘿嘿,康托老人家想到了;

     引自http://www.cnblogs.com/AndyHeart/archive/2012/03/20/2431428.html

    《 很显然,康托展开是本文的关键所在。你说康托他老人家当初是怎么想出来这种展开的方法的呢?我们还是以 s=["A", "B", "C"] 为例:

A B C | 0
A C B | 1
B A C | 2
B C A | 3
C A B | 4
C B A | 5

  他的思路可能是这样的:首先,确定一个目标:将每个排列映射为一个自然数,这个自然数是顺序增长的(或者至少要有一定的规律)。要说映射成自然数,第一个想到的方法自然是把数组的下标当作一个n进制的数字,但是正如本文开篇所讨论的,这个数字并没有什么规律;第二个方法是计数,也就是令 X = 当前排列之前有多少个排列。例如 A B C 是第一个排列,它前面没有任何排列,所以 X(ABC) = 0;A C B 前面有一个排列,所以 X(ACB) = 1……那么如何才能知道 X(BCA) = 3 也就是 B C A 的前面有3个排列呢?这里的技巧仍然是分解——把问题分隔成相互独立有限的小块。具体的方法是:先求出 B 第一次出现在最高位(也就是 B A C 这个排列)时前面有几个排列,再求出 B C A 是 B A C 后面第几个排列,把这个两个数相加就是想要的结果了。
  先看第一个问题:B 第一次出现在最高位(也就是 B A C 这个排列)时前面有几个排列?由于已知 B A C 前面的排列一定是 A 开头的,所以只有 A 后面的两个元素可以变化,所以排列数是 P(2,2) = 2! 个。
  第二个问题:B C A 是 B A C 后面第几个排列?因为都是 B 开头的,所以可以把开头的 B 忽略,问题变成 C A 是 A C 后面的第几个排列?同样,可以先考虑 C 第一次出现在最高位时前面有几个排列,因为 C A 前面的排列肯定是 A 开头的,所以只有 A 后面的一个元素可以变化,所以排列数是 P(1,1) = 1! 个。
  所以 X(BCA) = 2! + 1! = 3
  再例如想求 X(CBA),同样是先考虑 C 第一次出现在最高位时前面有多少个排列,因为比 C 小的元素有 A 和 B 两个,所以是 2*2! 个。再求出 B  A 是 A B 后面的第 1! 个排列。就可以知道 X(CBA) = 2*2! + 1! = 5 了

 》

对于{1,2,3,...,n}生成的已经从小到大排序好的全排列

x=a[n]*(n-1)!+a[n-1]*(n-2)!+...a[1]*0!//康托展开;
a[m]代表比在第m位的数字小并且没有在第m位之前出现过的数字的个数(以个位数为第1位)//简单来说就是比当前字符小并且未出现的个数;

x代表比这个排列小的排列的个数,所以这个数的顺序就是x+1;

注:
1.易知a[1]=0;

a[i]*(i-1)!的含义:说白了,就是找到比当前字符小并且未出现过的字符,因为我们求的是此全排列之前的全排列个数,此排列的位置只需加一即可;比当前字符小并且未出现的字符就可以在排在当前位置上,后面有i-1个位置,可以用i-1个字符全排列上去就是(i-1)!,反正第i位用比字符s[i]小的字符去填,所以生成的排列肯定小于要求的排列

很多博客上都用这组样例为例,我也不例外:

例1 {1,2,3,4,5}的全排列,并且已经从小到大排序完毕

(1)找出45231在这个排列中的顺序

比4小的数有3个
比5小的数有4个但4已经在之前出现过了所以是3个
比2小的数有1个
比3小的数有两个但2已经在之前出现过了所以是1个
比1小的数有0个

那么45231在这个排列中的顺序是3*4!+3*3!+1*2!+1*1!+0*0!+1=94

(2)找出35142在这个排序中的顺序

比3小的数有2个
比5小的数有4个但3已经在之前出现过了所以是3个
比1小的数有0个
比4小的数有3个但2,3已经在之前出现过了所以是1个
比2小的数有1个但1已经在之前出现过了所以是0个

那么35142在这个排序中的顺序是2*4!+3*3!+0*2!+1*1!+0*0!+1=68

例2 {1,2,3,4,5,6}的全排列,并且已经从小到大排序完毕

找出423615在这个排序中的顺序

比4小的数有3个
比2小的数有1个
比3小的数有2个但2已经在之前出现过了所以是1个
比6小的数有5个但4,2,3已经在之前出现过了所以是2个
比1小的数有0个
比5小的数有4个但1,2,3,4已经在之前出现过了所以是0个

那么423615在这个排序中的顺序是3*5!+1*4!+1*3!+2*2!+0*1!+0*0!+1=395

    看完以上三个样例,相信你已经明白了,NYOJ上那个题可以水过了。

    不过特别注意的就是,我们用字符串输入的时候字符a[0]实际上时排在第n位的;

    NYOJ这道题为例,给出两种代码:

1.0

char a[15];long long fun(int x){    return x==0?1:x*fun(x-1);}long long contor(char s[],int len){    long long sum=0;    for(int i=0; i<len; i++)    {        long long x=0;        for(int j=i+1; j<len; j++)//排在后面说明还未出现            if(a[i]>a[j])//比它小的;                x++;//比当前字符小并且未出现;        sum+=x*fun(len-i-1);    }    return sum+1;<span style="font-family: Arial, Helvetica, sans-serif;">//前面总共sum个排列,所以此排列排在sum+1;</span>}int main(){    int t,i;    scanf("%d",&t);    while(t--)    {        scanf("%s",a);        int x=strlen(a);        printf("%lld\n",contor(a,x));    }    return 0;}
2.0
char a[15];int v[15];long long fun(int x){    return x==0?1:x*fun(x-1);}long long contor(char s[],int len){    long long sum=0;    memset(v,0,sizeof(v));    for(int i=0; i<len; i++)    {        long long x=s[i]-'a';        int pos=s[i]-'a';        for(int j=0; j<pos; j++)//比当前字符小的;            if(v[j])                x--;//未出现的个数;        sum+=x*fun(len-i-1);        v[s[i]-'a']=1;//以出现,标记;    }    return sum+1;}int main(){    int t,i;    scanf("%d",&t);    while(t--)    {        scanf("%s",a);        int x=strlen(a);        printf("%lld\n",contor(a,x));    }    return 0;}

本文将持续更新,逆康托展开将在不久后写出,敬请期待;


                                                               下篇

    时隔一天,上一句话迎来了他的更新,下篇就是要介绍康托逆展开了,可能有点难懂,不过同样的我将用尽量简单的例子及描述来表达;

    由于感觉效率低下,所以昨晚写完上篇之后又带电脑回去学了学康托逆展开,室友Csb说不就是一个逆过程嘛,原理一样的啊;于是我又在网上看了看各种介绍,还真的是一样的也很易懂,不过网上的各种介绍总让我感觉千篇一律;没有什么详细的代码注释,举几个简单例子就是了,让我感觉有一点点不负责的态度;

     相信您看这里的时候您对康托展开已经有一定的了解;康托展开是求一个排列的在其所有可能排列中的次序,而康托逆展开则是给定这个次序,还原出这个排列,已知的条件是这个排列中的所有元素,所以我们可以知道这个排列的位数;根据康托展开公式:x=a[n]*(n-1)!+a[n-1]*(n-2)!+...a[1]*0!;既然已经知道了位数,那么(n-1)!、(n-2)!、...2!、1!都是已知的,先注意a[n]的含义及x=给定的次序-1;只需求出a[n]、a[n-1]、a[n-2]...即可知道在其下标对应的位置上的字符了;现在问题就是已经知道了x,n,求a[n];

     现在的问题是求a[n]了,首先还是要强调一下a[n]的含义及从公式:x=a[n]*(n-1)!+a[n-1]*(n-2)!+...a[1]*0!出发

     我们知道,a[n]表示比在位置n(从右边第一位开始算)上的字符小并且在n的左边的字符中未出现过的字符个数;易知:a[n]<n,即a[n]*(n-1)!<n!;

     x=a[n]*(n-1)!+a[n-1]*(n-2)!+...a[1]*0!     ->   x/(n-1)!=a[n]+(a[n-1]*(n-2)!+...a[1]*0!)/(n-1)!;

     而a[n-1]*(n-2)!+...a[1]*0!相当于求后n-1位的排列,易知a[n-1]*(n-2)!+...a[1]*0!<(n-1)!,故(a[n-1]*(n-2)!+...a[1]*0!)/(n-1)!<1,所以x/(n-1)!=a[n];

    a[n]的求法已知,那么康托逆展开就可以解决了;

    来看一个例子吧(自己举的)

    求由{1,2,3,4,5}组成的排列的第50小的排列;

    由上:x=50-1=49,n=5;   a[n]=x/(n-1)!

    第一位:49/4!=2,此时x=x-2*4!=x%4!=1;(有两个比它小的数所以第一位是3)

    第二位:1/3!=0,此时x=x-0*3!=x%3!=1;(有0个比它小的数所以第二位是1)

    第三位:1/2!=0,此时x=x-0*2!=x%2!=1;(有0个比它小的数但由于1已经出现过所以第三位是2)

    第四位:1/1!=1,此时x=x-1*1!=x%1!=0;(有1个比它小的数但由于1、2、3都已经出现过所以第四位是5)

    第五位:只剩4了;

    所以第50小的排列是 31254

   上例可以用康托展开公式计算一下:2*4!+0*3!+0*2!+1*1!+0*0!=49;return 49+1;

    所以逆康托展开就是这样;

   来看看NYOJ143,典型逆康托展开,只是千万要注意细节问题,还有抓住问题本质,从问题出发寻找方法;

1.0

#include<bits/stdc++.h>using namespace std;const int N=13;int a[N]= {1,1,2,6,24,120,720,5040,40320,362880,3628800,39916800,479001600};int v[N];int main(){    int t,i,j,n;    scanf("%d",&t);    while(t--)    {        scanf("%d",&n);        memset(v,0,sizeof(v));        n--;        for(i=11; i>=0; i--)        {            int x=n/a[i];//表示待求的字符有x个比他小并且未出现过的;            n%=a[i];            j=0;            while((v[j]||x)&&j<12)            {                if(!v[j]) x--;                j++;            }            v[j]=1;            printf("%c",j+'a');        }        printf("\n");    }    return 0;}
2.0
#include<bits/stdc++.h>using namespace std;const int N=13;int a[N]= {1,1,2,6,24,120,720,5040,40320,362880,3628800,39916800,479001600};int v[N];int main(){    int t,i,j,k,n;    scanf("%d",&t);    while(t--)    {        scanf("%d",&n);        memset(v,0,sizeof(v));        n--;        for(i=11; i>=0; i--)        {            int x=n/a[i];//表示待求的字符有x个比他小并且未出现过的;            n%=a[i];            for(k=1; k<=12; k++)                if(!v[k])                    break;            j=k;            while((v[j]||x)&&j<=12)            {                if(!v[j]) x--;                j++;            }            v[j]=1;            printf("%c",j+96);        }        printf("\n");    }    return 0;}   
3.0
#include<bits/stdc++.h>using namespace std;const int N=13;int a[N]= {1,1,2,6,24,120,720,5040,40320,362880,3628800,39916800,479001600};int v[N];int main(){    int t,i,j,k,n;    scanf("%d",&t);    while(t--)    {        scanf("%d",&n);        memset(v,0,sizeof(v));        n--;        for(i=11; i>=0; i--)        {            int x=n/a[i];//表示待求的字符有x个比他小并且未出现过的;            n%=a[i];            for(k=1;k<=12; k++)                if(!v[k])                    break;            for(j=k; j<=12&&x; j++)                if(!v[j]) x--;            while(v[j]) j++;            v[j]=1;            printf("%c",j+96);        }        printf("\n");//        for(i=1; i<=12; i++)//            if(!v[i])//                printf("%c\n",i+96);    }    return 0;}        



如需转载,请标明:转自http://blog.csdn.net/nyist_tc_lyq/article/details/51882599


3

1 0
原创粉丝点击