组合算法实现
从m个数里面取n个数的算法。最容易理解的就是递归,但是其效率太低。
实现方法一:
// 组合算法
// 本程序的思路是开一个数组,其下标表示1到m个数,数组元素的值为1表示其下标
// 代表的数被选中,为0则没选中。
// 首先初始化,将数组前n个元素置1,表示第一个组合为前n个数。
// 然后从左到右扫描数组元素值的“10”组合,找到第一个“10”组合后将其变为
// “01”组合,同时将其左边的所有“1”全部移动到数组的最左端。
// 当第一个“1”移动到数组的m-n的位置,即n个“1”全部移动到最右端时,就得
// 到了最后一个组合。
// 例如求5中选3的组合:
// 1 1 1 0 0 //1,2,3
// 1 1 0 1 0 //1,2,4
// 1 0 1 1 0 //1,3,4
// 0 1 1 1 0 //2,3,4
// 1 1 0 0 1 //1,2,5
// 1 0 1 0 1 //1,3,5
// 0 1 1 0 1 //2,3,5
// 1 0 0 1 1 //1,4,5
// 0 1 0 1 1 //2,4,5
// 0 0 1 1 1 //3,4,5
import java.util.ArrayList;
import java.util.List;
/**
* 面试中遇到的问题,在网上查找资料,加上自己的总结, java 代码实现组合的算法
* 从n个数里取出m个数的组合是n*(n-1)*...*(n-m+1)/m*(m-1)*...2*1 该方法比较好理解,但具体算法的分析却有难度。
*
* @date
* @Java教程:http://www.javaweb.cc
*
*/
class Zuhe1 {
/**
* @param a:组合数组
* @param k:生成组合个数
* @return :所有可能的组合数组列表
*/
private List zuhe(int[] a, int m) {
Zuhe1 zuhe = new Zuhe1();
List list = new ArrayList();
int n = a.length;
boolean flag = false; // 是否是最后一种组合的标记
// 生成辅助数组。首先初始化,将数组前n个元素置1,表示第一个组合为前n个数。
int[] tempNum = new int[n];
for (int i = 0; i < n; i++) {
if (i < m) {
tempNum[i] = 1;
} else {
tempNum[i] = 0;
}
System.out.print(tempNum[i]);
}
print(tempNum);// 打印辅助数组
list.add(zuhe.createResult(a, tempNum, m));// 打印第一中默认组合
do {
int pose = 0; // 记录改变的位置
int sum = 0; // 记录改变位置 左侧 1 的个数
// 然后从左到右扫描数组元素值的“10”组合,找到第一个“10”组合后将其变为“01”
for (int i = 0; i < (n - 1); i++) {
if (tempNum[i] == 1 && tempNum[i + 1] == 0) {
tempNum[i] = 0;
tempNum[i + 1] = 1;
pose = i;
break;
}
}
print(tempNum);// 打印辅助数组
list.add(zuhe.createResult(a, tempNum, m));// 打印第一中默认组合
// 同时将其左边的所有“1”全部移动到数组的最左端。
for (int i = 0; i < pose; i++) {
if (tempNum[i] == 1)
sum++;
}
for (int i = 0; i < pose; i++) {
if (i < sum)
tempNum[i] = 1;
else
tempNum[i] = 0;
}
// 判断是否为最后一个组合:当第一个“1”移动到数组的m-n的位置,即n个“1”全部移动到最右端时,就得到了最后一个组合。
flag = false;
for (int i = n - m; i < n; i++) {
if (tempNum[i] == 0)
flag = true;
}
} while (flag);
return list;
}
// 根据辅助数组和原始数组生成 结果数组
public int[] createResult(int[] a, int[] temp, int m) {
int[] result = new int[m];
int j = 0;
for (int i = 0; i < a.length; i++) {
if (temp[i] == 1) {
result[j] = a[i];
System.out.println("result[" + j + "]:" + result[j]);
j++;
}
}
return result;
}
// 打印
public void print1(List list) {
for (int i = 0; i < list.size(); i++) {
System.out.println();
int[] temp = (int[]) list.get(i);
for (int j = 0; j < temp.length; j++) {
System.out.print(temp[j] + " ");
}
}
}
// 打印整数数组的方法
public void print(int[] a) {
System.out.println("生成的辅助数组为:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]);
}
System.out.println();
}
public static void main(String[] args) {
int[] a = { 1, 2, 3, 4, 5 }; // 整数数组
int m = 3; // 待取出组合的个数
Zuhe1 zuhe = new Zuhe1();
List list = zuhe.zuhe(a, m);
zuhe.print1(list);
}
}
解决这类问题,用for循环嵌套是不现实的(只能对指定的m、n编程,而且程序看上去异常繁琐),较好的方法是回朔法。下面给出这类问题的一般算法的c/c++描述:
int combine(int a[],int sub){
//a[1..?]表示候选集,sub表示一个排列(组合)的元素个数
{
int total=sizeof(a);
int order[sub+1];
int count=0;//符合条件的排列(组合)的个数
order[0]=-1;
for(int i=1;i<=sub;i++)
order[i]=i;
int k=sub;
bool flag=true;
while(order[0]!=-1){
if(flag){
for(i=1;i<=sub;i++)//输出符合要求的组合
printf("%d ",a[order[i]]);
printf("\n");
count++;
flag=false;
}
order[k]++;
if(order[k]==total+1){
order[k--]=0;
continue;
}
...
//在此加入order[k]的限制条件
//如果条件满足,则往下执行
//否则continue;
if(k<sub){
order[++k]=order[k-1];
continue;
}
if(k==sub)
flag=true;
}
return count;
}
/*
*组合 回溯
*a为源数据,调用时用f(a,0,"")
*/
void f(int[] a,int n,String v){
if(n==a.length){
System.out.println(v);
}else{
f(a,n+1,v);
f(a,n+1,v+","+a[n]);
}
}
public class NAllArrangement {
private int count = 0; // 解数量
private int n; // 输入数据n
private int[] a; // 解向量
private int[] d; // 解状态
/**
* @param args
*/
public static void main(String[] args) {
// 测试例子
NAllArrangement na = new NAllArrangement(4, 100);
na.tryArrangement(1);
}
public NAllArrangement(int _n, int maxNSize) {
n = _n;
a = new int[maxNSize];
d = new int[maxNSize];
}
/**
* 处理方法
*
* @param k
*/
public void tryArrangement(int k) {
for (int j = 1; j <= n; j++) { // 搜索解空间
if (d[j] == 0) {
a[k] = j;
d[j] = 1;
} else { // 表明j已用过
continue;
}
if (k < n) { // 没搜索到底
tryArrangement(k + 1);
} else {
count++;
output(); // 输出解向量
}
d[a[k]] = 0; // 回溯
}
}
/**
* 输出解向量
*/
private void output() {
System.out.println("count = " + count);
for (int j = 1; j <= n; j++) {
System.out.print(a[j] + " ");
}
System.out.println("");
}
}
回溯法是基本算法的一种,可以用于解决大致这样的问题:假设我们有一个N个元素的集合{N},现在要依据该集合生成M个元素的集合{M},每一个元素的生成都依据一定的规则CHECK。
用回溯法解决此问题,我们可以划分为三个重要组成部分。
步骤
从第一步开始至第M步,每一步都从{N}中选取一个元素放入结果{M}中。
界定
每次选择一个元素时,我们都要用规则CHECK来界定{N}中的元素谁合适。界定规则的描述将决定算法的效率和性能。
回溯
如果第k步不能找到合适的元素或者需要得到更多的结果,返回到第k-1步,继续选择下一个第k-1步的元素。
让我们来运用以上的描述和C++语言来解决数学排列和组合问题。
问题1:从N个元素中选择M个元素进行排列,列出所有结果。
//
permutation.h:Listallthepermutationsubsetsofacertianset.
//
#pragma
once

#include
<
iostream
>
#include
<
iomanip
>
using
namespace
std;
//
ComputeP(N,M).Nisthetotalnumber,Mistheselectednumber.
template
<
int
N,
int
M
>
class
CPermutation
{
private:
int**m_result;//two-dimensionarrayof[m_nCount][M]
intm_nCount;//howmanyresults
intm_nIndex;//0-m_nCount-1
public:
//Listallpossibleresultsbynesting
voidCount()
{
m_nIndex=0;
intresult[N],used[N];
for(inti=0;i<N;i++)
used[i]=0;
CountRecur(result,used,0);
}
voidCountRecur(intresult[M],intused[M],inti)
{
for(intk=0;k<N;k++)
{
if(used[k])
continue;
result[i]=k;
used[k]=1;
if(i<M-1)
CountRecur(result,used,i+1);
else
Add(result);
used[k]=0;
}
}
//Savetheresult
voidAdd(intsz[M])
{
memcpy(m_result[m_nIndex],sz,M*sizeof(int));
++m_nIndex;
}
//Countthenumberofsubsets
//C(N,M)=N!/((N-M)!*M!)
staticintNumberOfResult()
{
if(N<=0||M<=0||M>N)
return0;
intresult=1;
for(inti=0;i<M;i++)
result*=N-i;
returnresult;
}
//Printthemtothestandardoutputdevice
voidPrint()
{
for(inti=0;i<m_nCount;i++)
{
cout<<setw(3)<<setfill('')<<i+1<<":";
for(intj=0;j<M;j++)
cout<<setw(3)<<setfill('')<<m_result[i][j]+1;
cout<<endl;
}
}
CPermutation()
{
//allocatememoriesfortheresult
m_nCount=NumberOfResult();
m_result=newint*[m_nCount];
for(inti=0;i<m_nCount;i++)
m_result[i]=newint[M];
}
~CPermutation()
{
//deallocatememoriesfortheresult
for(inti=0;i<m_nCount;i++)
delete[]m_result[i];
delete[]m_result;
}
}
;
问题2:从N个元素中选择M个元素进行组合,列出所有结果。
与排列不同的是,组合不需要对选择出来的M个元素进行排列,不妨假定每一组结果中的M个元素从小到大排列。
//
combination.h:listallthesubsetsofacertianset
//
#pragma
once

#include
<
iostream
>
#include
<
iomanip
>
using
namespace
std;
//
ListalltheM-subsetsoftheN-set
template
<
int
N,
int
M
>
class
CCombination
{
private:
int**m_result;//two-dimensionarrayofm_nCount*M
intm_nCount;//howmanyresultsofM-lengtharray
public:
CCombination()
{
//allocatememoriesfortheresult
m_nCount=NumberOfResult();
m_result=newint*[m_nCount];
for(inti=0;i<m_nCount;i++)
m_result[i]=newint[M];
}
~CCombination()
{
//deallocatememoriesfortheresult
for(inti=0;i<m_nCount;i++)
delete[]m_result[i];
delete[]m_result;
}
//processofcounting
voidCount()
{
intsz[M];
intnResultCount=0;
CountRecur(sz,0,0,nResultCount);
}
//Printthemtothestandardoutputdevice
voidPrint()
{
usingstd::cout;
usingstd::setw;
usingstd::setfill;
usingstd::endl;
for(inti=0;i<m_nCount;i++)
{
cout<<setw(3)<<setfill('')<<i+1<<":";
for(intj=0;j<M;j++)
cout<<setw(3)<<setfill('')<<m_result[i][j]+1;
cout<<endl;
}
}
private:
//Countthenumberofsubsets
//C(N,M)=N!/((N-M)!*M!)
intNumberOfResult()
{
intresult=1;
for(inti=0;i<M;i++)
result*=N-i;
for(intj=1;j<=M;j++)
result/=j;
returnresult;
}
//Getthecurrentvalue
//sz-arrayofthecurrentresult
//nIndex-indexofsz,0<=nIndex<M
//nStartVal-thecurrentminimumvalue,0<=nStartVal<N
//nResultCount-indexofm_result
voidCountRecur(intsz[M],intnIndex,intnStartVal,int&nResultCount)
{
if(nStartVal+M-nIndex>N)
return;
for(inti=nStartVal;i<N;i++)
{
sz[nIndex]=i;
if(nIndex==M-1)
Add(sz,nResultCount);
else
CountRecur(sz,nIndex+1,i+1,nResultCount);
}
}
//Savetheresult
voidAdd(int*sz,int&nIndex)
{
memcpy(m_result[nIndex],sz,M*sizeof(int));
++nIndex;
}
}
;
当然,如果将回溯法运用到工程中去,正确性只是必要条件之一,效率显得相当重要,高效的界定代码则是其中的关键。甚至可以考虑换一种方法来实现,当然回溯的思想应该都一样。下面一段代码实现N的阶乘(排列的一个特例),是我在工程中的一个应用。思路则是假设我们已经按照一种方式列出了所有结果
1, 2, 3
1, 3, 2
2, 1, 3
2, 3, 1
3, 1, 2
3, 2, 1
然后纵向的填写所有结果至结果数组中。
//
pi:thisalgorithlistsallthesequencesofacertianarray
//
#pragma
once

#include
"
common.h
"

//
howmanysequenceforNcells
int
NumberOfPI(
int
N)
{
if(N<0)
return0;
if(N==0)
return1;
intpi=1;
for(inti=1;i<=N;i++)
pi*=i;
returnpi;
}

//
printallthesequencetoacertainarray
//
N-numberofcells
//
PI-arrayofcells
//
selPI-indexofarraywithinitialvalueof0andmaximumvalueofN-1
//
Index-resultarray
//
selIndex-numberofresultarray
void
PrintPI(
int
N,
int
*
PI,
int
selPI,
int
**
Index,
int
selIndex)
{
if(N<=0)
return;
intnum=NumberOfPI(N-selPI-1);
for(inti=selPI;i<N;i++)
{
swap(PI[selPI],PI[i]);
PrintPI(N,PI,selPI+1,Index,selIndex);
for(intj=0;j<num;j++)
Index[selIndex++][selPI]=PI[selPI];
swap(PI[selPI],PI[i]);
}
}

int
PrintPITest()
{
//Number
staticconstintN=5;
//Resultinallsequences
intPI[N];
for(intl=0;l<N;l++)
PI[l]=l+1;
//Allocatethetwo_demensionarray
constintnum=NumberOfPI(N);
int**Index;
Index=newint*[num];
for(intl=0;l<num;l++)
Index[l]=newint[N];
//Calculating.
PrintPI(N,PI,0,Index,0);
//Printittocout
for(inti=0;i<num;i++)
{
printf("%5d:",i+1);
for(intj=0;j<N;j++)
printf("%2d",Index[i][j]);
printf("/n");
}
//Checkifthereisanyidenticalindexes.
for(inti=0;i<num-1;i++)
for(intj=i+1;j<num;j++)
{
intk=0;
for(;k<N;k++)
if(Index[i][k]!=Index[j][k])
break;
if(k==N)
printf("CheckERROR!/n");
}
//Releasethetwo_demensionarray
for(intl=0;l<num;l++)
delete[]Index[l];
delete[]Index;
return0;
}
事实上,如果你在工程应用中用到的只是可列举的排列组合结果,你应该考虑把结果直接编码到程序中去,这样最快了,只是扩展性不好罢了。
408

被折叠的 条评论
为什么被折叠?



