【问题标题】:Specific binary permutation generating function特定的二元置换生成函数
【发布时间】:2016-02-01 09:42:29
【问题描述】:

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

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

这是我目前所拥有的,但它产生了很多数字。

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] = '\0';

    do {
        std::string val(charArray);
        uint64_t num = convertToNum(val);
        if(num >= smaller && num <= larger){
            accum ++;
        }
    } while ( std::next_permutation(charArray, (charArray + length)));

} 

【问题讨论】:

  • 从技术上讲,这不是有效的 C++,因为它没有 variable-length arrays。如果你想要便携,你应该使用std::vector。或者如果你想创建一个字符串,为什么不使用std::string
  • 这个问题是相关的:stackoverflow.com/questions/30947343/…。您还需要找到第一个数字 >= 具有正确位数的下限,这可以通过翻转位来完成,因此它是 O(wordsize)
  • 谢谢@rici,你对我如何处理位翻转部分有什么建议吗?我找不到任何与我正在尝试做的事情完全相关的帖子。

标签: c++


【解决方案1】:

(注:二进制值中1位的个数一般称为population count——popcount,简称popcount——或Hamming weight。)

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

  1. 找出单词的最长后缀,包括一个 0、一个非空的 1 序列,最后是一个可能为空的 0 序列。

  2. 将第一个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 &amp; -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;
}
*/    

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

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

  2. 只要值的 popcount 太高,通过将低位 1 位添加到数字来消除最后一次运行的 1(使用与上面完全相同的 x&amp;-x 技巧)。由于这是从右到左工作的,它不能循环超过 64 次,每比特一次。

  3. 1234563 @times(其中k 是目标popcount),不需要像第一步那样重新计算每个循环的population count。

在下面的实现中,我再次使用 GCC 内置函数 __builtin_popcountll。这个没有对应的 Posix 函数。请参阅Wikipedia page 了解替代实现和支持该操作的硬件列表。请注意,找到的值可能会超出范围的末尾;此外,该函数可能返回一个小于提供的参数的值,表明没有合适的值。所以你需要在使用前检查结果是否在期望的范围内。

// 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
    }
  }
}

【讨论】:

  • 只是想确保我理解正确。因此,如果二进制表示中没有足够的数字,则将我需要的数字添加到最右边的位置,其中包含零。如果有太多,只需在数字本身上加 1,看看我在那个二进制表示中有多少个?
  • @Phil_B:实际上插入了工作代码。希望对您有所帮助。
  • 谢谢@rici。这正是我所需要的。
猜你喜欢
  • 1970-01-01
  • 2013-03-09
  • 2020-09-16
  • 1970-01-01
  • 2018-03-25
  • 1970-01-01
  • 1970-01-01
  • 2019-01-05
  • 1970-01-01
相关资源
最近更新 更多