组合算法

源代码:http://blog.youkuaiyun.com/cxjddd/archive/2004/07/20/46311.aspx

组合的算法

内容简介:

  本文讲述求一种求排列组合中的“组合”的算法。本算法通过组织选入组合
的元素和未选入组合的元素之间次序,以方便求得下一个组合——更大的或更小
的。算法采用了与 STL 中的(next、prev)permutation 类似的表达方法,将
所有组合纳入一个“由小到大”的线性序列里。


关键字:

组合 人字形 算法 STL 排列


正文:

  受 STL 的排列算法(next_permutation 和 prev_permutation)的影响,
想写一个组合的算法,其结果就是一个类似于“人”字的算法。形如“人”字,
所有的元素被处理成左边升序,右边降序。


  先说一下组合之间的次序,以 {1, 2, 3, 4} 选二为例,最“小”的组合是:
{1, 2},然后是 {1, 3},{1, 4},{2, 3},{2, 4},最“大”的组合则是 {3,
4}。如果是从小到大的列出组合,则只要从剩余的元素里选出下一个更大的元素,
然后就可以很快地求出下一个更大的组合。比如对组合 {1, 2},可从剩下的 3、
4 里选取 3 来代替 2,求得下一个组合为 {1, 3}。而对组合 {2, 4},可从剩
下的 1、3 里选取 3 来代替 4 求得前一个组合 {2, 3}。


  数据表示:用左闭右开区间表示,待选元素的集合由输入区间 [first,
last) 表示,入选组合的元素在区间 [first, middle),剩下的元素在区间
[middle, last)。

 △△△◎◎◎●
 ↑  ↑  ↑
first middle last

图中“△”为组合中的元素,“◎”为剩下的元素,下同。

  区间 [first, middle) 一定是升序的;而 [middle, last) 则由升序和降
序两部分组成,如果没有左边的升序,则全部是降序,相反,没有右边的降序,
则全部是升序。而且,[first, middle) 与 [middle, last) 的升序部分是连续
的。左升右降,所以如“人”字形。以下称 [first, middle) 为前部;称
[middle, last) 为后部;且称后部中的升序部分为升部,其降序部分为降部。

      ◎     ◎    △       ◎     △   
     ◎◆    ◎◆    ■◎     △◆    △■   
    ◎◆◆   △◆◆    ■◆◎   △■◆   △■■   
   △◆◆◆   ■◆◆◎   ■◆◆◎  ■■◆◎  ■■■◎  
  △■◆◆◆  △■◆◆◆  △■◆◆◆ △■■◆◆  ■■■◆◎ 
 △■■◆◆◆ △■■◆◆◆ △■■◆◆◆ ■■■◆◆◎ ■■■◆◆◎
 ■■■◆◆◆ ■■■◆◆◆ ■■■◆◆◆ ■■■◆◆◆ ■■■◆◆◆
 ■■■◆◆◆ ■■■◆◆◆ ■■■◆◆◆ ■■■◆◆◆ ■■■◆◆◆
    ↑↑↑    ↑↑↓    ↓↓↓    ↑↓↓    ↓↓↓
   a  e   b e    be     be     be  

图中“↑”表示元素属于升部,“↓”则属于降部。

  令前部的最后一个元素是 b,则所有升部的元素都是不小于 *b 的,而所有
属于降部的元素都是小于 *b 的。

  按照这个特点,可以写出规范化的函数 adjust_combination():先将前部
和后部的元素分别排序,然后以后面的元素以 *b 为“轴”对折,即可。代码大
致如下:

? // 规范化
? adjust_combination (first, middle, last)
? {
??? sort_combination (first, middle);
??? sort_combination (middle, last);

??? j = lower_bound (middle, last, *b);
??? /* 此时 [middle, j) 为降部的元素,而 [j, last) 为升部的元素
???? */

??? reverse (j, last);??// 把 [j, last) 反转
??? reverse (middle, last);?// 再把 [middle, last) 反转
??? /* 升部还是升序,调到左边;
???? * 降部已经是降序,调到右边了。
???? */
? }

  这个算法中有两个 STL 的函数(算法),在实现中是大量使用了的。这样
的函数大概有如下几个:

iter_swap (i, j):交换两个迭代器指向的空间 *i 和 *j;

