枚举C(n,m)组合的算法集锦

来源:互联网 发布:微信网络诈骗 编辑:程序博客网 时间:2024/06/05 00:30

1.绪论
1.1问题描述
现有一个大小为n的样本空间,从其他随机抽取m个样本,其中n,m>=0,m<=m,请枚举所有的组合情况。
1.2组合数求解的意义
排列组合是组合学最基本的概念。排列,就是指从给定个数的元素中取出指定个数的元素进行排序;组合则是指从给定个数的元素中仅仅取出指定个数的元素,不考虑排序。排列组合的中心问题是研究给定要求的排列和组合可能出现的情况总数。排列组合与古典概率论关系密切,是进一步学习的基础,本论文研究的是如何快速有效枚举组合问题中的基本问题C(N,M)的组合数集,意在节约计算机资源的使用和提高生产效率。解决方案将针对绪论中描述的问题给出七中不同的解法。

2.解决方案
2.1 解法简介
2.1.1解法一:RDC(Recursive-Definition Combination) 定义-递归法
根据C(n,m)的定义,在n个样本中抽取m个,可以理解为现在n这个样本中抽取1个,在从剩下的n-1个样本中抽取m-1个;或者是先去除n中的任意一个,在从n-1个样本中抽取m个。如现在有总样本{0,1,2,3,4},要从其中抽取3个组成组合数集,可以有以下的步骤:
(1)取得第一个元素{0},再从余下的{1,2,3,4}抽取2个;
(2)从{1,2,3,4}中抽取第二个元素,得到{0,1},再从余下的{2,3,4}中抽取1个;
(3)从{2,3,4}中获取第三个元素,得到{0,1,2},再从余下的{3,4}中抽取0个;
(4)去除{0,1,2,3,4}中的0,从剩下的{1,2,3,4}中抽取3个,返回第一步;
从以上的步骤可以得知,这是一个递归的过程,于是我们有了第一个根据定义产生的方法,程序如下:

/*RDC.c文件*/#include <stdio.h>#include <stdlib.h>int *dst_array;long long index=0,count;static void printA(int *parray,int n);void parray_init(int parray,n);void print_combine(int *parray,int n,int m);//打印长度为n的数组元素static void printA(int *parray,int n){    int i;    for(i=0;i<n;i++){        printf("%d ",parray[i]);    }    printf("\n");}//初始化数组void parray_init(int *parray,int n){      for(i=0;i<n;i++){       *(parray+i) = i;    }}//递归打印组合数  void print_combine(int *pArray,int n,int m){    if(n < m || m==0)    return ;//情况一:不符合条件,返回    print_combine(pArray+1,n-1,m);//情况二:不包含当前元素的所有的组合           dst_array[index++]=pArray[0];//情况三:包含当前元素    if(m==1){//情况三-1:截止到当前元素         printA(dst_array,index);        count++;        index--;        return;    }    print_combine(pArray+1,n-1,m-1);//情况三-2:包含当前元素但尚未截止    index--;//恢复index值}//程序运行及测试int main(){    int n,m;//存放数据的数组,及n和m    count = 0;    printf("please enter a couple likes 10 2: \n");    while(scanf("%d %d",&n,&m)!=2 || m<=0 || n<m){       printf("please enter a couple n,m (n>0,m>0,m<=n) likes 10 2: \n");    }    int i,parray[n];    dst_array = (int *)malloc(sizeof(int)*m);   parray_init(parray,n);   print_combine(parray,n,m);//求数组中所有数的组合   free(dst_array);   end_t = clock();    return 0;}

