所以我正在编写一个程序,我需要生成不仅具有特定长度的二进制数字串,而且还具有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)));
}
答案 0 :(得分:1)
(注意:二进制值中的1位数通常称为人口数 - popcount,简称 - 或汉明重量。)
有一个众所周知的bit-hack循环遍历具有相同人口数的所有二进制单词,基本上执行以下操作:
找到由0组成的单词的最长后缀,非空的1s序列,最后是0的可能空序列。
将前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
也是一个扩展。)< / 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的第一个数字。为此,可以使用以下简单算法:
从范围中的第一个值开始。
只要值的popcount太高,通过将低位1位添加到数字(使用与上面完全相同的x&-x
技巧)来消除最后一次1s运行。由于这种方法从右向左工作,因此它不能循环超过64次,每位一次。
当popcount太小时,通过将低位0位更改为1来添加最小可能位。因为这会在每个循环上添加一个1位,所以它也不能超过{{ 1}}次(其中k
是目标popcount),与第一步不同,没有必要重新计算每个循环的填充计数。
在以下实现中,我再次使用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;
}