特定的二进制排列生成函数

Specific binary permutation generating function

本文关键字:函数 排列 二进制      更新时间:2023-10-16

所以我正在编写一个程序,我需要在其中生成二进制数字符串,这些字符串不仅是特定长度,而且具有特定数量的 1 和 0。此外,将生成的这些字符串与更高和更低的值进行比较,以查看它们是否在该特定范围内。 我遇到的问题是我正在处理 64 位无符号整数。因此,有时,需要 al 64 位的非常大的数字会为根本不在范围内的值产生大量二进制字符串排列,并且需要大量时间。

我很好奇算法是否有可能接受两个绑定值,一个数字,并且只在具有特定数量的绑定值之间生成二进制字符串。

这就是我目前所拥有的,但它正在产生许多数字。

void generatePermutations(int no_ones, int length, uint64_t smaller, uint64_t larger, uint64_t& accum){
    char charArray[length+1];
    for(int i = length - 1; i > -1; i--){
        if(no_ones > 0){
            charArray[i] = '1';
            no_ones--;
        }else{
            charArray[i] = '0';
        }
    }
    charArray[length] = '';
    do {
        std::string val(charArray);
        uint64_t num = convertToNum(val);
        if(num >= smaller && num <= larger){
            accum ++;
        }
    } while ( std::next_permutation(charArray, (charArray + length)));
} 

(注意:二进制值中的 1 位数通常称为总体计数 - 简称 popcount - 或汉明权重。

有一个众所周知的bit-hack可以循环遍历具有相同人口计数的所有二进制字,它基本上执行以下操作:

  1. 查找单词的最长后缀,该后缀

  2. 由 0、非空序列 1 和最后可能为空的序列 0 组成。
  3. 将第一个 0 更改为 1;将后面的 1 更改为 0,然后将所有其他 1(如果有(移到单词的末尾。

例:

00010010111100
       ^-------- beginning of the suffix
00010011         0 becomes 1
        0        1 becomes 0
         00111   remaining 1s right-shifted to the end
这可以通过使用

x中的最低阶集合位是x & -x的事实(其中-表示x的2s补码负数(这一事实可以非常快速地完成。要找到后缀的开头,只需将最低顺序集合位添加到数字中,然后找到新的最低顺序设置位。(用几个数字试试这个,你应该看到它是如何工作的。

最大的问题是执行正确的移位,因为我们实际上并不知道位数。传统的解决方案是使用除法(按原始低阶 1 位(进行右移,但事实证明,相对于其他操作数,现代硬件上的除法确实很慢。循环一个位移位通常比除法快,但在下面的代码中,我使用了 gcc 的 __builtin_ffsll ,如果目标硬件上存在操作码,它通常会编译成适当的操作码。(详见man ffs;我使用内置函数来避免功能测试宏,但它有点丑陋,限制了您可以使用的编译器范围。OTOH,ffsll也是一个扩展。

为了便于移植,我也包含了基于分区的解决方案;但是,在我的i5笔记本电脑上,它需要几乎三倍的时间。

template<typename UInt>
static inline UInt last_one(UInt ui) { return ui & -ui; }
// next_with_same_popcount(ui) finds the next larger integer with the same
// number of 1-bits as ui. If there isn't one (within the range
// of the unsigned type), it returns 0.
template<typename UInt>
UInt next_with_same_popcount(UInt ui) {
  UInt lo = last_one(ui);
  UInt next = ui + lo;
  UInt hi = last_one(next);
  if (next) next += (hi >> __builtin_ffsll(lo)) - 1;
  return next;
}
/*
template<typename UInt>
UInt next_with_same_popcount(UInt ui) {
  UInt lo = last_one(ui);
  UInt next = ui + lo;
  UInt hi = last_one(next) >> 1;
  if (next) next += hi/lo - 1;
  return next;
}
*/    

唯一剩下的问题是在给定范围内找到具有正确弹出计数的第一个数字。为了帮助解决这个问题,可以使用以下简单的算法:

  1. 从范围中的第一个值开始。

  2. 只要该值的弹出计数太高,就可以通过将低阶 1 位添加到数字中来消除最后一次 1 的运行(使用与上述完全相同的x&-x技巧(。由于这是从右到左的,因此它不能循环超过 64 次,每个比特一次。

  3. 当弹出计数太小时,通过将低阶 0 位更改为 1 来添加尽可能小的位。由于这会在每个循环上添加一个 1 位,因此它也不能循环超过 k 次(其中 k 是目标弹出计数(,并且与第一步不同,没有必要重新计算每个循环的人口计数。

在下面的实现中,我再次使用内置的 GCC,__builtin_popcountll .这个没有相应的 Posix 函数。请参阅维基百科页面了解替代实现和支持该操作的硬件列表。请注意,找到的值可能会超过范围的末尾;此外,该函数可能返回的值小于提供的参数,指示没有适当的值。因此,在使用之前,您需要检查结果是否在所需范围内。

// next_with_popcount_k returns the smallest integer >= ui whose popcnt
// is exactly k. If ui has exactly k bits set, it is returned. If there
// is no such value, returns the smallest integer with exactly k bits.
template<typename UInt>
UInt next_with_popcount_k(UInt ui, int k) {
  int count; 
  while ((count = __builtin_popcountll(ui)) > k)
    ui += last_one(ui);
  for (int i = count; i < k; ++i)
    ui += last_one(~ui);
  return ui;
}

通过将第一个循环更改为

while ((count = __builtin_popcountll(ui)) > k) {
  UInt lo = last_one(ui);
  ui += last_one(ui - lo) - lo;
}

这减少了大约 10% 的执行时间,但我怀疑该函数是否会被调用到足够频繁以使其值得。根据 CPU 实现 POPCOUNT 操作码的效率,使用单个位扫描执行第一个循环可能会更快,以便能够跟踪 popcount 而不是重新计算它。在没有 POPCOUNT 操作码的硬件上几乎可以肯定是这种情况。

一旦你有了这两个函数,迭代一个范围内的所有k位值就变得微不足道了:

void all_k_bits(uint64_t lo, uint64_t hi, int k) {
  uint64_t i = next_with_popcount_k(lo, k);
  if (i >= lo) {
    for (; i > 0 && i < hi; i = next_with_same_popcount(i)) {
      // Do what needs to be done
    }
  }
}