Greedy algorithms and bit manipulation
Two small topics that appear often enough to be worth a file. Greedy is about justifying a choice; bits are about recognising a handful of tricks.
Greedy
Take the locally best option and never reconsider. Fast and simple when it works — and confidently wrong when it doesn’t.
The question that matters: why is greedy correct here? Producing a greedy solution without justifying it is the weak version of the answer.
Two properties must hold:
- Greedy choice property — a locally optimal choice is part of some globally optimal solution.
- Optimal substructure — an optimal solution contains optimal solutions to subproblems.
The standard justification is an exchange argument: take any optimal solution, show you can swap in the greedy choice without making it worse, therefore a greedy solution is optimal.
Where greedy works
| Problem | Greedy rule | Why |
|---|---|---|
| Activity selection | earliest end time | finishing soonest leaves the most room |
| Fractional knapsack | highest value/weight | you can take fractions |
| Huffman coding | merge two lowest frequencies | provably optimal prefix code |
| Minimum spanning tree | cheapest safe edge | cut property |
| Dijkstra | nearest unvisited node | needs non-negative weights |
| Jump game | furthest reachable index | reachability is monotone |
Where it fails
0/1 knapsack. Highest value/weight first is wrong when you can’t take fractions:
capacity 10
A: weight 6, value 30 (ratio 5.0)
B: weight 5, value 24 (ratio 4.8)
C: weight 5, value 24 (ratio 4.8)
greedy -> A only = 30
optimal -> B + C = 48
Greedy commits to A and then can’t fit anything else. Needs DP — see ../06_dp/02_knapsack_lcs_edit_distance.md.
Coin change with arbitrary denominations. Greedy works for standard currencies and fails for [1, 3, 4] making 6: greedy gives 4+1+1 (three coins), optimal is 3+3 (two).
That coin example is the cleanest way to show you understand greedy’s limits, and it’s short enough to state from memory.
The heuristic: if the problem asks for an optimum and choices interact, suspect DP. If a simple ordering makes each choice independent, suspect greedy — then justify it.
Bit manipulation
x & 1 # 1 if odd
x >> 1 # divide by 2 (floor for non-negative)
x << 1 # multiply by 2
x & (x - 1) # clear the lowest set bit
x & -x # ISOLATE the lowest set bit
x ^ y # differing bits
~x # bitwise NOT (in Python: -x - 1)
The two worth memorising are x & (x - 1) and x & -x. Almost every bit puzzle uses one of them.
Counting set bits
def count_bits(x: int) -> int:
count = 0
while x:
x &= x - 1 # clears the lowest set bit each iteration
count += 1
return count
x.bit_count() # Python 3.10+ — just use this
Brian Kernighan’s method runs in O(set bits) rather than O(total bits). Python 3.10 added int.bit_count(), so know the trick and use the builtin.
XOR properties
The three that solve most XOR problems:
x ^ x = 0
x ^ 0 = x
XOR is commutative and associative
Single number — every element appears twice except one:
from functools import reduce
import operator
def single_number(nums: list[int]) -> int:
return reduce(operator.xor, nums) # pairs cancel, the loner survives
O(n) time, O(1) space, and it beats the hash-set solution on space. Elegant enough to be worth recognising instantly.
Two single numbers — everything else appears twice:
def two_singles(nums: list[int]) -> tuple[int, int]:
xor_all = reduce(operator.xor, nums) # = a ^ b
bit = xor_all & -xor_all # any bit where a and b differ
a = reduce(operator.xor, (n for n in nums if n & bit))
return a, a ^ xor_all
Partition by a differing bit, so each group contains exactly one of the two singles. This uses both key tricks together.
Subsets via bitmasks
def subsets(nums: list[int]) -> list[list[int]]:
n = len(nums)
return [[nums[i] for i in range(n) if mask >> i & 1]
for mask in range(1 << n)]
Each of the 2^n masks encodes one subset. Also the basis of bitmask DP — travelling salesman, assignment problems — where the state is a set of visited items packed into an integer.
Python specifics worth knowing
- Integers are arbitrary precision, so there’s no overflow and no fixed width. Tricks assuming 32-bit wraparound don’t translate directly.
~xis-x - 1, not a 32-bit complement. Mask with& 0xFFFFFFFFif you need fixed-width behaviour.- Negative numbers behave as infinite two’s complement, so
-1 >> 1is-1, not0.
That first point catches people porting solutions from C++ or Java.
Interview angle
- “When is greedy correct?” — when the greedy choice property and optimal substructure hold. Justify with an exchange argument: any optimal solution can be modified to include the greedy choice without getting worse.
- “Give an example where greedy fails.” — coin change with
[1, 3, 4]making 6: greedy takes 4+1+1, optimal is 3+3. Or 0/1 knapsack, where the best ratio item can block two better ones. - “Greedy or DP?” — if a simple ordering makes each choice independent of the rest, greedy. If choices interact so that taking one closes off better combinations, DP.
- “What does
x & (x - 1)do?” — clears the lowest set bit. Repeatedly applying it counts set bits inO(set bits).x & -xisolates that bit instead, which is how you partition by a differing bit. - “Find the element appearing once when all others appear twice.” — XOR everything. Pairs cancel to zero and the loner survives.
O(n)time andO(1)space, better than a hash set. - “Anything different about bit manipulation in Python?” — integers are arbitrary precision, so there’s no overflow and
~xis-x - 1rather than a 32-bit complement. Mask explicitly if you need fixed-width semantics.