从n个元素中返回k个元素的所有组合算法

642

我想编写一个函数,该函数接受字母数组作为参数以及要选择的这些字母的数量。

比如你提供了一个由8个字母组成的数组,并希望从中选择3个字母。那么你应该得到:

8! / ((8 - 3)! * 3!) = 56

由3个字母组成的返回数组(或单词)。


4
编程语言有偏好吗? - Jonathan Tran
9
你希望如何处理重复的字母? - wcm
在PHP中,以下代码应该可以解决问题:https://dev59.com/D2855IYBdhLWcg3wilCD#8880362 - Kemal Dağ
@wcm 我在这里找不到处理重复字母的解决方案。我回答了一个需要重复(并需要C ++)的问题:https://dev59.com/ll0a5IYBdhLWcg3wxbHm - Jonathan Mee
这里有一篇简明的文章,提供了一个看起来很高效的C#实现:https://msdn.microsoft.com/zh-cn/library/aa289166(v=vs.71).aspx - Angelo
显示剩余4条评论
77个回答

3

以下是我的C++解决方案:

我尽量少限制迭代器类型,因此这个解决方案只假设它是一个前向迭代器,也可以是const_iterator。这应该适用于任何标准容器。在参数没有意义的情况下,它会抛出std::invalid_argument异常。

#include <vector>
#include <stdexcept>

template <typename Fci> // Fci - forward const iterator
std::vector<std::vector<Fci> >
enumerate_combinations(Fci begin, Fci end, unsigned int combination_size)
{
    if(begin == end && combination_size > 0u)
        throw std::invalid_argument("empty set and positive combination size!");
    std::vector<std::vector<Fci> > result; // empty set of combinations
    if(combination_size == 0u) return result; // there is exactly one combination of
                                              // size 0 - emty set
    std::vector<Fci> current_combination;
    current_combination.reserve(combination_size + 1u); // I reserve one aditional slot
                                                        // in my vector to store
                                                        // the end sentinel there.
                                                        // The code is cleaner thanks to that
    for(unsigned int i = 0u; i < combination_size && begin != end; ++i, ++begin)
    {
        current_combination.push_back(begin); // Construction of the first combination
    }
    // Since I assume the itarators support only incrementing, I have to iterate over
    // the set to get its size, which is expensive. Here I had to itrate anyway to  
    // produce the first cobination, so I use the loop to also check the size.
    if(current_combination.size() < combination_size)
        throw std::invalid_argument("combination size > set size!");
    result.push_back(current_combination); // Store the first combination in the results set
    current_combination.push_back(end); // Here I add mentioned earlier sentinel to
                                        // simplyfy rest of the code. If I did it 
                                        // earlier, previous statement would get ugly.
    while(true)
    {
        unsigned int i = combination_size;
        Fci tmp;                            // Thanks to the sentinel I can find first
        do                                  // iterator to change, simply by scaning
        {                                   // from right to left and looking for the
            tmp = current_combination[--i]; // first "bubble". The fact, that it's 
            ++tmp;                          // a forward iterator makes it ugly but I
        }                                   // can't help it.
        while(i > 0u && tmp == current_combination[i + 1u]);

        // Here is probably my most obfuscated expression.
        // Loop above looks for a "bubble". If there is no "bubble", that means, that
        // current_combination is the last combination, Expression in the if statement
        // below evaluates to true and the function exits returning result.
        // If the "bubble" is found however, the ststement below has a sideeffect of 
        // incrementing the first iterator to the left of the "bubble".
        if(++current_combination[i] == current_combination[i + 1u])
            return result;
        // Rest of the code sets posiotons of the rest of the iterstors
        // (if there are any), that are to the right of the incremented one,
        // to form next combination

        while(++i < combination_size)
        {
            current_combination[i] = current_combination[i - 1u];
            ++current_combination[i];
        }
        // Below is the ugly side of using the sentinel. Well it had to haave some 
        // disadvantage. Try without it.
        result.push_back(std::vector<Fci>(current_combination.begin(),
                                          current_combination.end() - 1));
    }
}

2
这是我的JavaScript解决方案,通过使用reduce/map更加函数化,几乎消除了所有变量。

