397. Integer Replacement
Problem Description
You are given a positive integer n
. Your goal is to reduce this number to 1
using the minimum number of operations possible.
The allowed operations are:
- If
n
is even, divide it by 2 (replacen
withn / 2
) - If
n
is odd, you can either add 1 or subtract 1 (replacen
withn + 1
orn - 1
)
You need to find the minimum number of operations required to transform n
into 1
.
For example:
- If
n = 8
: You can do8 → 4 → 2 → 1
, which takes 3 operations - If
n = 7
: You can do7 → 8 → 4 → 2 → 1
(using +1 first) or7 → 6 → 3 → 2 → 1
(using -1 first), with the second path taking 4 operations
The solution uses bit manipulation to make optimal decisions. When n
is even, it always divides by 2 (right shift by 1). When n
is odd, it checks the last two bits: if both are 1 (meaning n & 3 == 3
), it's generally better to add 1 (except for the special case when n = 3
, where subtracting 1 is optimal). Otherwise, it subtracts 1. This greedy approach ensures we create more trailing zeros in the binary representation, allowing for more efficient divisions by 2 in subsequent steps.
Intuition
The key insight comes from looking at the binary representation of numbers. When we divide an even number by 2, we're essentially removing a trailing zero from its binary form. This is the most efficient operation since it directly reduces the number.
For odd numbers, we need to make a choice between adding or subtracting 1. Let's think about what happens in binary:
- Subtracting 1 from an odd number turns the last bit from 1 to 0, making it even
- Adding 1 to an odd number also makes it even, but might create a different pattern
The crucial observation is about consecutive 1s in the binary representation. When we have a number ending in 11
in binary (like 3, 7, 11, 15...), adding 1 creates a cascading effect. For example:
7
in binary is111
. Adding 1 gives1000
(8)11
in binary is1011
. Adding 1 gives1100
(12)
Notice how adding 1 to numbers ending in 11
creates more trailing zeros, which means we can perform more divisions by 2 in subsequent steps. This is why when n & 3 == 3
(checking if the last two bits are both 1), we prefer to add 1.
However, there's a special case: when n = 3
. Here, 3 → 2 → 1
(2 operations) is better than 3 → 4 → 2 → 1
(3 operations), so we subtract instead.
The pattern n & 3
checks the last two bits:
- If it equals 3 (
11
in binary), we generally add 1 to create more zeros - If it equals 1 (
01
in binary), we subtract 1 to immediately get an even number
This greedy strategy of maximizing trailing zeros in the binary representation minimizes the total number of operations needed to reach 1.
Solution Approach
The implementation uses a greedy algorithm with bit manipulation to efficiently determine the optimal operation at each step.
Here's the step-by-step breakdown of the algorithm:
-
Initialize a counter: Start with
ans = 0
to track the number of operations. -
Main loop: Continue until
n
becomes 1:- Check if even: Use
(n & 1) == 0
to test ifn
is even. This bitwise AND operation checks if the least significant bit is 0.- If even, perform right shift:
n >>= 1
(equivalent ton = n // 2
)
- If even, perform right shift:
- Handle odd numbers: When
n
is odd, make the decision between adding or subtracting 1:- Check if
n != 3
and(n & 3) == 3
:(n & 3)
masks the last two bits ofn
- If the result equals 3 (binary
11
), it means both of the last two bits are 1 - In this case, add 1:
n += 1
- Otherwise (when last two bits are
01
or whenn = 3
):- Subtract 1:
n -= 1
- Subtract 1:
- Check if
- Increment counter: After each operation, increment
ans += 1
- Check if even: Use
-
Return the result: Once
n
reaches 1, return the total count of operations.
The bit manipulation techniques used:
n & 1
: Checks if a number is odd (returns 1) or even (returns 0)n & 3
: Extracts the last two bits of the numbern >>= 1
: Right shift by 1 position, equivalent to integer division by 2
This approach ensures O(log n) time complexity since we're essentially reducing the number by at least half every two operations in the worst case. The space complexity is O(1) as we only use a constant amount of extra space.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with n = 15
to see how the bit manipulation strategy works:
Step 1: n = 15 (binary: 1111
)
- Is 15 even? No,
15 & 1 = 1
- Check last two bits:
15 & 3 = 3
(binary11
) - Since
n ≠ 3
and last two bits are11
, we add 1 - n becomes 16, operations = 1
Step 2: n = 16 (binary: 10000
)
- Is 16 even? Yes,
16 & 1 = 0
- Divide by 2 (right shift):
16 >> 1 = 8
- n becomes 8, operations = 2
Step 3: n = 8 (binary: 1000
)
- Is 8 even? Yes,
8 & 1 = 0
- Divide by 2:
8 >> 1 = 4
- n becomes 4, operations = 3
Step 4: n = 4 (binary: 100
)
- Is 4 even? Yes,
4 & 1 = 0
- Divide by 2:
4 >> 1 = 2
- n becomes 2, operations = 4
Step 5: n = 2 (binary: 10
)
- Is 2 even? Yes,
2 & 1 = 0
- Divide by 2:
2 >> 1 = 1
- n becomes 1, operations = 5
Result: 5 operations total
Path taken: 15 → 16 → 8 → 4 → 2 → 1
Notice how adding 1 to 15 (1111
→ 10000
) created four trailing zeros, allowing us to perform four consecutive divisions by 2. This demonstrates why the algorithm prefers adding 1 when the last two bits are 11
- it creates more opportunities for efficient division operations.
Solution Implementation
1class Solution:
2 def integerReplacement(self, n: int) -> int:
3 """
4 Find minimum number of operations to reduce n to 1.
5 Operations allowed: if n is even, divide by 2; if n is odd, add or subtract 1.
6
7 Strategy:
8 - If even: always divide by 2
9 - If odd: check last two bits
10 - If ends in 11 (binary) and n != 3: add 1 (creates more trailing zeros)
11 - Otherwise: subtract 1
12 """
13 operations_count = 0
14
15 while n != 1:
16 # Check if n is even (last bit is 0)
17 if (n & 1) == 0:
18 # Divide by 2 using right shift
19 n >>= 1
20 # Check if last two bits are 11 (binary) and n is not 3
21 # This means n % 4 == 3, which benefits from adding 1
22 elif n != 3 and (n & 3) == 3:
23 n += 1
24 else:
25 # For n == 3 or when last two bits are 01
26 # Subtracting 1 is optimal
27 n -= 1
28
29 operations_count += 1
30
31 return operations_count
32
1class Solution {
2 /**
3 * Finds the minimum number of operations to reduce n to 1.
4 * Operations allowed: if n is even, divide by 2; if n is odd, add or subtract 1.
5 *
6 * @param n The positive integer to reduce to 1
7 * @return The minimum number of operations required
8 */
9 public int integerReplacement(int n) {
10 int operationCount = 0;
11
12 // Continue until n becomes 1
13 while (n != 1) {
14 // Check if n is even (last bit is 0)
15 if ((n & 1) == 0) {
16 // If even, divide by 2 using unsigned right shift
17 n >>>= 1;
18 }
19 // Check if n is odd and last two bits are both 1 (binary ends with 11)
20 // Exception: when n is 3, we should decrement instead of increment
21 else if (n != 3 && (n & 3) == 3) {
22 // Increment n (this leads to more consecutive zeros after division)
23 ++n;
24 }
25 // For other odd numbers (binary ends with 01) or when n is 3
26 else {
27 // Decrement n
28 --n;
29 }
30
31 // Increment the operation counter
32 ++operationCount;
33 }
34
35 return operationCount;
36 }
37}
38
1class Solution {
2public:
3 int integerReplacement(int N) {
4 int steps = 0;
5 long num = N; // Use long to avoid overflow when incrementing INT_MAX
6
7 while (num != 1) {
8 // If num is even, divide by 2 (right shift by 1)
9 if ((num & 1) == 0) {
10 num >>= 1;
11 }
12 // If num is odd
13 else {
14 // Special case: for 3, always decrement (3 -> 2 -> 1 is optimal)
15 // For other numbers ending in binary '11', increment is better
16 // This is because adding 1 to '...11' gives '...00', allowing more divisions
17 if (num != 3 && (num & 3) == 3) {
18 ++num;
19 }
20 // For numbers ending in binary '01' or when num is 3, decrement
21 else {
22 --num;
23 }
24 }
25 ++steps;
26 }
27
28 return steps;
29 }
30};
31
1function integerReplacement(N: number): number {
2 let steps: number = 0;
3 // Use a larger number type to avoid overflow when incrementing large values
4 let num: number = N;
5
6 while (num !== 1) {
7 // If num is even, divide by 2 (right shift by 1)
8 if ((num & 1) === 0) {
9 num >>= 1;
10 }
11 // If num is odd
12 else {
13 // Special case: for 3, always decrement (3 -> 2 -> 1 is optimal)
14 // For other numbers ending in binary '11', increment is better
15 // This is because adding 1 to '...11' gives '...00', allowing more divisions
16 if (num !== 3 && (num & 3) === 3) {
17 num++;
18 }
19 // For numbers ending in binary '01' or when num is 3, decrement
20 else {
21 num--;
22 }
23 }
24 steps++;
25 }
26
27 return steps;
28}
29
Time and Space Complexity
Time Complexity: O(log n)
The algorithm repeatedly divides n
by 2 when it's even, or adjusts it by ±1 when it's odd. In the worst case, we need to consider how many operations are required to reduce n
to 1.
- When
n
is even, we perform a right shift (n >>= 1
), which is equivalent to dividing by 2. - When
n
is odd, we either add or subtract 1, making it even, then in the next iteration divide by 2.
Since each pair of operations (when dealing with odd numbers) effectively reduces n
by approximately half, and we're repeatedly halving the number until it reaches 1, the total number of iterations is proportional to log₂ n
. Even in cases where we add 1 (like when n & 3 == 3
), this is a strategic choice that leads to more consecutive divisions by 2, maintaining the logarithmic behavior.
Space Complexity: O(1)
The algorithm uses only a constant amount of extra space:
- Variable
ans
to count the number of operations - Variable
n
is modified in-place - No additional data structures, recursion, or dynamic allocations are used
All operations are performed using a fixed number of variables regardless of the input size.
Common Pitfalls
1. Integer Overflow When Using Addition
One of the most common pitfalls in this problem is not considering integer overflow when adding 1 to large odd numbers. In languages with fixed integer sizes (like Java or C++), when n
is close to the maximum integer value and you perform n + 1
, it can cause overflow.
Example Problem Case:
- In Java/C++, if
n = Integer.MAX_VALUE
(2,147,483,647), adding 1 would cause overflow - The binary representation of MAX_VALUE is
01111111111111111111111111111111
- Since it ends in
11
, the algorithm would try to add 1, causing overflow
Solution:
Instead of actually modifying n
by adding or subtracting, you can simulate the operations or use unsigned integers:
class Solution:
def integerReplacement(self, n: int) -> int:
operations_count = 0
# Handle potential overflow by using long or treating as unsigned
while n != 1:
if (n & 1) == 0:
n >>= 1
elif n == 3:
# Special case: for n=3, subtracting is better
n -= 1
elif (n & 3) == 3:
# Instead of n += 1, we can directly divide by 2 twice
# This avoids the intermediate addition step
n = (n + 1) >> 1
operations_count += 1 # Count the +1 operation
else:
n -= 1
operations_count += 1
return operations_count
2. Forgetting the Special Case n = 3
Another common mistake is not handling n = 3
as a special case. Without this check, the algorithm would choose to add 1 (since 3 in binary is 11
), leading to the path 3 → 4 → 2 → 1
(3 operations) instead of the optimal 3 → 2 → 1
(2 operations).
Incorrect Implementation:
# Wrong: Missing n == 3 check if (n & 3) == 3: # This would incorrectly add 1 when n = 3 n += 1 else: n -= 1
Correct Implementation:
# Correct: Explicitly check for n == 3 if n != 3 and (n & 3) == 3: n += 1 else: n -= 1
3. Misunderstanding the Bit Pattern Check
Some might incorrectly check only the last bit instead of the last two bits when deciding between adding or subtracting for odd numbers. The pattern (n & 3) == 3
checks if the number ends in 11
in binary, which indicates that adding 1 would create more consecutive zeros for efficient division.
Why checking two bits matters:
- Numbers ending in
01
(like 5, 9, 13): subtracting 1 gives a number ending in00
- Numbers ending in
11
(like 7, 11, 15): adding 1 gives a number ending in00
- Having more trailing zeros allows for more consecutive divisions by 2
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!