具体的二进制排列生成函数

时间:2016-01-31 15:20:18

标签: c++

所以我正在编写一个程序,我需要生成不仅具有特定长度的二进制数字串,而且还具有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)));

} 

1 个答案:

答案 0 :(得分:1)

(注意:二进制值中的1位数通常称为人口数 - popcount,简称 - 或汉明重量。)

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

  1. 找到由0组成的单词的最长后缀,非空的1s序列,最后是0的可能空序列。

  2. 将前0改为1;以下1到0,然后将所有其他1(如果有的话)移到单词的末尾。

  3. 示例:

    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也是一个扩展。)< / p>

    我已经包含了基于部门的解决方案以及便携性;但是,我的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位添加到数字(使用与上面完全相同的x&-x技巧)来消除最后一次1s运行。由于这种方法从右向左工作,因此它不能循环超过64次,每位一次。

    3. 当popcount太小时,通过将低位0位更改为1来添加最小可能位。因为这会在每个循环上添加一个1位,所以它也不能超过{{ 1}}次(其中k是目标popcount),与第一步不同,没有必要重新计算每个循环的填充计数。

    4. 在以下实现中,我再次使用GCC内置函数k。这个没有相应的Posix功能。有关替代实现以及支持该操作的硬件列表,请参阅Wikipedia page。请注意,找到的值可能会超出范围的结尾;此外,该函数可能返回一个小于提供的参数的值,表明没有适当的值。因此,在使用之前,您需要检查结果是否在所需范围内。

      __builtin_popcountll

      通过将第一个循环更改为:

      ,可以提高效率
      // 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;
      }
      

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

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

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