【问题标题】:Calculating the next higher number which has same number of set bits?计算下一个具有相同数量设置位的更高数字?
【发布时间】:2015-06-19 21:11:45
【问题描述】:

geeksforgeeks网站上给出了这个问题的解决方案。

我想知道是否存在更好更简单的解决方案?这个理解起来有点复杂。只是一个算法就可以了。

【问题讨论】:

  • 肯定有更简单的解决方案,但这并不能使它们变得更好。
  • 我投票结束这个问题,因为它更适合这里:cs.stackexchange.com
  • @gsamaras 我认为这在 Stack Exchange 确实具有实际意义,因为它是关于一个具体的算法。
  • 这是@templatetypedef,我真的不明白你的评论。
  • @gsamaras 对不起,这是一个错字。我认为这非常适合 Stack Overflow,因为问题涉及实际的算法实现,而不是算法背后的理论。这有意义吗?

标签: c++ algorithm


【解决方案1】:

我很确定这个算法与你的链接算法一样有效且更容易理解。

这里的策略是要理解,在不增加 1 的数量的情况下使数字变大的唯一方法是携带 1,但如果携带多个 1,则必须将它们加回。

  • 给定一个号码1001 1100

  • 右移直到值为奇数,0010 0111。记住轮班次数:shifts = 2;

  • 右移直到值为偶数,0000 0100。记住执行的移位次数和消耗的位数。 shifts += 3; bits = 3;

  • 到目前为止,我们已经从算法中采用了 5 次移位和 3 位来携带可能的最低位。现在我们偿还了。

  • 将最右边的位设为 1。0000 0101。我们现在欠它 2 位。 bits -= 1

  • 左移 3 次以添加 0。 0010 1000。我们做了三遍,因为shifts - bits == 3 shifts -= 3

  • 现在我们欠数字两位和两次移位。所以将它左移两次,每次将最左边的位设置为 1。 1010 0011。我们已经偿还了所有的比特和所有的班次。 bits -= 2; shifts -= 2; bits == 0; shifts == 0

这里还有一些其他示例...每个步骤都显示为current_val, shifts_owed, bits_owed

0000 0110
0000 0110, 0, 0 # Start
0000 0011, 1, 0 # Shift right till odd
0000 0000, 3, 2 # Shift right till even
0000 0001, 3, 1 # Set LSB
0000 0100, 1, 1 # Shift left 0's
0000 1001, 0, 0 # Shift left 1's

0011 0011
0011 0011, 0, 0 # Start
0011 0011, 0, 0 # Shift right till odd
0000 1100, 2, 2 # Shift right till even
0000 1101, 2, 1 # Set LSB
0001 1010, 1, 1 # Shift left 0's
0011 0101, 0, 0 # Shift left 1's

【讨论】:

  • 这比链接算法容易得多。但是,我只想知道这是正确的方法,对吗?我的意思是,我从没想过会这么容易。这就是为什么,有点退缩了。
  • 另外,这会比网站上给出的更有效吗?
  • 我们不是在第二步中也取 2 位,同时我们正在右移它直到我们得到一个奇数值吗?我们不会算吗?
  • 这种算法可能更容易理解,但仍然不如链接算法效率。链接算法使用固定数量的 7 个算术和位运算,而您的则有循环。
  • @petersohn:没错,我也是这么想的。但事实证明,至少在我的笔记本电脑(i5)上,除法速度足够慢,循环效果更好。但是,链接算法中的除法可以通过移位循环执行,结果证明这是赢家。我将基准实现放在答案中。
【解决方案2】:

有一种更简单,但效率肯定较低的方法。如下:

  • 计算数字中的位数(将数字右移直到为零,然后计算最右边的位为 1 的次数)。
  • 增加数字直到得到相同的结果。

当然效率极低。考虑一个 2 的幂的数字(设置 1 位)。您必须将这个数字加倍才能得到答案,在每次迭代中将数字加 1。当然不行。

如果你想要一个更简单的高效算法,我认为没有。事实上,这对我来说似乎很简单直接。

编辑:通过“更简单”,我的意思是它易于实现,并且可能具有更少的代码行。