2.1.2解法二:BC10(Binary Combination-10)二进制移位法
另一种常用的组合数集求解方法是二进制移位法,其原理如下:
仍然以从样本{0,1,2,3,4}中抽取3个元素为例子。用0或1表示某位置上的元素是否被选到,比如[1,1,1,0,0]这个序列表示[0,1,2],[0,1,1,1,0]则表示[1,2,3];而下一个序列总是由当前的序列产生,它的产生规律如下:
初始化第一个序列为{1,1,1,0,0},从左向右找到第一对’10’,标识1的位置,交换他们的值,即’10’=>’01’,然后将标识位置前所有的’1’全部移动到序列的开头,就产生了一个新的序列,新序列依照前面的变化规律产生下一个新的序列,如5选3,变化规律如下:
1 1 1 0 0 //0,1,2
1 1 0 1 0 //0,1,3
1 0 1 1 0 //0,2,3
0 1 1 1 0 //1,2,3
1 1 0 0 1 //0,1,4
1 0 1 0 1 //0,2,4
0 1 1 0 1 //1,2,4
1 0 0 1 1 //0,3,4
0 1 0 1 1 //1,3,4
0 0 1 1 1 //2,3,4
根据以上思想,可以得到以下解法:

/*BC10.c文件*/#include <stdio.h>static void printA(int *dst_array,int n);double counts_combitions(int n,int m);double print_combine(int n,int m);//打印长度为n的数组元素static void printA(int *dst_array,int n){    int i;    for(i=0;i<n;i++){        if(dst_array[i])           printf("%d ",i);    }    printf("\n");}//计算C(n,m)组合数总数double counts_combitions(int n,int m){     double sum_up=1,sum_down=1;   int i=n,j=m;   if(n<m)     return 0LL;   if(m==0)     return (double) n;   while(j>0){     sum_up *= i;     sum_down *= j;     --i;     --j;   }   return (double)(sum_up/sum_down);    }//枚举组合,返回组合总数double print_combine(int n,int m){   int dst_array[n],i,j,k,end;   double count,total_count;   count = 1;   total_count = counts_combitions(n,m);  //初始化第一个序列   for(k=0;k<m;++k)      dst_array[k] = 1;   for(k=m;k<n;k++)      dst_array[k] = 0; printA(dst_array,m);  //序列的最后一个位置   end = n-1;   //循环枚举组合数   while(count<total_count){     //从左向右找到第一对10并交换值,标识这个1的位置     for(i=0,k=0;i<end;i++){        if(dst_array[i]>dst_array[i+1]){            dst_array[i] = 0;            dst_array[i+1] = 1;            break;        }        if(dst_array[i])          ++k;      /*统计标识位前的1的数量*/     }//将标识位的前面所有1移动到序列开头     if(*dst_array==0 && k>0 && i>0){        for(j=0;j<k;j++)            dst_array[j] = 1;        for(j=k;j<i;j++)            dst_array[j] = 0;     }     printA(dst_array,m);     ++count;   }   return count;}int main(){    int n,m;//存放数据的数组,及n和m    long long count;    printf("please enter a couple likes 10 2: \n");    while(scanf("%d %d",&n,&m)!=2 || m<=0 || n<m){         printf("please enter a couple n,m (n>0,m>0,m<=n) likes 10 2: \n");    }     count = print_combine(n,m);//求数组中所有数的组合   return 0;}

2.1.3解法二-优化:BC10A(Binary Combination-10 Advanced)二进制移位法优化
如果n远远大于m,则解法二遍历查找第一对10位置以及移动标识位的1到序列开头会造成巨大的遍历开销,通过索引和反索引的方式记录和更新1的位置,通过栈的方式记录下一个序列第一对的10的标识位,可在一定程度上提高性能。算法如下:

