|
| 1 | +"""Next higher integer with the same number of set bits (SNOOB). |
| 2 | +author: @0xPrashanthSec |
| 3 | +
|
| 4 | +Given a non-negative integer n, return the next higher integer that has the same |
| 5 | +number of 1 bits in its binary representation. If no such number exists within |
| 6 | +Python's unbounded int range (practically always exists unless n is 0 or all |
| 7 | +ones packed at the most significant end for fixed-width), this implementation |
| 8 | +returns -1. |
| 9 | +
|
| 10 | +This is the classic SNOOB algorithm from "Hacker's Delight". |
| 11 | +
|
| 12 | +Reference: https://graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation |
| 13 | +
|
| 14 | +>>> next_higher_same_ones(0b0011) |
| 15 | +5 |
| 16 | +>>> bin(next_higher_same_ones(0b0011)) |
| 17 | +'0b101' |
| 18 | +>>> bin(next_higher_same_ones(0b01101)) # 13 -> 14 (0b01110) |
| 19 | +'0b1110' |
| 20 | +>>> next_higher_same_ones(1) |
| 21 | +2 |
| 22 | +>>> next_higher_same_ones(0) # no higher with same popcount |
| 23 | +-1 |
| 24 | +>>> next_higher_same_ones(-5) # negative not allowed |
| 25 | +Traceback (most recent call last): |
| 26 | + ... |
| 27 | +ValueError: n must be a non-negative integer |
| 28 | +""" |
| 29 | +from __future__ import annotations |
| 30 | + |
| 31 | + |
| 32 | +def next_higher_same_ones(n: int) -> int: |
| 33 | + """Return the next higher integer with the same number of set bits as n. |
| 34 | +
|
| 35 | + :param n: Non-negative integer |
| 36 | + :return: Next higher integer with same popcount or -1 if none |
| 37 | + :raises ValueError: if n < 0 |
| 38 | + """ |
| 39 | + if n < 0: |
| 40 | + raise ValueError("n must be a non-negative integer") |
| 41 | + if n == 0: |
| 42 | + return -1 |
| 43 | + |
| 44 | + # snoob algorithm |
| 45 | + # c = rightmost set bit |
| 46 | + c = n & -n |
| 47 | + # r = ripple carry: add c to n |
| 48 | + r = n + c |
| 49 | + if r == 0: |
| 50 | + return -1 |
| 51 | + # ones = pattern of ones that moved from lower part |
| 52 | + ones = ((r ^ n) >> 2) // c |
| 53 | + return r | ones |
| 54 | + |
| 55 | + |
| 56 | +if __name__ == "__main__": # pragma: no cover |
| 57 | + import doctest |
| 58 | + |
| 59 | + doctest.testmod() |
0 commit comments