从 C 中的 n 生成 k 个排列

Posted

技术标签:

【中文标题】从 C 中的 n 生成 k 个排列【英文标题】:Generating k permutations from n in C 【发布时间】:2018-07-11 13:47:13 【问题描述】:

我基本上需要 C 中以下 Python itertools 命令的等效结果:

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。

问题。

    有没有比迭代 n^k 次更好的方法? 我可以创建一个惰性迭代器,在给定当前排列的情况下移动到下一个排列吗?

EDIT 这不是 std::next_permutation 实现的副本,因为它将置换整个输入范围。 我已经明确提到我需要 n 个排列中的 k 个。即,如果我的范围是 10,我希望所有长度(k)的排列都说 5,当长度或排列与输入范围的长度相同时,std::next_permutation 起作用

更新 这是一个丑陋的递归 NextPerm 解决方案,它比我的旧解决方案快 4 倍,并且提供增量 nextPerm,就像 Python 惰性迭代器一样。

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;

【问题讨论】:

@anatolyg 我需要从 n 个排列中选择 k 个,该链接只排列一个完整的范围 除了使用std::next_permutation 的代码,this algorithm 是您正在寻找的。该算法是对以下问题的回答:Generating N choose K Permutations in C++。关键是在每次排列后将元素从k+1反转为n,以避免重复排列。 @Emil no,它的工作原理是选择 k 然后排列那些 k,排列的顺序与我想要的输出不同。查看选择 k 和 permute 输出的行为差异以及我想要的输出。还特别需要在C中 我明白你在说什么。这实际上让我想起了堆的算法。 @Emil 是的,在我的问题中也有联系 【参考方案1】:

对标准排列算法进行简单修改,将产生 k 排列。

按字典顺序排列(又名std::next_permutation

在 C++ 中,k 排列可以通过使用std::next_permutation 的简单权宜之计生成,并且只需在每次调用std::next_permutation 之前反转排列的n-k-后缀。

它的工作原理相当清楚:该算法按顺序生成排列,因此以给定前缀开头的第一个排列的剩余后缀按升序排列,而具有相同前缀的最后一个排列的后缀按降序排列。递减顺序与递增顺序相反,因此只需调用std::reverse 即可。

字典序next-permutation算法非常简单:

    从末尾向后搜索一个可以通过将其与后面的元素交换来增加的元素。

    找到最右边的此类元素后,找到可以与之交换的最小的后续元素,并将它们交换。

    将新后缀按升序排序(通过反转它,因为它以前是降序的)。

字典式算法的一个优点是它可以透明地处理具有重复元素的数组。只要任何给定元素的重复次数为 O(1),next-permutation 的摊销为 O(1)(每次调用),在最坏的情况下为 O(n)。在生成 k 排列时,额外的翻转导致 next_k_permutation 的成本为 O(n-k),如果 k 固定,则实际上是 O(n)。这仍然相当快,但不如非迭代算法快,后者可以保持状态,而不是在步骤 1 中进行搜索以确定要移动的元素。

下面的 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 的算法可以很容易地修改以产生 k 排列。唯一的变化是,当算法递归到位置n - k 时,k-suffix 被报告为 k-permutation,并且 (n-k)-prefix 的转换方式与 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 你看我的回答了吗?我的算法正是您在答案的第一部分中描述的。我只是没有定义交换和反向方法,而是直接在原地做了。也许我错过了什么。 @joseph:是的,它们本质上是一样的,现在我看了一下。也许我应该在一开始就离开堆的 k 置换解决方案。 @siddharth:也许你复制+粘贴不正确。我知道的唯一 MSVC 不兼容是在 main 函数中使用了 VLA,这与算法实现几乎没有关系。我没有 MSVC,但我通过 gcc.godbolt.org 进行了检查:godbolt.org/g/u5kwnH(我在该版本中将int elements[n] 更改为int elements[52],以避免缺少 VLA。) 我按照建议运行了 k=5 和 n=52 的 k_heap_permute,它在 144 秒内打印了预期的 311875200 个 5 排列。当然,大部分时间是打印;当我将回调更改为只计算调用并向辅助函数添加计数器时,它花费了 6.2 秒并报告了“311875200 排列;7485004799 交换;311875200 翻转;0 rotate_lefts”。 (因为它总是在递归的底部做出相同的决定,所以你要么得到所有的翻转,要么得到所有的rotate_lefts)。 7485004799 次交换是因为翻转调用交换 23 次(翻转 47 个元素),加上一个交换排列。

以上是关于从 C 中的 n 生成 k 个排列的主要内容,如果未能解决你的问题,请参考以下文章

多重全排列的生成与构造

生成 0, 1,...n-1, n 个 k 数的所有可能组合。每个组合应按升序排列[重复]

Python排列组合的计算方法

排列组合

一个数组有n个数,无序,找出从大到小排列在第k位的数,其中1<=k<=n,C/C++实现

从 n 个元素生成长度为 r 的组合而不重复或排列的函数的时间复杂度是多少?