【讨论】:

    【解决方案3】:

    根据我碰巧遇到的一些代码,它与 geeksforgeeks 解决方案非常相似(请参阅此答案:https://stackoverflow.com/a/14717440/1566221)和高度优化的 @QuestionC's answer 版本,它避免了一些变化,我得出结论:除法在某些 CPU(即,在我的 Intel i5 笔记本电脑上)上足够慢,以至于循环实际上胜出。

    但是,可以用移位循环替换 g-for-g 解决方案中的除法,结果证明这是最快的算法,同样只是在我的机器上。我将代码粘贴在这里,供任何想测试它的人使用。

    对于任何实现,都有两种烦人的极端情况:一种是给定整数为 0;另一种是给定整数为 0。另一个是整数是可能的最大值。以下函数都具有相同的行为:如果给定具有 k 位的最大整数,则它们返回具有 k 位的最小整数,从而重新启动循环。 (这也适用于 0:这意味着给定 0,函数返回 0。)

    带除法的 Bit-hack 解决方案:

    template<typename UnsignedInteger>
    UnsignedInteger next_combination_1(UnsignedInteger comb) {
      UnsignedInteger last_one = comb & -comb;
      UnsignedInteger last_zero = (comb + last_one) &~ comb;
      if (last_zero)
        return comb + last_one + ((last_zero / last_one) >> 1) - 1;
      else if (last_one)
        return UnsignedInteger(-1) / last_one;
      else
        return 0;
    }
    

    用移位循环代替除法的 Bit-hack 解决方案

    template<typename UnsignedInteger>
    UnsignedInteger next_combination_2(UnsignedInteger comb) {
      UnsignedInteger last_one = comb & -comb;
      UnsignedInteger last_zero = (comb + last_one) &~ comb;
      UnsignedInteger ones = (last_zero - 1) & ~(last_one - 1);
      if (ones) while (!(ones & 1)) ones >>= 1;
      comb += last_one;
      if (comb) comb += ones >> 1; else comb = ones;
      return comb;
    }
    

    优化换档方案

    template<typename UnsignedInteger>
    UnsignedInteger next_combination_3(UnsignedInteger comb) {
      if (comb) {
        // Shift the trailing zeros, keeping a count.
        int zeros = 0; for (; !(comb & 1); comb >>= 1, ++zeros);
        // Adding one at this point turns all the trailing ones into
        // trailing zeros, and also changes the 0 before them into a 1.
        // In effect, this is steps 3, 4 and 5 of QuestionC's solution,
        // without actually shifting the 1s.
        UnsignedInteger res = comb + 1U;
        // We need to put some ones back on the end of the value.
        // The ones to put back are precisely the ones which were at
        // the end of the value before we added 1, except we want to
        // put back one less (because the 1 we added counts). We get
        // the old trailing ones with a bit-hack.
        UnsignedInteger ones = comb &~ res;
        // Now, we finish shifting the result back to the left
        res <<= zeros;
        // And we add the trailing ones. If res is 0 at this point,
        // we started with the largest value, and ones is the smallest
        // value.
        if (res) res += ones >> 1;
        else res = ones;
        comb = res;
      }
      return comb;
    }
    

    (有人会说上面是另一个小技巧,我不会争论。)

    高度不具代表性的基准

    我通过运行所有 32 位数字对此进行了测试。 (也就是说,我用 i 创建了最小的模式,然后循环遍历所有的可能性,对于 i 的每个值从 0 到 32。):

    #include <iostream>
    int main(int argc, char** argv) {
      uint64_t count = 0;
      for (int i = 0; i <= 32; ++i) {
        unsigned comb = (1ULL << i) - 1;
        unsigned start = comb;
        do {
          comb = next_combination_x(comb);
          ++count;
        } while (comb != start);
      }
      std::cout << "Found " << count << " combinations; expected " << (1ULL << 32) << '\n';
      return 0;
    }
    

    结果:

    1. Bit-hack with division: 43.6 seconds
    2. Bit-hack with shifting: 15.5 seconds 
    3. Shifting algorithm:     19.0 seconds
    

    【讨论】:

    • 我在 i7 机器和 64 位 Linux 上尝试过,第 2 种算法比第 3 种算法稍慢。事实上,第二个跑的速度和你差不多,但其他的快一点(第一个~35s,第三个~12s)。
    • @petersohn:我用稍微改变的框架重新运行了我的测试,时间更改为 16.2 秒 (2)、12.7 秒 (3)。稍后我会再挖一些。在现代硬件上很难获得可靠的工作台。感谢您提供额外的数据点。不管你怎么看,它显示除法有多慢(在 i7 上可能稍微慢一些)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-11
    • 2015-09-19
    • 1970-01-01
    • 2017-09-04
    • 2020-04-05
    相关资源
    最近更新 更多