尽管排列组合是生活中经常遇到的问题,可在程序设计时,不深入思考或者经验不足都让人无从下手。由于排列组合问题总是先取组合再排列,并且单纯的排列问题相对简单,所以本文仅对组合问题的实现进行详细讨论。以在n个数中选取m(0<m<=n)个数为例,问题可分解为:
1. 首先从n个数中选取编号最大的数,然后在剩下的n-1个数里面选取m-1个数,直到从n-(m-1)个数中选取1个数为止。
2. 从n个数中选取编号次小的一个数,继续执行1步,直到当前可选编号最大的数为m。
很明显,上述方法是一个递归的过程,也就是说用递归的方法可以很干净利索地求得所有组合。
下面是递归方法的实现:
/// 求从数组a[1..n]中任选m个元素的所有组合。
/// a[1..n]表示候选集,n为候选集大小,n>=m>0。
/// b[1..M]用来存储当前组合中的元素(这里存储的是元素下标),
/// 常量M表示满足条件的一个组合中元素的个数,M=m,这两个参数仅用来输出结果。
void combine( int a[], int n, int m,  int b[], const int M )

 for(int i=n; i>=m; i--)   // 注意这里的循环范围
 {
  b[m-1] = i - 1;
  if (m > 1)
   combine(a,i-1,m-1,b,M);
  else                     // m == 1, 输出一个组合
  {   
   for(int j=M-1; j>=0; j--)
    cout << a[b[j]] << " ";
   cout << endl;
  }
 }
}

因为递归程序均可以通过引入栈,用回溯转化为相应的非递归程序,所以组合问题又可以用回溯的方法来解决。为了便于理解,我们可以把组合问题化归为图的路径遍历问题,在n个数中选取m个数的所有组合,相当于在一个这样的图中(下面以从1,2,3,4中任选3个数为例说明)求从[1,1]位置出发到达[m,x](m<=x<=n)位置的所有路径:
1  2  3  4
    2  3  4
        3  4
上图是截取n×n右上对角矩阵的前m行构成,如果把矩矩中的每个元素看作图中的一个节点,我们要求的所有组合就相当于从第一行的第一列元素[1,1]出发,到第三行的任意一列元素作为结束的所有路径,规定只有相邻行之间的节点,并且下一行的节点必须处于上一行节点右面才有路径相连,其他情况都无路径相通。显然,任一路径经过的数字序列就对应一个符合要求的组合。
下面是非递归的回溯方法的实现:
/// 求从数组a[1..n]中任选m个元素的所有组合。
/// a[1..n]表示候选集,m表示一个组合的元素个数。
/// 返回所有组合的总数。
int combine(int a[], int n, int m)
{  
 m = m > n ? n : m;

int* order = new int[m+1];   
 for(int i=0; i<=m; i++)
  order[i] = i-1;            // 注意这里order[0]=-1用来作为循环判断标识
 
 int count = 0;                               
 int k = m;
 bool flag = true;           // 标志找到一个有效组合
 while(order[0] == -1)
 {
  if(flag)                   // 输出符合要求的组合
  {  
   for(i=1; i<=m; i++)                   
    cout << a[order[i]] << " ";
   cout << endl;
   count++;
   flag = false;
  }

order[k]++;                // 在当前位置选择新的数字
  if(order[k] == n)          // 当前位置已无数字可选,回溯
  {
   order[k--] = 0;
   continue;
  }    
 
  if(k < m)                  // 更新当前位置的下一位置的数字         
  {
   order[++k] = order[k-1];
   continue;
  }
 
  if(k == m)
   flag = true;
 }

delete[] order;
 return count;
}

下面是测试以上函数的程序:
int main()
{
 const int N = 4;
 const int M = 3;
 int a[N];
 for(int i=0;i<N;i++)
  a[i] = i+1;

// 回溯方法
 cout << combine(a,N,3) << endl;

// 递归方法
 int b[M];
 combine(a,N,M,b,M);

return 0;
}

由上述分析可知,解决组合问题的通用算法不外乎递归和回溯两种。在针对具体问题的时候,因为递归程序在递归层数上的限制,对于大型组合问题而言,递归不是一个好的选择,这种情况下只能采取回溯的方法来解决。

n个数的全排列问题相对简单,可以通过交换位置按序枚举来实现。STL提供了求某个序列下一个排列的算法next_permutation,其算法原理如下:
1. 从当前序列最尾端开始往前寻找两个相邻元素,令前面一个元素为*i,后一个元素为*ii,且满足*i<*ii;
2. 再次从当前序列末端开始向前扫描,找出第一个大于*i的元素,令为*j(j可能等于ii),将i,j元素对调;
3. 将ii之后(含ii)的所有元素颠倒次序,这样所得的排列即为当前序列的下一个排列。
其实现代码如下:
template <class BidirectionalIterator>
bool next_permutation(BidirectionalIterator first, BidirectionalIterator last)
{
  if (first == last) return false;   // 空範圍
  BidirectionalIterator i = first;
  ++i;
  if (i == last) return false;       // 只有一個元素
  i = last;                          // i 指向尾端
  --i;

for(;;)
 {
  BidirectionalIterator ii = i;
  --i;
  // 以上,鎖定一組(兩個)相鄰元素
  if (*i < *ii)                     // 如果前一個元素小於後一個元素
  { 
   BidirectionalIterator j = last;  // 令 j指向尾端
   while (!(*i < *--j));            // 由尾端往前找,直到遇上比 *i 大的元素
   iter_swap(i, j);                 // 交換 i, j
   reverse(ii, last);               // 將 ii 之後的元素全部逆向重排
   return true;
  }
  if (i == first)                   // 進行至最前面了
  { 
   reverse(first, last);            // 全部逆向重排
   return false;
  }
 }
}