reverse (f, l):反转,把 [f, l) 里的元素前后对调;

inplace_merge (f, m, l):合并,把两个有序区间 [f, m) 和 [m, l) 合并成
有序区间 [f, l);

lower_bound (f, l, v):在有序区间 [f, l) 里找出第一个不小于 v 的位置;

upper_bound (f, l, v):在有序区间 [f, l) 里找出第一个大于 v 的位置。

  除了上面这些 STL 的函数,还有一个 sort_combination() 用来排序的,
其实现以 inplace_merge() 为辅助,使用归并排序。

  依照后部中元素与 *b 的大小关系,可以这样区分后部中的升部与降部:令
[middle, last) 中最大的元素为 e,e 后第一个小于的元素为 f(若无,则令
f = last)。则若 *e >= *b,一定有 [middle, f) 是升部,[f, last) 为降部;
相反若 *e < *b,一定有 e == middle 且 [middle, last) 为降部。


  下面以“无重复元素,组合从小到大”的条件来分析。

  先考察一下这样的程序,{1, 2, 3, 4, 5, 6} 选三:

? for (a = 1; a <= 6-2; a++)
??? for (b = a + 1; b <= 6-1; b++)
????? for (c = c + 1; c <= 6; c++)
??????? out (a, b, c);

  这个程序就是“从小到大”的,其中的 c 对应前部的最后一个元素,总体
{a, b, c} 对应前部。如果 c 的值不是最大的,则找出 c 的下一个更大的值代
替 c。如果 c 已经是最大的值,则让 b 的值变大;如果 b 也到了最大的值,
就让 a 的值变大。如果 a 不能再大,则结束。当然,a 实际最大值也就是 4,
b 是 5。如果是 a 或 b 的值变了,那么其后的元素的值也要一起改变,就如组
合 {1, 4} 变成 {2, 3}。


  从上面可以看出,最重要的事情是,从当前组合里选出要被替换的元素,从
剩下的元素里选出用来替换的元素,然后就交换、调整一下。简单地说,就是要
从前部里找出最大的可以变大的元素,并变大。这个算法里安排这样的人字形结
构,就是为了便于找出两个关键的元素。

  如果存在升部(*b < *e,或是 *b < *middle),则表示后面有比 *b 更大
的元素,所以选取升部的第一个元素 *middle 与 *b 交换。加上调整的话,可
以用“冒泡”法把 *b 往后移。

? // 存在升部,替换掉 *b
? if (*b < *e)
??? {
????? j = b;
????? i = j++;
????? while ((j != last) && (*i < *j))
??????? iter_swap (i++, j++);
??? }

  如果只有降部,那么有两种情况:一是 *first > *e,组合已经是最大的了;
另外就不是最大的组合了,可以求下一个更大的。前面一种情况很简单,调整到
最小的组合即可。后一种情况,则要求出前部中要替换掉的元素和降部中要选出
的元素。

? // 只有降部,且已经是最大的组合
? if (*e < *first)
??? {
????? reverse (first, middle);
????? reverse (first, last);
??? }

  (后一种情况)前部中要替换掉的元素 *i 可以由 *middle 求出,也就是
前部里小于 *middle 的最大的元素。求出了要被替换的元素 *i,然后就可以求
出用来替换的元素 *j 了:也就是降区里大于 *i 的最小的元素。除了交换 *i
和 *j,还要把 i 到 j 之间的元素调整好。这里先后两次很有意思,前面一次
是小于里的最大的,后面一次是大于里的最小的。

??? // 只的降部时,求下一个更大的组合
??? // 要被替换的元素:
??? i = b;
??? while (!(*--i < *middle))
????? ;
??? // 用来替换的元素:
??? j = last;
??? while (!(*i < *--j))
????? ;

??? // 交换
??? iter_swap (i, j);

??? // 调整 i 至 j 的元素
??? reverse (++i, middle);? // 最大的元素在前
??? reverse (i, j);???????? // 现在最小的元素在前了

  前面所说的,就是当“没有重复元素,且从小到大”时的算法。可以区分成
三种情况:一是存在升部;二是恰好最大;三是从降部里选出元素。这样一个处
理无重复元素的 next_combination 就出来了,也可以命名成
next_combination_unique()。


  如果有重复元素,那么就要多上“等于”比较,上面三种情况就要做些改变。
