Calculating the next higher number which has same number of set bits?

后端 未结 3 478
不思量自难忘°
不思量自难忘° 2021-01-17 06:35

A solution is given to this question on geeksforgeeks website.

I wish to know does there exist a better and a simpler solution? This is a bit complicated to understa

3条回答
  •  孤独总比滥情好
    2021-01-17 07:04

    Based on some code I happened to have kicking around which is quite similar to the geeksforgeeks solution (see this answer: https://stackoverflow.com/a/14717440/1566221) and a highly optimized version of @QuestionC's answer which avoids some of the shifting, I concluded that division is slow enough on some CPUs (that is, on my Intel i5 laptop) that looping actually wins out.

    However, it is possible to replace the division in the g-for-g solution with a shift loop, and that turned out to be the fastest algorithm, again just on my machine. I'm pasting the code here for anyone who wants to test it.

    For any implementation, there are two annoying corner cases: one is where the given integer is 0; the other is where the integer is the largest possible value. The following functions all have the same behaviour: if given the largest integer with k bits, they return the smallest integer with k bits, thereby restarting the loop. (That works for 0, too: it means that given 0, the functions return 0.)

    Bit-hack solution with division:

    template
    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 solution with division replaced by a shift loop

    template
    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;
    }
    

    Optimized shifting solution

    template
    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;
    }
    

    (Some would say that the above is yet another bit-hack, and I won't argue.)

    Highly non-representative benchmark

    I tested this by running through all 32-bit numbers. (That is, I create the smallest pattern with i ones and then cycle through all the possibilities, for each value of i from 0 to 32.):

    #include 
    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;
    }
    

    The result:

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

提交回复
热议问题