function combinations(arr, size) {
  var len = arr.length;

  if (size > len) return [];
  if (!size) return [[]];
  if (size == len) return [arr];

  return arr.reduce(function (acc, val, i) {
    var res = combinations(arr.slice(i + 1), size - 1)
      .map(function (comb) { return [val].concat(comb); });
    
    return acc.concat(res);
  }, []);
}

var combs = combinations([1,2,3,4,5,6,7,8],3);
combs.map(function (comb) {
  document.body.innerHTML += comb.toString() + '<br />';
});

document.body.innerHTML += '<br /> Total combinations = ' + combs.length;


2

基于Java解决方案的PHP算法,用于返回n个元素中k个元素的所有组合(二项式系数):

$array = array(1,2,3,4,5);

$array_result = NULL;

$array_general = NULL;

function combinations($array, $len, $start_position, $result_array, $result_len, &$general_array)
{
    if($len == 0)
    {
        $general_array[] = $result_array;
        return;
    }

    for ($i = $start_position; $i <= count($array) - $len; $i++)
    {
        $result_array[$result_len - $len] = $array[$i];
        combinations($array, $len-1, $i+1, $result_array, $result_len, $general_array);
    }
} 

combinations($array, 3, 0, $array_result, 3, $array_general);

echo "<pre>";
print_r($array_general);
echo "</pre>";

相同的解决方案,但使用JavaScript实现:
var newArray = [1, 2, 3, 4, 5];
var arrayResult = [];
var arrayGeneral = [];

function combinations(newArray, len, startPosition, resultArray, resultLen, arrayGeneral) {
    if(len === 0) {
        var tempArray = [];
        resultArray.forEach(value => tempArray.push(value));
        arrayGeneral.push(tempArray);
        return;
    }
    for (var i = startPosition; i <= newArray.length - len; i++) {
        resultArray[resultLen - len] = newArray[i];
        combinations(newArray, len-1, i+1, resultArray, resultLen, arrayGeneral);
    }
} 

combinations(newArray, 3, 0, arrayResult, 3, arrayGeneral);

console.log(arrayGeneral);

抱歉,变量$array、$len、$start_position、$result_array、$result_len和&$general_array是用来做什么的? - Robert Johnstone
combinations函数是一个递归函数。它会迭代自身,直到$len等于0。它使用$array、$start_position、$result_array和$result_len进行本地计算,并使用&$general_array作为参数接受通过引用传递的变量,以便可以修改$general_array变量并在不同调用之间保持值。希望这可以帮助您。 - quAnton
如果您想知道如何使用它,则$array包含从中制作组合的值。$len是组合的长度,$start_position指示从哪个位置开始获取元素。如果将$start_position = 1设置为仅计算2,3,4,5而不是1,2,3,4,5。$result_array用于每次调用时的临时计算。$result_len必须与$len具有相同的值。&general_array保存结果或组合。 - quAnton

2

JavaScript,基于生成器的递归方法:

最初的回答:

function *nCk(n,k){
  for(var i=n-1;i>=k-1;--i)
    if(k===1)
      yield [i];
    else
      for(var temp of nCk(i,k-1)){
        temp.unshift(i);
        yield temp;
      }
}

function test(){
  try{
    var n=parseInt(ninp.value);
    var k=parseInt(kinp.value);
    log.innerText="";
    var stop=Date.now()+1000;
    if(k>=1)
      for(var res of nCk(n,k))
        if(Date.now()<stop)
          log.innerText+=JSON.stringify(res)+" ";
        else{
          log.innerText+="1 second passed, stopping here.";
          break;
        }
  }catch(ex){}
}
n:<input id="ninp" oninput="test()">
&gt;= k:<input id="kinp" oninput="test()"> &gt;= 1
<div id="log"></div>

这种方式(通过减少i和使用unshift())可以按照递减的顺序生成组合和组合内的元素,看起来更加美观。
测试在1秒后停止,因此输入奇怪的数字相对较安全。"最初的回答"

2

跟风并发布另一个解决方案。这是一个通用的Java实现。输入:(int k) 是要选择的元素数量,(List<T> list) 是要选择的列表。返回组合的列表 (List<List<T>>)