/*BC10A.c文件*/#include <stdio.h>double print_combine(int n,int m);static void printA(int *dst_array,int n);void my_memcpy(int *dst,int const *src,int n);void my_init(int *a1,int *a2,int n,int m);//打印长度为n的数组元素static void printA(int *dst_array,int n){    int i;    for(i=0;i<n;i++){        printf("%d ",dst_array[i]);    }    printf("\n");}//拷贝数组void my_memcpy(int *dst,int const * src,int n){    int i;    for(i=n-1;i>=0;i--)        *(dst+i) = *(src+i);}//初始化数据void my_init(int *a1,int *a2,int n,int m){   int i;   for(i=0;i<m;++i){     *(a1+i) = 1;     *(a2+i) = i;   }   for(i=m;i<n;i++)    a1[i] = 0;}//计算组合数的总数double counts_combitions(int n,int m){     double sum_up=1,sum_down=1;   int i=n,j=m;   if(n<m)     return 0;   if(m==0)     return (long long) n;   while(j>0){     sum_up *= i;     sum_down *= j;     --i;     --j;   }   return sum_up/sum_down;  }//枚举组合double print_combine(int n,int m){  int combition[n],index[n],reverse[n],stack[n],curIndex,one,k,j=0,stackLenght = 0;    double count;    count = 1;    stack[0] = m-1;  //将第一对10的1的位置放置栈中    //初始化第一个序列,索引,反索引    my_init(combition,index,n,m);     my_memcpy(reverse,index,m);    printA(index,m);    //循环枚举组合    while(stackLenght>=0){         curIndex = stack[stackLenght];//从栈中取出当前序列中第一对10的1的位置索引,作为标识位         --stackLenght;         //交换值         combition[curIndex] = 0;         combition[curIndex+1] = 1;        //将下一个序列的可能的第一对10的1的位置放入栈中          if(curIndex+2<n && combition[curIndex+2]==0)          stack[++stackLenght] = curIndex+1;        //更新序列,及其索引和反索引         index[reverse[curIndex]] = curIndex+1;         reverse[curIndex+1] = reverse[curIndex];         one = reverse[curIndex];        //将标识位前的1移动到序列开头,并更新索引和反索引         for(k=0;k<one;++k){          combition[index[k]] = 0;          combition[k] = 1;          reverse[k] = k;          index[k] = k;         }        //将下一个序列的可能的第一对10的1的位置放入栈中          if(combition[one]==0 && one>=1)          stack[++stackLenght] = one-1;printA(index,m);       ++count;    }    return count;}int main(){    int n,m;//存放数据的数组,及n和m    printf("please enter a couple likes 10 2: \n");    while(scanf("%d %d",&n,&m)!=2 || m<=0 || n<m){         printf("please enter a couple n,m (n>0,m>0,m<=n) likes 10 2: \n");    }    print_combine(n,m);//求数组中所有数的组合   return 0;}

2.1.4解法三(原创):LDC(Location Difference Combination)位置差值法
观察解法二中的二进制移位法,可以发现0在这个算法中其实可以被忽略的,算法中真正要考察的是1的位置,既然如此,我们可以用一个数组来记录其中1的位置作为序列,对于C(n,m)可以通过以下规则得到下一个序列。
(1) 开启一个大小为m的数组a,初始化第一个序列,数组的中每个元素的值等于其下标,即a[i] = i;
(2) 从下标为0的数组元素开始查找,当前后两两元素差值大于1(a[i]-a[i+1]>1)时则将前者的值加一(++a[i]);若找不到这样的元素对,则将最后一个元素的值加一(++a[m-1]);此后,若下标为0的元素的值不等于0(a[0]!=0),则将前者前的所有元素的值初始化(对于k<=i,a[k]=k);
(3) 检查下标为0的元素的值是否已经达到上限(a[0]==n-m?),如未达到,重复第(2)个步骤,否则结束。
如C(5,3)的所有序列如下:
0 1 2
0 1 3
0 2 3
1 2 3
0 1 4
0 2 4
1 2 4
0 3 4
1 3 4
2 3 4
程序如下:

