组合算法

组合的算法    出处:http://community.csdn.net/Expert/topic/3143/3143703.xml?temp=.5916101 

                       原作者:cxjddd (空浮无根)

内容简介:

  本文讲述求一种求排列组合中的“组合”的算法。本算法通过组织选入组合
的元素和未选入组合的元素之间次序,以方便求得下一个组合——更大的或更小
的。算法采用了与 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 已经说完了,但是写得比较含糊。在下篇里
将细说一些东西,但完整的程序实现这里已经有了。

template
void
sort_combination (BiIterator first, BiIterator last)
{
  if (first == last) // $(AC;SPT*KX
(B    return;

  BiIterator i = first;
  ++i;
  if (i == last)     // $(AR;8vT*KX
(B    return;
 
  int half = distance (first, last) / 2;  // half of the length
  BiIterator middle = first;
  advance (middle, half);  // middle += half

  sort_combination (first, middle);     // sort first part
  sort_combination (middle, last);      // sort second part

  inplace_merge (first, middle, last);  // merge two parts
}

template
void
adjust_combination (BiIterator first, BiIterator middle, BiIterator last)
{
  // the front part or the back part have no elements
  if ((first == middle) || (middle == last))
    return;

  sort_combination (first, middle);
  sort_combination (middle, last);

  BiIterator b = middle;
  --b;
  BiIterator j = lower_bound (middle, last, *b);
  reverse (j, last);
  reverse (middle, last);
}

template
void
init_combination (BiIterator first, BiIterator middle, BiIterator last,
    bool min)
{
  sort_combination (first, last);

  if (min == false)
    {
      // the max combination
      reverse (first, last);
      reverse (first, middle);
    }
}

template
bool
next_combination (BiIterator first, BiIterator middle, BiIterator last)
{
  if ((first == middle) || (middle == last))
    return false;

  // last element of [first, middle)
  BiIterator b = middle;
  --b;

  if (*b < *middle)
    {
      BiIterator j = b;
      while ((++b != last) && (*j < *b))
 {
   iter_swap (j, b);
   j = b;
 }
      return true;
    }

  BiIterator e = last;
  --e;
  while (e != middle)
    {
      BiIterator k = e;
      --k;
      if (!(*k < *e))
 e = k;
      else
 break;
    }
 
  BiIterator f = e;
  ++f;
  while ((f != last) && !(*f < *e))
    ++f;

  if (!(*first < *e))
    {
      reverse (first, middle);
      reverse (first, last);
      return false;
    }

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

      iter_swap (i, j);
      reverse (++i, middle);
      reverse (i, j);
    }
  return true;
}

template
bool
prev_combination (BiIterator first, BiIterator middle, BiIterator last)
{
  if ((first == middle) || (middle == last))
    return false;
 
  BiIterator b = middle;
  --b;
 
  if (*middle < *b)
    {
      BiIterator i = upper_bound (first, middle, *middle);
      if (i != b)
 iter_swap (i, middle);
      else
 {
   BiIterator s = middle;
   while ((++s != last) && !(*s < *middle))
     ;
   reverse (b, s);
 }

      return true;
    }
 
  BiIterator e = last;
  --e;
  while (e != middle)
    {
      BiIterator k = e;
      --k;
      if (!(*k < *e))
 e = k;
      else
 break;
    }
 
  BiIterator f = e;
  ++f;
  while ((f != last) && !(*f < *e))
    ++f;

  if (/*!(*e < *b) && */(f == last))
    {
      reverse (first, last);
      reverse (first, middle);
      return false;
    }

  BiIterator i = upper_bound (first, middle, *f);
  if (i == b)
    {
      BiIterator 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;
}

 

你可能感兴趣的:(算法)