这里还是只用“小于”比较,等于和大于就都要从小于里变化出来。第一种情况
可改成 *b 小于 *middle。第二种情况要改一下判断的条件,改成 !(*first <
*e)。剩下的情况,则要分两种:一是 *b < *e,表示存在升部且有比 *b 大的
元素,从升部里找出比 *b 大的元素,替换 *b;另外则与无重复元素时的第三
种类似,从前部里找出最大的比 *e 小的元素 *i,从降部里找出最小的比 *i
大的元素 *j,交换 i 和 j 并调整。

  (汗!写到前面,发现一个思路上的 bug,还好不会出问题。)

  总结“有重复元素,从小到大”的代码大致如下:

? /* 此代码未验证,以源代码为准 */
? if (*b < *middle)
??? {
????? j = b;
????? i = j++;
????? while ((j != last) && (*i < *j))
??????? iter_swap (i++, j++);
?
????? return true;
??? }
?
? if (!(*first < *e))
??? {
????? reverse (first, middle);
????? reverse (first, last);
????? return false;
??? }

? if (*b < *e)
??? {
????? bb = b;
????? while ((++b != e) && !(*b < *bb))
??????? ;
????? reverse (bb, f);
????? reverse (b, f);
????? return true;
??? }
? else
??? {
????? i = b;
????? while (!(*--i < *e))
??????? ;
????? j = last;
????? while (!(*i < *--j))
??????? ;
????? iter_swap (i, j);
????? reverse (++i, middle);
????? reverse (i, j);
????? return true;
??? }


  下面讨论从大到小的算法,同样,先假设没有相同的元素。从大到小的算法,
简单地说是,找出最大的可以变小的元素,并变小。

  如果不存在升部(*middle < *b 或说 *e < *b),则 *middle(*e) 就是
要新选入的元素,而要选出的元素就是第一个比 *e 大的元素。这种情况是最简
单的了。

? if (*middle < *b)
??? {
????? i = upper_bound (first, middle, *middle);
????? iter_swap (i, middle);
??? }

  如果存在升部(*b < *middle),也分两种情况;一是 f == last,表示只
有升部而没有降部,到达最小的组合;另一种,*f 就是要新选入的元素,找出
第一个比 *f 大的元素,即为要选出的元素(当然,要选入选出的元素可能更
多)。

? /* 只有升部,已经是最小的组合 */
? if (f == last)
??? {
????? reverse (first, last);
????? reverse (first, middle);
??? }

  第二种情况,找出第一个比 *f 大的元素 *i,则 *i 就是要换出的元素,
而 (i, middle) 则要换成所有最大的元素(*b 应该是全部元素中最大的元素,
然后其前依次是次小的元素)。

? /* 有升部也有降部,做较大调整 */
? i = upper_bound (first, middle, *f);
? iter_swap (i, f);
? reverse (++i, f);
? reverse (i, middle);


  如果有重复元素,从大到小这种算法主要的改动是要判断换出的位置是否为
b,进行不同的处理。主要是因为相同元素的存在,使交换元素后的调整变得更
复杂一些,因而做相应变化。

? if (*middle < *b)
??? {
????? i = upper_bound (first, middle, *middle);
????? if (i != b)
??????? iter_swap (i, middle);
????? else
??????? {
?? s = middle;
?? while ((++s != last) && !(*s < *middle))
???? ;
?? reverse (b, s);
?}
????? return true;
??? }

? if (f == last)
??? {
????? reverse (first, last);
????? reverse (first, middle);
????? return false;
??? }

? i = upper_bound (first, middle, *f);
? if (i == b)
??? {
????? s = f;
????? while ((++s != last) && !(*s < *f))
??????? ;
????? reverse (b, f);
????? reverse (b, s);
??? }
? else
??? {
????? iter_swap (i, f);
????? reverse (++i, f);
????? reverse (i, middle);
??? }
? return true;


  至此,组合算法 combination 已经说完了,但是写得比较含糊。在下篇里
将细说一些东西,但完整的程序实现这里已经有了。

源代码:http://blog.youkuaiyun.com/cxjddd/archive/2004/07/20/46311.aspx

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值