• 子集生成和全排列


      一、子集生成

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

    百科

  • 相关阅读:
    关于mysql无法添加中文数据的问题以及解决方案
    如何在Eclipse中使用tomcat9 运行servlet开发简单的动态网页?
    Eclipse新建Servlet时候,不会自动生成mapping到web.xml,而是在代码中加入注解@WebServlet
    win10 Tomcat9.x控制台乱码问题解决
    命令行键入命令时,几个比较常用的快捷键
    Mesh Filter & Mesh Render
    Java路径
    Java复制数组的四种方法:arraycopy()方法、clone() 方法、copyOf()和copyOfRange()方法
    static class 静态类(Java)
    Java将文件转为字节数组
  • 原文地址:https://www.cnblogs.com/jslee/p/3426417.html
Copyright © 2020-2023  润新知