-1

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 understand. Just an algorithm will be fine.

gsamaras
  • 71,951
  • 46
  • 188
  • 305
John Lui
  • 1,434
  • 3
  • 23
  • 37
  • There are surely simpler solutions, but that does not make them better. – harold Jun 19 '15 at 21:16
  • 1
    I'm voting to close this question as off-topic because it fits better here: http://cs.stackexchange.com/ – gsamaras Jun 19 '15 at 21:18
  • @gsamaras I think that this actually does have practical relevance here at Stack Exchange, since it's about a concrete algorithm. – templatetypedef Jun 19 '15 at 21:20
  • Here is SO @templatetypedef, I do not really get your comment. – gsamaras Jun 19 '15 at 21:22
  • @gsamaras Sorry, that was a typo. I think this is a good fit for Stack Overflow, since the question concerns a practical algorithm implementation rather than the theory behind the algorithm. Does that make sense? – templatetypedef Jun 19 '15 at 21:23
  • Hmm, let's say that you are right @templatetypedef. I would still feel that it's too broad. – gsamaras Jun 19 '15 at 21:25
  • "simpler" in what sense? "better" in what sense? The answer given in the link is pretty simple, and only involves half a dozen or so steps (albeit one of them being a divide, which should be a shift if we can count trailing zeros easily) – Mats Petersson Jun 19 '15 at 21:26
  • The algorithm is not straightforward because the problem is not straightforward. A simpler solution would probably be even harder to understand, as it's all bit manipulation. – Mark Ransom Jun 19 '15 at 22:27
  • Well, it feels very homeworky but I think this is a good problem. First, that linked algorithm is confusing as hell so someone should do better. Second, this is a really important problem because it is an isomorphism of the problem of finding the next combinatoric of a set which comes up a lot (there's a SO dupe for that bit it's pretty academic). Third, I think this might help with a project Euler problem I've been stuck on so I appreciate it. :) – QuestionC Jun 19 '15 at 22:45

3 Answers3

2

There is a simpler, though definitely less efficient one. It follows:

  • Count the number of bits in your number (right shift your number until it reaches zero, and count the number of times the rightmost bit is 1).
  • Increment the number until you get the same result.

Of course it is extremely inefficient. Consider a number that's a power of 2 (having 1 bit set). You'll have to double this number to get your answer, incrementing the number by 1 in each iteration. Of course it won't work.

If you want a simpler efficient algorithm, I don't think there is one. In fact, it seems pretty simple and straightforward to me.

Edit: By "simpler", I mean it's mpre straightforward to implement, and possibly has a little less code lines.

petersohn
  • 11,292
  • 13
  • 61
  • 98
2

I am pretty sure this algorithm is as efficient and easier to understand than your linked algorithm.

The strategy here is to understand that the only way to make a number bigger without increasing its number of 1's is to carry a 1, but if you carry multiple 1's then you must add them back in.

  • Given a number 1001 1100

  • Right shift it until the value is odd, 0010 0111. Remember the number of shifts: shifts = 2;

  • Right shift it until the value is even, 0000 0100. Remember the number of shifts performed and bits consumed. shifts += 3; bits = 3;

  • So far, we have taken 5 shifts and 3 bits from the algorithm to carry the lowest digit possible. Now we pay it back.

  • Make the rightmost bit 1. 0000 0101. We now owe it 2 bits. bits -= 1

  • Shift left 3 times to add the 0's. 0010 1000. We do it three times because shifts - bits == 3 shifts -= 3

  • Now we owe the number two bits and two shifts. So shift it left twice, setting the leftmost bit to 1 each time. 1010 0011. We've paid back all the bits and all the shifts. bits -= 2; shifts -= 2; bits == 0; shifts == 0

Here's a few other examples... each step is shown as 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

QuestionC
  • 10,006
  • 4
  • 26
  • 44
  • This is considerably easy than the linked algorithm. But, I just want to know that this is the correct method, right? I mean, I never thought that it could be so easy. That's why, a little taken back. – John Lui Jun 20 '15 at 04:47
  • And also, will be this be more efficient than the one given on the website? – John Lui Jun 20 '15 at 04:54
  • aren't we taking 2 bits in the second step as well while we are right shifting it until we get an odd value? We won't count that? – John Lui Jun 20 '15 at 05:38
  • This algorithm may be easier to understand, but it still less efficient than the linked one. The linked algorithm uses a fixed number of 7 arithmetic and bit operations, while yours has loops. – petersohn Jun 20 '15 at 06:12
  • @petersohn: True, and it's what I thought, too. But it turns out, at least on my laptop (an i5), that division is slow enough that a loop works out better. However, the division in the linked algorithm can be performed with a shift loop, and that turned out to be the winner. I'm putting the benchmark implementations in an answer. – rici Jun 20 '15 at 06:56
  • @QuestionC: can you elaborate more on "only way to make a number bigger is to carry 1"? – newbie_old Jun 21 '15 at 07:55
  • @petersohn I toned down the efficiency bragging. =) – QuestionC Jun 22 '15 at 13:24
  • @newbie_old It is about maintaining the invariant number of set bits. When adding binary, the only operations are adding to a 0 or adding to a 1 (then carrying digits as normal). If you turn a 0 to a 1, you're increasing the number of set bits, invalidating the problem's invariant. If you add a 1 to a 1 then you set the 0, carry the 1, and the number of set bits is hard to predict, but we know it won't increase. It's a lot to express in one phrase, but the idea is we know we *have* to perform a carry at least once in each addition, so we try to do the smallest carry possible. – QuestionC Jun 22 '15 at 13:48
2

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

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

Optimized shifting solution

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

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

The result:

1. Bit-hack with division: 43.6 seconds
2. Bit-hack with shifting: 15.5 seconds 
3. Shifting algorithm:     19.0 seconds
Community
  • 1
  • 1
rici
  • 234,347
  • 28
  • 237
  • 341
  • I tried it on my i7 machine and 64-bit Linux, the 2nd algorithm was slightly slower than the 3rd. In fact, the 2nd one ran about the same speed as yours, but the others were a bit faster (the 1st ~35s and the 3rd ~12s). – petersohn Jun 20 '15 at 08:51
  • @petersohn: I reran my test with a slightly changed framework, and the times changed to 16.2 secs (2), 12.7 secs (3). I'll dig some more later. It is hard to get reliable benches on modern hardware. Thanks for the extra datapoint. However you look at it, it shows how slow division is (perhaps slightly less so on an i7). – rici Jun 20 '15 at 15:00