子集生成和全排列

  一、子集生成

  给定一个集合,枚举它所有可能的子集,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.非递归

  n个数的排列可以从1.2....n开始,至n.n-1....2.1结束。也就是按数值大小递增的顺序找出每一个排列。
  以6个数的排列为例,其初始排列为123456,最后一个排列是654321,如果当前排列是124653,找它的下一个排列的方法是,从这个序列中从右至左找第一个左邻小于右邻的数,如果找不到,则所有排列求解完成,如果找得到则说明排列未完成。本例中将找到46,计4所在的位置为i,找到后不能直接将46位置互换,而又要从右到左到第一个比4大的数,本例找到的数是5,其位置计为j,将i与j所在元素交换125643,然后将i+1至最后一个元素从小到大排序得到125346,这就是124653的下一个排列,如此下去,直至654321为止。算法结束。
  
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

百科

 

posted on 2013-11-16 09:58  依蓝jslee  阅读(734)  评论(0编辑  收藏  举报

导航