下面程序演示了利用next_permutation来求取某个序列全排列的方法:
int main()
{
 int ia[] = {1,2,3,4};
 vector<int> iv(ia,ia+sizeof(ia)/sizeof(int));

copy(iv.begin(),iv.end(),ostream_iterator<int>(cout," "));
 cout << endl;
 while(next_permutation(iv.begin(),iv.end()))
 {
  copy(iv.begin(),iv.end(),ostream_iterator<int>(cout," "));
  cout << endl;
 }

return 0;
}
注意:上面程序中初始序列是按数值的从小到大的顺序排列的,如果初始序列无序的话,上面程序只能求出从当前序列开始的后续部分排列,也就是说next_permutation求出的排列是按排列从小到大的顺序进行的。

Copyright@戴维 2006.5  于北京

排列组合问题的通用算法相关推荐

  1. 解决排列组合问题的通用算法

    很多网友发贴询问诸如:八皇后问题.彩票问题(从m中数中选择n(m>=n)的组合)等,其实这都可归结为排列组合的问题.解决这类问题,用for循环嵌套是不现实的(只能对指定的m.n编程,而且程序看上 ...

  2. 算法 64式 17、排列组合算法整理

    1算法思想 排列组合 1.1含义 排列: 含义:从元素列表中取出指定个数的元素进行排序 公式:从n个不同元素中取出m个元素的排列个数=n!/(n-m)! 组合: 含义:从元素列表中取出指定个数的元素, ...

  3. java排列组合问题汇总【经典】

    面试或笔试中,多次遇到以下4个关于排列组合的手撕算法,这里做个笔记,方法日后查阅: 1. 无重复元素的数组,求全排列: 2. 有重复元素的数组,求全排列: 3. 无重复元素的数组,求组合[子集]: 4 ...

  4. java 数组的排列组合_java数组排列组合问题汇总

    面试或笔试中,多次遇到以下4个关于排列组合的手撕算法,这里做个笔记,方法日后查阅: 1. 无重复元素的数组,求全排列: 2. 有重复元素的数组,求全排列: 3. 无重复元素的数组,求组合[子集]: 4 ...

  5. 三个字母排列组合c语言,n个球的排列组合算法

    /*算法说明: 问题:现在有n个球,分别编号为1,2,3···n,对这n个球有多少种不重复的排列,列出所有的排列. 解:由数学知识可知:第一个球有n种选择,第二个球有n-1个选择··所以共有n!种选择 ...

  6. Algorithm:【Algorithm算法进阶之路】之算法中的数学编程相关习题(时间速度、进制转换、排列组合、条件概率、斐波那契数列)

    Algorithm:[Algorithm算法进阶之路]之算法中的数学编程相关习题(时间速度.进制转换.排列组合.条件概率.斐波那契数列) 目录 时间速度 排列组合 进制转换 条件概率 斐波那契数列 时 ...

  7. [算法] 求排列组合: 从n个数中任选m个数组成一个新数

    #include <iostream> #include <vector>using namespace std;// 求排列组合算法: C(n, m): 从n个数中任选m个数 ...

  8. 【算法】蓝桥杯dfs深度优先搜索之排列组合总结

    [导航] 上一篇文章 → <[算法]蓝桥杯dfs深度优先搜索之凑算式总结>   为了重申感谢之意,再次声明下文的大部分灵感均来自于[CSDN]梅森上校<JAVA版本:DFS算法题解两 ...

  9. C++语言基础 —— STL —— 算法 —— 排列组合算法

    [概述] 首先要了解什么是 "下一个" 排列组合,什么是 "上一个" 排列组合. 假设有三个数字组成的序列:{a,b,c} 则这个序列有6种可能的排列组合:ab ...

最新文章

  1. 基于点云方式的6D姿态识别
  2. qt 连接mysql数据库_QT连接MYSQL数据库教程
  3. IE下 c00ce56e 错误竟然是nginx 字符设置的问题
  4. 为什么存png还有白色底_PNG的算法原理
  5. Sonar与jenkins集成
  6. 如何将vue项目打包为.apk文件
  7. CVPR 2019 | 条件运动传播:从运动中学习物体性质
  8. Ansible 学习总结(1)—— Ansible 入门详解
  9. 基于类的软件复用技术
  10. 系统学习机器学习之非参数方法
  11. 解决Win11安装Keil芯片包失败/软件卡死/无法解压的问题
  12. win10易升计算机丢失,微软Windows 10易升
  13. android开发先学什么,Android开发入门教程应该先学什么
  14. html问卷调查实验报告,网上问卷调查实验报告..doc
  15. 免费又好用怎么把文字转换成语音呢?分享我常用的3个配音神器
  16. 如何将多个Excel 表合并成一个
  17. java算法:复合数据结构
  18. 20 周年会上的这条视频,看哭了 10 万阿里人
  19. 多目标线性规划(matlab编程)
  20. testufo测试刷新率测试_高刷屏比低刷屏强多少?用UFO Test工具一目了然

热门文章

  1. js正则表达式及语法
  2. 基于STM32+PS2的智能遥控小车
  3. ? .和? ?运算符
  4. 电池供电的电容麦_区别真不小 动圈麦和电容麦你了解吗?
  5. 进程同步(操作系统)
  6. 智能手机和PC降温,英伟达英特尔等把钱砸向了AI
  7. 这就是传说中的一行代码一句注释?Python每日一练----种花
  8. bsdiff算法c语言实现,bsdiff的使用
  9. strcmp函数的两种实现
  10. sort函数数组排序(c++方法)