public static <T> List<List<T>> getCombinations(int k, List<T> list) {
    List<List<T>> combinations = new ArrayList<List<T>>();
    if (k == 0) {
        combinations.add(new ArrayList<T>());
        return combinations;
    }
    for (int i = 0; i < list.size(); i++) {
        T element = list.get(i);
        List<T> rest = getSublist(list, i+1);
        for (List<T> previous : getCombinations(k-1, rest)) {
            previous.add(element);
            combinations.add(previous);
        }
    }
    return combinations;
}

public static <T> List<T> getSublist(List<T> list, int i) {
    List<T> sublist = new ArrayList<T>();
    for (int j = i; j < list.size(); j++) {
        sublist.add(list.get(j));
    }
    return sublist;
}

2
《计算机程序设计艺术》第4A卷:组合算法,第1部分,7.2.1.3节中的L算法(字典序组合)的C代码如下:
#include <stdio.h>
#include <stdlib.h>

void visit(int* c, int t) 
{
  // for (int j = 1; j <= t; j++)
  for (int j = t; j > 0; j--)
    printf("%d ", c[j]);
  printf("\n");
}

int* initialize(int n, int t) 
{
  // c[0] not used
  int *c = (int*) malloc((t + 3) * sizeof(int));

  for (int j = 1; j <= t; j++)
    c[j] = j - 1;
  c[t+1] = n;
  c[t+2] = 0;
  return c;
}

void comb(int n, int t) 
{
  int *c = initialize(n, t);
  int j;

  for (;;) {
    visit(c, t);
    j = 1;
    while (c[j]+1 == c[j+1]) {
      c[j] = j - 1;
      ++j;
    }
    if (j > t) 
      return;
    ++c[j];
  }
  free(c);
}

int main(int argc, char *argv[])
{
  comb(5, 3);
  return 0;
}

2
我写了一个类来处理与二项式系数有关的常见函数,这也是你的问题所属的类型。它执行以下任务:
  1. 将任何N选K的所有K索引以漂亮的格式输出到文件中。 K索引可以替换为更具描述性的字符串或字母。 这种方法使解决这种类型的问题非常简单。

  2. 将K索引转换为排序后的二项式系数表中条目的正确索引。 这种技术比依赖迭代的旧技术快得多。 它通过使用帕斯卡三角形固有的数学属性来实现。 我的论文谈到了这一点。 我相信我是第一个发现并发布这种技术的人,但我可能是错的。

  3. 将排序后的二项式系数表中的索引转换为相应的K索引。

  4. 使用Mark Dominus方法计算二项式系数,这样不太可能溢出,并且可以处理更大的数字。

  5. 该类是用.NET C#编写的,并提供一种使用通用列表管理与问题相关的对象的方法。 该类的构造函数采用名为InitTable的布尔值,当为true时将创建一个通用列表以保存要管理的对象。 如果该值为false,则不会创建表格。 不需要创建表格即可执行上述4种方法。 提供访问器方法以访问表格。

  6. 有一个相关的测试类,展示了如何使用类及其方法。 它已经通过了2个案例的广泛测试,并且没有已知的错误。

要阅读有关此类并下载代码的信息,请参见Tablizing The Binomial Coeffieicent

将此类转换为C ++应该不难。


将其称为“马克·多米努斯方法”实际上是不正确的,因为正如我所提到的,它至少有850年的历史,而且并不难想到。为什么不称之为“Lilavati方法”呢? - Mark Dominus

1
我们可以使用比特的概念来实现这一点。假设我们有一个字符串 "abc",并且我们想要得到所有长度为 2 的元素组合(即 "ab"、"ac"、"bc")。我们可以找到从 1 到 2^n(不包括)范围内的数字中设置的比特集。在这里是从 1 到 7,无论我们在哪里设置了比特 = 2,我们都可以从字符串中打印相应的值。
例如:
- 1 - 001 - 2 - 010 - 3 - 011 -> 打印 ab (str[0],str[1]) - 4 - 100 - 5 - 101 -> 打印 ac (str[0],str[2]) - 6 - 110 -> 打印 ab (str[1],str[2]) - 7 - 111。
代码示例:
public class StringCombinationK {   
    static void combk(String s , int k){
        int n = s.length();
        int num = 1<<n;
        int j=0;
        int count=0;

        for(int i=0;i<num;i++){
            if (countSet(i)==k){
                setBits(i,j,s);
                count++;
                System.out.println();
            }
        }

        System.out.println(count);
    }

