先来几个题目链接吧,不管您看会没看会,看会了可以直接去做,没看会可以带着问题再看本篇;
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