/*LDC.c文件*/#include <stdio.h>void dst_init(int *dst_array,int n);static void printA(int *dst_array,int n);void print_combine(int n,int m);//打印长度为n的数组元素static void printA(int *dst_array,int n){    int i;    for(i=0;i<n;i++){        printf("%d ",dst_array[i]);    }    printf("\n");}//初始化目标数组void dst_init(int *dst_array,int n){  int i;  for(i=0;i<n;i++)    dst_array[i] = i;}//枚举组合void print_combine(int n,int m){     int i,k,maxhead,level;    int dst_array[m]; //开一个大小为m的数组   maxhead = n-m; //dst_array[0]的上限   level = m-1; //最大标识位   for(k=0;k<m;k++) //初始化第一个序列    dst_array[k] = k;  printA(dst_array,m);    while(*dst_array<maxhead){     for(i=0;i<level;i++){        if(dst_array[i+1]-dst_array[i]>1){ //若能找到这样的元素对,则将前者加一             ++dst_array[i];              break;          }        }     if(i==level) //若找不到,则将最后的元素值加一        ++dst_array[i];     if(*dst_array!=0) //只要dst_array[0]!=0,则初始化前面的元素        for(k=0;k<i;k++)          dst_array[k] = k;     printA(dst_array,m);   }  }int main(){    int n,m;//存放数据的数组,及n和m    printf("please enter a couple likes 10 2: \n");    while(scanf("%d %d",&n,&m)!=2 || m<=0 || n<m){         printf("please enter a couple n,m (n>0,m>0,m<=n) likes 10 2: \n");    }   print_combine(n,m);//求数组中所有数的组合    return 0;}

2.1.5解法四(原创):POC(Position Offset Combination)位置偏移量法
从一个简单的例子说起,比如现在一个数组{0,1,2,3,4,5},要求不重复选取4个,请问有多少种组合。我们按照从小到大的顺序罗列了15种结果:(粗体表示但前指针位置)
组合数 对应的偏移量数组
{0,1,2,3}, {0,0,0,0}
{0,1,2,4}, {0,0,0,1}
{0,1,2,5}, {0,0,0,2}
{0,1,3,4}, {0,0,1,1}
{0,1,3,5}, {0,0,1,2}
{0,1,4,5}, {0,0,2,2}
{0,2,3,4}, {0,1,1,1}
{0,2,3,5}, {0,1,1,2}
{0,2,4,5}, {0,1,2,2}
{0,3,4,5}, {0,2,2,2}
{1,2,3,4}, {1,1,1,1}
{1,2,3,5}, {1,1,1,2}
{1,2,4,5}, {1,1,2,2}
{1,3,4,5}, {1,2,2,2}
{2,3,4,5}, {2,2,2,2}
观察以上的组合数,我们发现,第一个位置上的数的范围是[0,2],第二个位置上的数的范围是[1,3],第三个位置上的数的范围是[2,4],第四位置的数的范围是[3,5],所以以上的15种组合能以{[0,1,2],[1,2,3],[2,3,4],[3,4,5]}中的后一个位置的数大于前一个位置的数的组合来表示,并且我们发现每个位置的最大最小值之差=2=6-4=n-m,也就是所,对于任意的C(n,m)问题,设c=n-m它的组合结果集均可[(0~0+c),(1~1+c),…..,(k~k+c),(m-1~m-1+c)]表示中满足后一个位置必须大于前一个位置的组合数集。
观察以上组合数,每一个组合数都由它的上一个组合数变化而来:并且指针的移动符合某种特定的规律:
(1)如果当前指针位于末端,其位置偏移量若未满,则该位置偏移量+1。
(2)当前指针位置上的偏移量(也就是该位置上的数是否已经最大,这里用偏移量图示以便理解指针变化的规律)如果已满,则指针不断向前移动位,直至找到第一个偏移量未满的位置,该位置的偏移量+1,并使得该指针之后的位置偏移量与当前指针的位置偏移量一致。比如:
{0,1,4,5}=> {0,2,3,4}
{0,0,2,2} =>{0,1,1,1}
可以发现:当前指针上的位置偏移量每+1,后面的指针的位置偏移量与当前指针的位置偏移量保持一致。
(3)每一次当前指针的位置偏移量+1,指针便自动移动到末端,该位置偏移量若满了直接保存组合数,未满则+1,同样保存组合数如:
{0,2,3,4} =>{0,2,3,5}
{0,1,1,1} =>{0,1,1,2}
(4)偏移量每改变一次,则产生一个新的组合数,这个组合数将用于产生下一个组合数。以此类推,产生所有的组合数,若当前指针以位于0端,并且该位置偏移量已满,则结束并返回组合数结果集。
程序如下:

/*POC.c文件*/#include <stdio.h>void print_combine(int n,int m);static void printA(int *dst_array,int n);//打印长度为n的数组元素static void printA(int *dst_array,int n){    int i;    for(i=0;i<n;i++){        printf("%d ",dst_array[i]);    }    printf("\n");}/*枚举组合*/void print_combine(int n,int m){    int maxBucketID,offset,bucketID,bid,i,j=0,bucket[m];    maxBucketID = m-1; //最后一个桶的编号    bucketID = maxBucketID;    offset = n-m; //每个桶的最大容量-1    for(i=0;i<m;++i)      bucket[i] = i; //初始化第一个桶printA(bucket,m);    while(1){      while(bucket[bucketID]==bucketID+offset){         --bucketID;         if(bucketID<0)  return; //表示所有的桶已经满了,结束循环      }      ++bucket[bucketID];//该桶的值+1//设置当前桶的位置之后的所有桶的位置偏移量与当前桶的偏移量保持一致对应的值      for(i=bucketID+1,bid=bucket[bucketID]-bucketID;i<=maxBucketID;++i)         bucket[i] = i+bid;//填满最后一个桶  bucketID = maxBucketID;  for (i=bucket[bucketID];i<=bucketID+offset;++i)       bucket[bucketID] = i;printA(bucket,m);    }     }int main(){    int n,m;//存放数据的数组,及n和m    printf("please enter a couple likes 10 2: \n");    while(scanf("%d %d",&n,&m)!=2 || m<=0 || n<m){         printf("please enter a couple n,m (n>0,m>0,m<=n) likes 10 2: \n");    }   print_combine(n,m);//求数组中所有数的组合    return 0;}

2.1.6解法五(原创):PGC(Position Gap Combination)后置差值法
该方法是混合了解法三和解法四思想的变异版本,但是它不需要初始化第一个序列,也不需要为每个位置规定上下限,性能比POC要好一点,但是不如LDC。程序如下:

/*PGC.c文件*/#include <stdio.h>double print_combine(int m,int n);//打印长度为n的数组元素static void printA(int *dst_array,int n){    int i;    for(i=0;i<n;i++){        printf("%d ",dst_array[i]);    }    printf("\n");}double print_combine(int n,int m){    if(n < m || m<=0 || n<=0)    return 0;//情况一:不符合条件,返回    int k;    if(m==1){      for (k = 0; k < n; ++k)      {         printf("%d ",k);         printf("\n");      }      return n;    }    int i=0,j=0,g=n-m,h=m-1,dst_array[m];    double count=0;    dst_array[0] = 0;    while(*dst_array<g){ //最后一个序列的dst_array[0]=g;      if( i<h) {        dst_array[i] = j;         i++;      }              else {        while(j<n){ //总是将最后一个位置的值迭代到其上限          dst_array[i] = j;          ++count;          printA(dst_array,m);          ++j;        }        for(k=i;k>0;k--){           if(dst_array[k]-dst_array[k-1]>1) //回到上一个未达到值上限的位置            break;          }        i = k-1;        j = dst_array[i];      }        j++;    }    ++count;    //printA(dst_array,m);    return count;}int main(){    int n,m;//存放数据的数组,及n和m    scanf("%d%d",&n,&m);    print_combine(n,m);//求数组中所有数的组合    return 0;}