    static void setBits(int i,int j,String s){ // print the corresponding string value,j represent the index of set bit
        if(i==0){
            return;
        }

        if(i%2==1){
            System.out.print(s.charAt(j));                  
        }

        setBits(i/2,j+1,s);
    }

    static int countSet(int i){ //count number of set bits
        if( i==0){
            return 0;
        }

        return (i%2==0? 0:1) + countSet(i/2);
    }

    public static void main(String[] arhs){
        String s = "abcdefgh";
        int k=3;
        combk(s,k);
    }
}

1
我正在寻找PHP的类似解决方案,然后发现了以下内容。
class Combinations implements Iterator
{
    protected $c = null;
    protected $s = null;
    protected $n = 0;
    protected $k = 0;
    protected $pos = 0;

    function __construct($s, $k) {
        if(is_array($s)) {
            $this->s = array_values($s);
            $this->n = count($this->s);
        } else {
            $this->s = (string) $s;
            $this->n = strlen($this->s);
        }
        $this->k = $k;
        $this->rewind();
    }
    function key() {
        return $this->pos;
    }
    function current() {
        $r = array();
        for($i = 0; $i < $this->k; $i++)
            $r[] = $this->s[$this->c[$i]];
        return is_array($this->s) ? $r : implode('', $r);
    }
    function next() {
        if($this->_next())
            $this->pos++;
        else
            $this->pos = -1;
    }
    function rewind() {
        $this->c = range(0, $this->k);
        $this->pos = 0;
    }
    function valid() {
        return $this->pos >= 0;
    }

    protected function _next() {
        $i = $this->k - 1;
        while ($i >= 0 && $this->c[$i] == $this->n - $this->k + $i)
            $i--;
        if($i < 0)
            return false;
        $this->c[$i]++;
        while($i++ < $this->k - 1)
            $this->c[$i] = $this->c[$i - 1] + 1;
        return true;
    }
}


foreach(new Combinations("1234567", 5) as $substring)
    echo $substring, ' ';

来源

我不太确定这个类的效率如何,但我只是用它来做种子。


1
这里是一些简单的代码,打印出所有的C(n,m)组合。它通过初始化一组数组索引并将其移动到下一个有效组合来实现。索引被初始化为指向最低的m个索引(按字典顺序排序的最小组合)。然后从第m个索引开始,我们尝试向前移动索引。如果索引已经达到其限制,则尝试上一个索引(一直到索引1)。如果我们可以向前移动一个索引,则重置所有更大的索引。
m=(rand()%n)+1; // m will vary from 1 to n

for (i=0;i<n;i++) a[i]=i+1;

// we want to print all possible C(n,m) combinations of selecting m objects out of n
printf("Printing C(%d,%d) possible combinations ...\n", n,m);

// This is an adhoc algo that keeps m pointers to the next valid combination
for (i=0;i<m;i++) p[i]=i; // the p[.] contain indices to the a vector whose elements constitute next combination

done=false;
while (!done)
{
    // print combination
    for (i=0;i<m;i++) printf("%2d ", a[p[i]]);
    printf("\n");

    // update combination
    // method: start with p[m-1]. try to increment it. if it is already at the end, then try moving p[m-2] ahead.
    // if this is possible, then reset p[m-1] to 1 more than (the new) p[m-2].
    // if p[m-2] can not also be moved, then try p[m-3]. move that ahead. then reset p[m-2] and p[m-1].
    // repeat all the way down to p[0]. if p[0] can not also be moved, then we have generated all combinations.
    j=m-1;
    i=1;
    move_found=false;
    while ((j>=0) && !move_found)
    {
        if (p[j]<(n-i)) 
        {
            move_found=true;
            p[j]++; // point p[j] to next index
            for (k=j+1;k<m;k++)
            {
                p[k]=p[j]+(k-j);
            }
        }
        else
        {
            j--;
            i++;
        }
    }
    if (!move_found) done=true;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接