The Integer Replacement problem asks: Given a positive integer n
, return the minimum number of operations needed to transform n
into 1
.
In each operation, you may:
n
is even, replace n
with n / 2
n
is odd, replace n
with either n + 1
or n - 1
1
using the fewest possible operations. Each operation counts as one step.
1 <= n <= 231 - 1
At first glance, this problem appears to be a simple recursive challenge: keep halving when even, and when odd, try both incrementing and decrementing, picking the path with the fewest steps.
However, a brute-force approach will quickly become inefficient for large n
due to repeated calculations and the exponential number of paths when n
is odd.
To optimize, we notice:
n
is even, there is only one choice: halve it. This is always optimal.n
is odd, we can either increment or decrement. Choosing the better option at each step can significantly reduce the number of operations.Let's break down the solution step-by-step:
n
is even, the optimal move is always n / 2
.n
is odd, try both n + 1
and n - 1
, and take the minimum result.n
, check the two least significant bits. If n == 3
or n % 4 == 1
, decrementing is better; otherwise, incrementing is more optimal. This is because we want to maximize the number of trailing zeros after the operation, allowing more divisions by 2.The recursive + memoized approach is easier to understand and implement, while the bit manipulation greedy approach is faster and more space-efficient.
Let's trace the process with n = 7
:
For larger numbers, the greedy approach helps avoid unnecessary detours by always picking the path that leads to more trailing zeros (more consecutive divisions by 2).
Brute-Force Approach:
n
quickly, and memoization prevents redundant calculations.The Integer Replacement problem is a classic example of recursive optimization. By carefully analyzing the operations and leveraging memoization or bit manipulation, we can transform a naive exponential solution into a highly efficient one. The key insight is to minimize the number of steps for odd numbers by choosing the operation that allows more divisions by 2 in the future, and to avoid recomputation by caching results. This approach ensures the solution is both elegant and performant even for very large inputs.
class Solution:
def integerReplacement(self, n: int) -> int:
memo = {}
def helper(x):
if x == 1:
return 0
if x in memo:
return memo[x]
if x % 2 == 0:
memo[x] = 1 + helper(x // 2)
else:
memo[x] = 1 + min(helper(x + 1), helper(x - 1))
return memo[x]
return helper(n)
class Solution {
public:
int integerReplacement(int n) {
unordered_map memo;
function helper = [&](long long x) {
if (x == 1) return 0;
if (memo.count(x)) return memo[x];
if (x % 2 == 0) {
memo[x] = 1 + helper(x / 2);
} else {
memo[x] = 1 + min(helper(x + 1), helper(x - 1));
}
return memo[x];
};
return helper((long long)n);
}
};
class Solution {
private Map memo = new HashMap<>();
public int integerReplacement(int n) {
return helper((long)n);
}
private int helper(long x) {
if (x == 1) return 0;
if (memo.containsKey(x)) return memo.get(x);
int res;
if (x % 2 == 0) {
res = 1 + helper(x / 2);
} else {
res = 1 + Math.min(helper(x + 1), helper(x - 1));
}
memo.put(x, res);
return res;
}
}
var integerReplacement = function(n) {
const memo = new Map();
function helper(x) {
if (x === 1) return 0;
if (memo.has(x)) return memo.get(x);
let res;
if (x % 2 === 0) {
res = 1 + helper(x / 2);
} else {
res = 1 + Math.min(helper(x + 1), helper(x - 1));
}
memo.set(x, res);
return res;
}
return helper(n);
};