2.1.7解法六(原创):RCC(Recursive Chain Combination)链式递归法
该方法是对解法四的思想的进一步改善。假如总样本={0,1,2,3,4,5},需要从中抽出4个元素组成组合数集,每一个组合数集内部元素均按照从小到大的顺序排列,则可以得到以下15种组合。
{0,1,2,3}
{0,1,2,4}
{0,1,2,5}
{0,1,3,4}
{0,1,3,5}
{0,1,4,5}
{0,2,3,4}
{0,2,3,5}
{0,2,4,5}
{0,3,4,5}
{1,2,3,4}
{1,2,3,5}
{1,2,4,5}
{1,3,4,5}
{2,3,4,5}
对于每一个位置(用同一种格式的表示对齐位),有
第0层:[0,1,2]
第1层:[1,2,3]
第2层:[2,3,4]
第3层:[3,4,5]
当第k层的值为v,则下一层的值至少不小于v+1,即下一层的相对位置必须在上一层的对齐位或对齐位之后。程序如下:

 /*RCC.c文件*/#include <stdio.h>#include <stdlib.h>int volumn,maxlevel;static void printA(int *parray,int n);void print_combine(int *dst_array,int start,int level);//打印组合数static void printA(int *parray,int n){    int i;    for(i=0;i<n;i++){        printf("%d ",parray[i]);    }    printf("\n");}//枚举组合void print_combine(int *dst_array,int start,int curlevel){   int i;   for(i=start;i<curlevel+volumn;++i){      dst_array[curlevel] = i;      if(curlevel<maxlevel){        print_combine(dst_array,i+1,curlevel+1);      }      else{        printA(dst_array,maxlevel+1);      }   }}int main(){    int n,m;//存放数据的数组,及n和m    printf("please enter a couple likes 10 2: \n");    while(scanf("%d %d",&n,&m)!=2 || m<=0 || n<m){         printf("please enter a couple n,m (n>0,m>0,m<=n) likes 10 2: \n");    }    dst_array=(int *)malloc(sizeof(int)*m);    volumn = n-m+1;//每一层所容纳的数量    maxlevel = m-1; //最大一层的序号   print_combine(dst_array,0,0);//求数组中所有数的组合    return 0;}

2.1.8解法七(原创):ICC(Iteration Chain Combination)链式迭代法
该方法是解法六的迭代算法,也是目前这里边性能最好的组合算法。程序如下:

/*ICC.c文件*/#include <stdio.h>static void printA(int *parray,int n);double print_combine(int n,int m);//打印组合static void printA(int *parray,int n){    int i;    for(i=0;i<n;i++){        printf("%d ",parray[i]);    }    printf("\n");}//枚举组合double print_combine(int n,int m){   int level,maxlevel,volumn,i,dst_array[m];   double k = 0;   if(m==1){     for(i=0;i<n;++i)        printf("%d\n",i);     return n;   }   level = 1;   volumn = n-m;   maxlevel = m-1;   dst_array[0] = 0;   while(1){     dst_array[level] = dst_array[level-1]+1;     if(level==maxlevel) {        //printA(dst_array,m);        ++k;        for(i=dst_array[level]+1;i<n;++i){            dst_array[level] = i;            //printA(dst_array,m);            ++k;        }     while(dst_array[--level]==level+volumn && level>=0);     if(level<0)  return k;     dst_array[level]++;     }     ++level;   }}int main(){    int n,m;//存放数据的数组,及n和m    printf("please enter a couple likes 10 2: \n");    while(scanf("%d %d",&n,&m)!=2 || m<=0 || n<m){         printf("please enter a couple n,m (n>0,m>0,m<=n) likes 10 2: \n");    }    print_combine(n,m);//求数组中所有数的组合    return 0;}

2.2各种解法的比较
2.2.1时间和空间复杂度
以上所有的算法的时间复杂度均为O(C(N,M)),RDC,BC10A和BC10的空间复杂度是O(n),其他的空间复杂度是O(m);–可能有误,如果介意可自行计算。
2.2.2性能图示
图1显示的是在不输出组合子集的情况下直接测试的性能。
这里写图片描述

                     图1  各种算法性能比较

由图分析可知,各种算法的性能曲线均呈现先增后减的趋势,其中RDC和BC10A,性能略差,BC10中规中矩,POC,PGC和RCC性能中上,差距不大,而较为理想的则是LDC和ICC,LDC适合于m比较接近n的场合,而ICC反之。

3.结语
枚举C(n,m)的所有组合的算法不止一种,当C(n,m)的值比较小时,使用何种算法均可以解决问题,但是一旦求解的规模足够大的时候,不同算法之间就会出现差距。为此,我们需要为解决同一个问题(即使该问题已经存在不错的解决方案),也仍需不断地去寻求新的、更好的方案,以提高生产效率,节约资源。

原创粉丝点击