从n个元素中生成k个排列的C语言代码

6

我基本上需要将以下Python itertools 命令的等效结果转换为C:

a = itertools.permutations(range(4),2))

目前我的过程首先涉及从10个元素中“选择”5个元素,然后为这些5个元素生成排列,如此处所示 here

这种方法的问题在于输出顺序。我需要它是(a),而我得到的是(b),如下所示。

a = itertools.permutations(range(4),2)
for i in a:
    print(i)

(0, 1)
(0, 2)
(0, 3)
(1, 0)
(1, 2)
(1, 3)
(2, 0)
(2, 1)
(2, 3)
(3, 0)
(3, 1)
(3, 2)

b = itertools.combinations(range(4),2) 
for i in b:
    c = itertools.permutations(i)
    for j in c:
        print(j)
(0, 1)
(1, 0)
(0, 2)
(2, 0)
(0, 3)
(3, 0)
(1, 2)
(2, 1)
(1, 3)
(3, 1)
(2, 3)
(3, 2)

我正在使用的另一种方法如下:
void perm(int n, int k)
{
    bool valid = true;
    int h = 0, i = 0, j = 0, limit = 1;
    int id = 0;
    int perm[10] = { 0,0,0,0,0,0,0,0,0,0 };
    for (i = 0; i < k; i++)
        limit *= n;
    for (i = 0; i < limit; i++)
    {
        id = i;
        valid = true;
        for (j = 0; j < k; j++)
        {
            perms[j] = id % n;
            id /= n;
            for (h = j - 1; h >= 0; h--)
                if (perms[j] == perms[h])
                {
                    valid = false; break;
                }
            if (!valid) break;
        }
        if (valid)
        {
            for (h = k - 1; h > 0; h--)
                printf("%d,", perms[h]);
            printf("%d\n", perms[h]);
            count++;
        }
    }
}

由于内存限制,我无法无限制地存储排列。性能需要比上面的算法更好,因为当 n 为50且 k 为10时,我最终会遍历更多无效组合(60+%)。

我知道 Heap's algorithm 可以原地生成排列,但它仍然是针对整个数组而不是像我需要的 k of n。

问题:

  1. 是否有比迭代 n^k 次更好的方法?
  2. 我能否创建一个惰性迭代器,给定当前排列即可移至下一个排列?

编辑:这不是 std::next_permutation 实现的重复,因为它将排列整个输入范围。 我已经清楚地说明了我需要 n 中长度为 k 的所有排列。如果我的范围为 10,我希望所有长度为 k(例如 5)的排列,std::next_permutation 适用于长度或排列与输入范围的长度相同的情况。

更新: 这是一个丑陋的递归 NextPerm 解决方案,大约比我的旧解决方案快4倍,并提供类似 Python 的惰性迭代器的增量 nextPerm。

int nextPerm(int perm[], int k, int n)
{
    bool invalid = true;
    int subject,i;
    if (k == 1)
    {
        if (perm[0] == n - 1)
            return 0;
        else { perm[0]=perm[0]+1; return 1; }
    }
    subject = perm[k - 1]+1;
    
    while (invalid)
    {
        if (subject == n)
        {
            subject = 0;
            if (!nextPerm(perm, k - 1, n))
                return 0;
        }
        for (i = 0; i < k-1; i++)
        {
            if (perm[i] != subject)
                invalid = false;
            else
            {
                invalid = true;subject++; break; 
            }
        }
    }
    perm[k - 1] = subject;
    return 1;
}
int main()
{
    int a, k =3 ,n = 10;
    int perm2[3] = { 0,1,2}; //starting permutation
    unsigned long long count = 0;
    int depth = 0;
    do
    {
        for (a = 0; a < k - 1; a++)
            printf("%d,", perm2[a]);
        printf("%d\n", perm2[k - 1]);
        count++;
    }
    while (nextPerm(perm2,k,n));
    printf("\n%llu", count);
    getchar();
    return 0;
}

1
@anatolyg 我需要从 n 个元素中选择 k 个排列方式,那个链接只是对完整范围进行排列。 - Siddharth Chabra
除了使用 std::next_permutation 的代码之外,这个算法 就是你要找的。该算法是回答问题 在 C++ 中生成 N 选 K 排列 的答案。关键是在每次排列后反转从 k+1n 的元素,以避免重复排列。 - Emil
@Emil 不,那是基于选择k然后对这些k进行排列的原理,排列的顺序与我所需的输出不同。请查看选择k和排列的输出行为差异以及我的期望输出。此外,需要特别使用C语言实现。 - Siddharth Chabra
我明白你的意思。这实际上让我想起了堆算法。 - Emil
@Emil 是的,我在我的问题中也链接了那个。 - Siddharth Chabra
显示剩余2条评论
1个回答

