子集生成和全排列
一、子集生成
给定一个集合,枚举它所有可能的子集,eg:{0,1,2}
解: {0} {1} {2} {0,1} {1,2} {0,2} {0,1,2} 子集是无序的
1.增量构造法
public static void subsetIncre(int n,int[] A,int cur){
for (int i = 0; i < cur; i++) System.out.print(A[i]); //输出集合
System.out.println();
int s= cur!=0 ? A[cur-1]+1 : 0; //确定当前元素最小可能值(因为默认是按从小到大排序的,这样才能增量不重复)
for (int i = s; i < n; i++) { //所有剩余可能值,当无剩余可能值时,递归结束
A[cur] = i; //逐步确定每个A[]的值
subsetIncre(n,A,cur+1); //递归
}
}
上面代码的思路是一次选出一个元素放到集合中。
eg: 选第一个{0,*,*} 到 选第二个 {0,1,*} 到选第三个 {0,1,2}, 每一次都是集合的子集。
无法再添加更多元素,递归就结束。
此法用到了定序的方法:规定集合A中所有元素的编号从小到大排列,就不会产生重复的输出值。
总共递归 2^n 次。
2.位向量法
public static void subsetBit(int n,int[] A,int cur){
if (cur==n) {
for (int i = 0; i < n; i++) if (A[i]==1) System.out.print(i);
System.out.println();
return;
}
A[cur]=1;
subsetBit(n,A,cur+1);
A[cur]=0;
subsetBit(n,A,cur+1);
}
构造一个位向量来映射集合中元素的下标。
必须当所有元素都选择以后才能确定是一个完整的子集。
所以递归多了一些节点。这是一个二叉解答树。2^(n+1) -1次。
3.二进制法(略等于Hash)
其实要表示A={a,b,c,d,e,f,g....} 集合的子集,就是里面元素对应的下标的排列分布。
而int型的二进制表示法,刚好把所有的0,1表示都枚举完全了的。所以只要输出int型表示的所有值。就是集合的子集。
public static void doSubsetBinary(int n){
for (int i = 0; i < (1<<n); i++) {
subsetBinary(n, i);
}
}
public static void subsetBinary(int n,int a){
for (int i = 0; i < n; i++) {
if ((a&1)==1) System.out.print(i);
a>>=1;
}
System.out.println();
}
4.集合的所有子集是2的n次方证明
一个集合中的元素个数一共有n个,这个集合称为n元集。
n 元集的子集,按子集中元素个数来分类,可以是0个元素,1个元素,2个元素……n个元素,一共是n类。
由组合数公式得子集个数为 C(n,0)+C(n.1)+……C(n,n)=2^n
所以子集一共2^n个。
二、全排列
1.递归
//设(ri)perm(X)表示每一个全排列前加上前缀ri得到的排列.当n=1时,perm(R)=(r) 其中r是唯一的元素,这个就是出口条件. 当n>1时,perm(R)由(r1)perm(R1),(r2)perm(R2),...(rn)perm(Rn)构成. void Perm(list[],int k,int m) //k表示前缀的位置,m是要排列的数目. { if(k==m-1) //前缀是最后一个位置,此时打印排列数. { for(int i=0;i<m;i++) { printf("%d",list[i]); } printf("n"); } else { for(int i=k;i<m;i++) { //交换前缀,使之产生下一个前缀. Swap(list[k],list[i]); Perm(list,k+1,m); //将前缀换回来,继续做上一个的前缀排列. Swap(list[k],list[i]); } } } //此处为引用,交换函数.函数调用多,故定义为内联函数. inline void Swap(int &a,int &b) { int temp=a,a=b,b=temp; } //函数Perm(int list[],int k,int m)是求将list的第0~k-1个元素作为前缀、第k~m个元素进行全排列得到的全排列,如果k为0,且m为n,就可以求得一个数组中所有元素的全排列。其想法是将第k个元素与后面的每个元素进行交换,求出其全排列。这种算法比较节省空间。
代码改进
去掉重复符号的全排列:在交换之前可以先判断两个符号是否相同,不相同才交换,这个时候需要一个判断符号是否相同的函数。
2.非递归
int b[N]; int is_train(int a[],int n) { int i,j,k=1 ; for(i=1;i<=n;i++) { for(j=i+1;j<=n;j++) if(a[j]<a[i])b[k++]=a[j]; /*判断是否降序*/ if(k>1)is_train(b,k); else return(1); } } void train(int a[],int n) { int i,j,t,temp,count=1 ; t=1 ; printf("input the %3dth way:",count); for(i=1;i<=n;i++) printf("%3d",a[i]); printf("n"); while(t) { i=n ; j=i-1 ; /*从右往左找,找第一个左邻比右邻小的位置*/ while(j&&a[j]>a[i]) { j--; i--; } if(j==0)t=0 ; else t=1 ; if(t) { i=n ; /*从右往左找,找第一个比front大的位置*/ while(a[j]>a[i]) i--; temp=a[j],a[j]=a[i],a[i]=temp ; quicksort(a,j+1,N);/*调用快速排序*/ /*判断是否符合调度要求*/ if(is_train(a,N)==1) { count++; printf("input the %3dth way:",count); for(i=1;i<=n;i++) printf("%3d",a[i]); printf("n"); } } } }
3.Hash实现
我们经常使用的数的进制为“常数进制”,即始终逢p进1。例如,p进制数K可表示为
K = a0*p^0 + a1*p^1 + a2*p^2 + ... + an*p^n (其中0 <= ai <= p-1),它可以表示任何一个自然数。
一种特殊的变进制数,它能够被用来实现全排列的Hash函数,并且该Hash函数能够实现完美的防碰撞和空间利用(不会发生碰撞,且所有空间被完全使用,不多不少)。这种全排列Hash函数也被称为全排列数化技术。
我们考查这样一种变进制数:第1位逢2进1,第2位逢3进1,……,第n位逢n+1进1。它的表示形式为
K = a1*1! + a2*2! + a3*3! + ... + an*n! (其中0 <= ai <= i),
也可以扩展为如下形式(因为按定义a0始终为0),以与p进制表示相对应:
K = a0*0! + a1*1! + a2*2! + a3*3! + ... + an*n! (其中0 <= ai <= i)。
(后面的变进制数均指这种变进制数,且采用前一种表示法)
考查n位变进制数K的性质:
(1)当所有位ai均为i时,此时K有最大值
MAX[K] = 1*1! + 2*2! + 3*3! + ... + n*n!
= 1! + 1*1! + 2*2! + 3*3! + ... + n*n! - 1
= (1+1)*1! + 2*2! + 3*3! + ... + n*n! - 1
= 2! + 2*2! + 3*3! + ... + n*n! - 1
= ...
= (n+1)!-1
因此,n位K进制数的最大值为(n+1)!-1。
(2)当所有位ai均为0时,此时K有最小值0。
因此,n位变进制数能够表示0到(n+1)!-1的范围内的所有自然数,共(n+1)!个。
在一些状态空间搜索算法中,我们需要快速判断某个状态是否已经出现,此时常常使用Hash函数来实现。其中,有一类特殊的状态空间,它们是由全排列产生的,比如N数码问题。对于n个元素的全排列,共产生n!个不同的排列或状态。
假设我们有b0,b1,b2,b3,...,bn共n+1个不同的元素,并假设各元素之间有一种次序关系 b0<b1<b2<...<bn。对它们进行全排列,共产生(n+1)!种不同的排列。对于产生的任一排列 c0,c1,c2,..,cn,其中第i个元素ci(1 <= i <= n)与它前面的i个元素构成的逆序对的个数为di(0 <= di <= i),那么我们得到一个逆序数序列d1,d2,...,dn(0 <= di <= i)。这不就是前面的n位变进制数的各个位么?于是,我们用n位变进制数M来表示该排列:
M = d1*1! + d2*2! + ... + dn*n!
定理1 n+1个元素的全排列的每一个排列对应着一个不同的n位变进制数。
/*补充: 什么是逆序数:
跟标准列相反序数的总和
比如说
标准列是1 2 3 4 5
那么 5 4 3 2 1 的逆序数算法:
看第二个,4之前有一个5,在标准列中5在4的后面,所以记1个
类似的,第三个 3 之前有 4 5 都是在标准列中3的后面,所以记2个
同样的,2 之前有3个,1之前有4个
将这些数加起来就是逆序数=1+2+3+4=10
再举一个 2 4 3 1 5
4 之前有0个
3 之前有1个
1 之前有3个
5 之前有0个
所以逆序数就是1+3=4
*/
对于全排列的任意两个不同的排列p0,p1,p2,...,pn(排列P)和q0,q1,q2,...,qn(排列Q),从后往前查找第一个不相同的元素,分别记为pi和qi(0 < i <= n)。
(1)如果qi > pi,那么,
如果在排列Q中qi之前的元素x与qi构成逆序对,即有x > qi,则在排列P中pi之前也有相同元素x > pi(因为x > qi且qi > pi),即在排列P中pi之前的元素x也与pi构成逆序对,所以pi的逆序数大于等于qi的逆序数。又qi与pi在排列P中构成pi的逆序对,所以pi的 逆序数大于qi的逆序数。
(2)同理,如果pi > qi,那么qi的逆序数大于pi的逆序数。
因此,由(1)和(2)知,排列P和排列Q对应的变进制数至少有第i位不相同,即全排列的任意两个不同的排列具有不同的变进制数。
计算n个元素的一个排列的变进制数的算法大致如下(时间复杂度为O(n^2)):
int PermutationToNumber(const int permutation[], int n) { int result = 0; for (int j = 1; j < n; ++j) { int count = 0; for (int k = 0; k < j; ++k) { if (permutation[k] > permutation[j]) ++count; } // factorials[j]保存着j的阶乘 result += count * factorials[j]; } return result; }
参考:算法竞赛入门经典
http://blog.csdn.net/ivapple/article/details/7551990
百科