6
标准的排列算法可以通过简单的修改生成k个元素的排列序列。
按字典序排序(又称为std::next_permutation):
在C++中,可以通过使用std::next_permutation并在每次调用std::next_permutation之前反转排列的后缀n-k来生成k个元素的排列序列。
这个算法的工作原理很清楚:该算法按顺序生成排列,因此以给定前缀开头的第一个排列具有其余后缀按递增顺序排列,而具有相同前缀的最后一个排列具有按递减顺序排列的后缀。递减顺序就是递增顺序的反向,因此单个调用std::reverse就足够了。
按字典序排序的下一个排列算法非常简单:
1.从末尾向后搜索可通过将其与某个后面的元素交换而增加的元素。 2.一旦找到最右边的这样的元素,请找到最小的后续元素,可以将其与之交换。 3.将新的后缀按递增顺序排序(通过反转它,因为它先前是按递减顺序排列的)。
按字典序排序算法的优点在于它可以透明地处理具有重复元素的数组。只要任何给定元素的重复次数为O(1),next_permutation的摊销时间复杂度是O(1)(每次调用),在最坏情况下它是O(n)。在生成k个元素的排列序列时,额外的翻转会导致next_k_permutation的成本为O(n-k),如果k是固定的,则实际上是O(n)。这仍然很快,但不如非迭代算法快,后者可以维护状态而不是在第一步中进行搜索以找出要移动的元素。
以下C语言实现等同于std::reverse(); std::next_permutation();(除了它在反转之前交换元素):
#include <stddef.h>

/* Helper functions */
static void swap(int* elements, size_t a, size_t b) {
  int tmp = elements[a]; elements[a] = elements[b]; elements[b] = tmp;
}
static void flip(int* elements, size_t lo, size_t hi) {
  for (; lo + 1 < hi; ++lo, --hi) swap(elements, lo, hi - 1);
}

/* Given an array of n elements, finds the next permutation in
 * lexicographical order with a different k-prefix; in effect, it 
 * generates all k-permutations of the array.
 * It is required that the suffix be sorted in ascending order. This
 * invariant will be maintained by the function.
 * Before the first call, the array must be sorted in ascending order.
 * Returns true unless the input is the last k-permutation.
 */ 
int next_k_permutation(int* elements, size_t n, size_t k) {
  // Find the rightmost element which is strictly less than some element to its
  // right.
  int tailmax = elements[n - 1];
  size_t tail = k;
  while (tail && elements[tail - 1] >= tailmax)
    tailmax = elements[--tail];
  // If no pivot was found, the given permutation is the last one.
  if (tail) {
    size_t swap_in;
    int pivot = elements[tail - 1];
    // Find the smallest element strictly greater than the pivot, either
    // by searching forward from the pivot or backwards from the end.
    if (pivot >= elements[n - 1]) {
      for (swap_in = tail; swap_in + 1 < k && elements[swap_in + 1] > pivot; ++swap_in) {}
    } else {
      for (swap_in = n - 1; swap_in > k && elements[swap_in - 1] > pivot; --swap_in) {}
    }
    // Swap the pivots
    elements[tail - 1] = elements[swap_in];
    elements[swap_in] = pivot;
    // Flip the tail. 
    flip(elements, k, n);
    flip(elements, tail, n);
  }
  return tail;
}

以下是一个简单的驱动程序和一个示例运行:

这里有一个简单的驱动程序和一个样例运行:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int intcmp(const void* a, const void* b) {
  return *(int*)a < *(int*)b ? -1 : 
         *(int*)a > *(int*)b ?  1 :
                                0 ;
}

int main(int argc, char** argv) {
  size_t k = (argc > 1) ? atoi(argv[1]) : 0;
  if (argc < k + 2) {
    fprintf(stderr, "Usage: %s K element...\n"
                    "       where K <= number of elements\n",
                    argv[0]);
    return 1;
  }
  size_t n = argc - 2;
  int elements[n];
  for (int i = 0; i < n; ++i) elements[i] = atoi(argv[i + 2]);
  qsort(elements, n, sizeof *elements, intcmp);
  do {
    const char* delimiter = "";
    for (size_t i = 0; i < k; ++i) {
      printf("%s%2d ", delimiter, elements[i]);
      delimiter = " ";
    }
    putchar('\n');
  } while (next_k_permutation(elements, n, k));
  return 0;
}

样本运行(带有重复元素):

$ ./k_next_permutation 2 7 3 4 4 5
 3   4 
 3   5 
 3   7 
 4   3 
 4   4 
 4   5 
 4   7 
 5   3 
 5   4 
 5   7 
 7   3 
 7   4 
 7   5 

修改后的Heap算法

作为一种保持状态的算法的示例,Heap算法可以很容易地修改以生成k排列。唯一的变化在于当算法递归到 n-k位置时,k-后缀将作为k排列报告,并且(n-k)-前缀将被转换为如果Heap算法运行到结论时它将如何转换:如果长度为奇数,则将前缀反转,如果长度为偶数,则将其向左旋转一位。(顺便提一下,这是Heap算法的工作原理。)

使用递归算法有点麻烦,因为它不真正允许增量排列。但是,它很容易遵循。这里,我只是将一个函数对象传递到递归过程中,该函数对象按顺序调用每个排列。

#include <assert.h>
#include <stdbool.h>
#include <stddef.h>

/* Helper functions */
static void swap(int* elements, size_t a, size_t b) {
  int tmp = elements[a]; elements[a] = elements[b]; elements[b] = tmp;
}
static void flip(int* elements, size_t lo, size_t hi) {
  for (; lo + 1 < hi; ++lo, --hi) swap(elements, lo, hi - 1);
}
static void rotate_left(int* elements, size_t lo, size_t hi) {
  if (hi > lo) {
    int tmp = elements[lo];
    for (size_t i = lo + 1; i < hi; ++i) elements[i - 1] = elements[i];
    elements[hi - 1] = tmp;
  }
}

/* Recursive function; the main function will fill in the extra parameters */
/* Requires hi >= lo and hi >= k. Array must have size (at least) lo + k */    
static bool helper(int* array, size_t lo, size_t k, size_t hi,
                       bool(*process)(void*, int*, size_t), void* baton) {
  if (hi == lo) {
    if (!process(baton, array + lo, k)) return false;
    if (lo % 2)
      flip(array, 0, lo);
    else
      rotate_left(array, 0, lo);
  }
  else {
    for (size_t i = 0; i < hi - 1; ++i) {
      if (!helper(array, lo, k, hi - 1, process, baton))
        return false;
      swap(array, hi % 2 ? 0 : i, hi - 1);
    }
    if (!helper(array, lo, k, hi - 1, process, baton))
      return false;
  }
  return true;
}

/* Generate all k-permutations of the given array of size n.
 * The process function is called with each permutation; if it returns false,
 * generation of permutations is terminated.
 */ 
bool k_heap_permute(int* array, size_t n, size_t k,
                    bool(*process)(void*, int*, size_t), void* baton) {
  assert(k <= n);
  return helper(array, n - k, k, n, process, baton);
}

这是一个使用它的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

bool print_array(void* vf, int* elements, size_t n) {
  FILE* f = vf;
  const char* delim = "";
  for (size_t i = 0; i < n; ++i) {
    fprintf(f, "%s%2d", delim, elements[i]);
    delim = " ";
  }
  putc('\n', f);
  return true;
}

int main(int argc, char** argv) {
  size_t k = (argc > 1) ? atoi(argv[1]) : 0;
  if (argc < k + 2) {
    fprintf(stderr, "Usage: %s K element...\n"
                    "       where K <= number of elements\n",
                    argv[0]);
    return 1;
  }
  size_t n = argc - 2;
  int elements[n];
  for (int i = 0; i < n; ++i)
    elements[i] = atoi(argv[i + 2]);
  k_heap_permute(elements, n, k, print_array, stdout);
  return 0;
}

示例运行:

$ ./permut 2      1 5 9 7 3
 7  3
 9  3
 5  3
 1  3
 1  5
 7  5
 9  5
 3  5
 3  9
 1  9
 7  9
 5  9
 5  7
 3  7
 1  7
 9  7
 9  1
 5  1
 3  1
 7  1

但我正在寻找增量排列?由于这些排列作为大型k维数据集(k≥10)的索引,我需要能够访问nextPerm和可能的prevoiusPerm。 - Siddharth Chabra
你看了我的回答吗?我的算法正是你在回答的第一部分所描述的。我只是没有定义交换和反转方法,而是直接在原地完成了这些操作。也许我漏掉了什么。 - Joseph Wood
确认我不是疯了 :) 无论如何,我真的很喜欢你的解释和实现。 - Joseph Wood
1
@siddharth:也许你复制粘贴有误。我所知道的唯一MSVC不兼容性是在主函数中使用VLA,这与算法实现几乎无关。我没有MSVC,但我用gcc.godbolt.org检查了它:https://godbolt.org/g/u5kwnH(在该版本中,我将`int elements [n]更改为int elements [52]`以避免VLAs的缺失。) - rici
1
我按建议运行了k_heap_permute,其中k=5,n=52,并打印了预期的311875200个五元排列,用时144秒。当然,大部分时间都花在了打印上;当我将回调函数更改为仅计算调用次数并向辅助函数添加计数器时,它只花费了6.2秒,并报告“311875200个排列;7485004799个交换;311875200个翻转;0个左旋转”(由于它总是在递归底部做出相同的决策,所以您要么得到所有翻转,要么得到所有左旋转)。7485004799个交换是因为flip调用交换23次(以翻转47个元素),加上每个排列一个交换。 - rici
显示剩余13条评论

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