Smallest Good Base
HardPrompt
Given an integer n represented as a string, return the smallest good base of n.
We call k >= 2 a good base of n, if all digits of n base k are 1's.
Example 1:
Input: n = "13"
Output: "3"
Explanation: 13 base 3 is 111.Example 2:
Input: n = "4681"
Output: "8"
Explanation: 4681 base 8 is 11111.Example 3:
Input: n = "1000000000000000000"
Output: "999999999999999999"
Explanation: 1000000000000000000 base 999999999999999999 is 11.
Constraints:
nis an integer in the range[3, 1018].ndoes not contain any leading zeros.
Approaches
2 approaches with complexity analysis and trade-offs.
The core idea is to rephrase the problem. Instead of searching for the base k directly, which can be as large as n-1, we can search for the number of digits m in the base k representation. If n in base k is represented by m ones, the equation is n = 1 + k + k^2 + ... + k^(m-1). The number of digits m is bounded by log2(n), which is at most 60 for n <= 10^18. This small range for m allows us to iterate through all possible values of m. For a fixed m, the equation n = 1 + k + ... + k^(m-1) is monotonic with respect to k. This allows us to use binary search to find the correct integer k. We should iterate m from its maximum possible value down to 2. The first valid (k, m) pair we find will give the smallest k, because a larger m implies a smaller k.
Algorithm
- Parse the input string
nto alongvariable,numN. - The maximum possible number of digits
moccurs when the basekis minimal (i.e.,k=2). This givesm <= log2(n) + 1. Forn <= 10^18,mis at most 60. - Iterate
mfrom 60 down to 2. A largermimplies a smaller basek, so we iterate downwards to find the smallestkfirst. - For each
m, perform a binary search for the basekin the range[2, n^(1/(m-1))]. - In the binary search, for a candidate base
mid_k:- Calculate the sum
S = 1 + mid_k + mid_k^2 + ... + mid_k^(m-1). - This sum must be calculated carefully to prevent
longoverflow. If an overflow occurs, treat the sum as a very large number. - If
S == numN, we have found a valid basemid_k. Since we are iteratingmdownwards, thismid_kis the smallest possible good base. Return it as a string. - If
S < numN, the basemid_kis too small. Search in the upper half:low = mid_k + 1. - If
S > numN(or overflowed), the basemid_kis too large. Search in the lower half:high = mid_k - 1.
- Calculate the sum
- If the loop finishes without finding a good base, it means the only solution is for
m=2. In this case,n = k + 1, so the base isk = n - 1. Return(numN - 1)as a string.
Walkthrough
This approach tackles the problem by changing the primary variable of iteration. Instead of iterating through all possible bases k from 2 to n-1 (which is infeasible), we iterate through the number of digits, m. The number of digits m is small, ranging from 2 to about 60. For each fixed m, we need to find an integer k that satisfies n = 1 + k + ... + k^(m-1). Since the sum on the right is a monotonically increasing function of k, we can efficiently find k using binary search. The search space for k for a given m is from 2 to n-1, but can be tightly bounded by [2, n^(1/(m-1))]. During the binary search, we must carefully calculate the geometric sum to avoid long integer overflow, as intermediate products can exceed Long.MAX_VALUE. If the calculated sum for a candidate k matches n, we've found a solution. By searching for m from largest to smallest, the first solution found will have the smallest base k.
class Solution { public String smallestGoodBase(String n) { long numN = Long.parseLong(n); // The max number of digits 'm' is for the smallest base k=2. // 2^(m-1) <= n => m-1 <= log2(n) => m <= log2(n) + 1 // For n=10^18, log2(10^18) is approx 59.79. So max m is 60. for (int m = 60; m >= 2; m--) { long low = 2; // Upper bound for k: k^(m-1) < n => k < n^(1/(m-1)) long high = (long) (Math.pow(numN, 1.0 / (m - 1)) + 1); while (low <= high) { long k = low + (high - low) / 2; if (k < 2) { // This can happen if high becomes 1, we should search for k>=2 low = 2; continue; } long sum = 0; long term = 1; boolean overflow = false; for (int i = 0; i < m; i++) { if (Long.MAX_VALUE - term < sum) { overflow = true; break; } sum += term; if (i < m - 1) { // Check for overflow before multiplication if (term > Long.MAX_VALUE / k) { overflow = true; break; } term *= k; } } if (overflow || sum > numN) { high = k - 1; } else if (sum < numN) { low = k + 1; } else { return String.valueOf(k); } } } // Default case for m=2, k = n-1 return String.valueOf(numN - 1); }}Complexity
Time
O((log n)^2). The outer loop runs `O(log n)` times (for `m` from `~60` to 2). For each `m`, we perform a binary search for `k`. The number of binary search steps is `log(n^(1/(m-1))) = (1/(m-1))log(n)`. Inside the binary search, we calculate a sum in `O(m)` time. The total time is the sum over `m`: `Σ [ (log(n)/(m-1)) * m ]` for `m` from 2 to `log n`, which simplifies to `O((log n)^2)`.
Space
O(1) extra space. We only use a few variables to store state during the computation.
Trade-offs
Pros
Correctly solves the problem for large inputs within the given constraints.
Much more efficient than a naive brute-force search over
k.Robust against floating-point inaccuracies as it only uses
Math.powto establish a search bound, not for the final answer.
Cons
Slightly more complex to implement due to the nested loop structure (iteration + binary search).
The binary search adds a logarithmic factor to the complexity for each
m, making it slightly slower in practice than the direct calculation approach, although their asymptotic complexities are the same.
This approach is an optimization of the binary search method. Instead of searching for the base k for each possible number of digits m, we can directly calculate the only possible integer candidate for k. From the equation n = 1 + k + ... + k^(m-1), we can observe that k^(m-1) < n < (k+1)^(m-1). This implies k < n^(1/(m-1)) < k+1. Therefore, the only integer candidate for the base k is floor(n^(1/(m-1))). We can calculate this candidate using Math.pow and then verify if it's a good base by computing the sum 1 + k + ... + k^(m-1) and checking if it equals n. This eliminates the need for a binary search loop.
Algorithm
- Parse the input string
nto alongvariable,numN. - Iterate through the number of digits
mfrom a maximum possible value (e.g., 60) down to 2. - For each
m, calculate the candidate basekusing the approximationk ≈ n^(1/(m-1)). The precise integer candidate isk = floor(n^(1/(m-1))). This can be computed usingk = (long) Math.pow(numN, 1.0 / (m - 1)). - If the calculated
kis less than 2, we can continue to the nextm, as a valid base must be at least 2. - Verify if this candidate
kis a valid good base. Calculate the sumS = 1 + k + k^2 + ... + k^(m-1). - The sum calculation must be done carefully to avoid
longoverflow. - If
SequalsnumN, thenkis a good base. Since we are iteratingmfrom high to low, this is the smallest good base. Returnkas a string. - If the loop completes without finding a solution, the answer corresponds to
m=2, wherek = n - 1. Return(numN - 1)as a string.
Walkthrough
This method improves upon the previous one by avoiding the binary search. The core mathematical insight is that for a fixed number of digits m, there is at most one integer base k that can satisfy the equation n = 1 + k + ... + k^(m-1). This k can be estimated very accurately. Since n is the sum of a geometric series, n is slightly larger than k^(m-1). This gives the approximation k ≈ n^(1/(m-1)). In fact, it can be shown that the only integer candidate for k is floor(n^(1/(m-1))). So, for each m we iterate through, we calculate this single candidate k and check if it works. The check involves computing the geometric sum and comparing it to n, again being careful about potential overflows. This direct calculation is faster than a full binary search.
class Solution { public String smallestGoodBase(String n) { long numN = Long.parseLong(n); // Max m is around 60 for n <= 10^18 for (int m = 60; m >= 2; m--) { // From n = (k^m - 1) / (k - 1), we have k ≈ n^(1/(m-1)) long k = (long) Math.pow(numN, 1.0 / (m - 1)); if (k < 2) { continue; } // Verify if this k is the correct base long sum = 0; long term = 1; boolean overflow = false; for (int i = 0; i < m; i++) { if (Long.MAX_VALUE - term < sum) { overflow = true; break; } sum += term; if (i < m - 1) { if (term > Long.MAX_VALUE / k) { overflow = true; break; } term *= k; } } if (!overflow && sum == numN) { return String.valueOf(k); } } // The trivial case with m=2 digits (11) is always possible. // n = k + 1 => k = n - 1. This is the largest possible base. return String.valueOf(numN - 1); }}Complexity
Time
O((log n)^2). The outer loop runs `O(log n)` times (for `m` from `~60` to 2). Inside the loop, calculating the candidate `k` is fast. The verification step involves a loop that runs `m` times. The total time is the sum of `m` for `m` from 2 to `log n`, which is `Σ m ≈ (log n)^2 / 2`. This is asymptotically the same as the binary search approach but with better constant factors.
Space
O(1) extra space. Constant extra space is used for variables.
Trade-offs
Pros
The most efficient approach in practice due to avoiding the binary search loop.
Conceptually simpler than the binary search approach once the mathematical insight is understood.
Cons
Relies on the precision of
Math.pow. While it works for the given constraints, for extremely large numbers or different floating-point implementations, it might require checkingk-1ork+1as well to be fully robust. For this problem's constraints, it is reliable.
Solutions
Solution
class Solution {public String smallestGoodBase(String n) { long num = Long.parseLong(n); for (int len = 63; len >= 2; --len) { long radix = getRadix(len, num); if (radix != -1) { return String.valueOf(radix); } } return String.valueOf(num - 1); }private long getRadix(int len, long num) { long l = 2, r = num - 1; while (l < r) { long mid = l + r >>> 1; if (calc(mid, len) >= num) r = mid; else l = mid + 1; } return calc(r, len) == num ? r : -1; }private long calc(long radix, int len) { long p = 1; long sum = 0; for (int i = 0; i < len; ++i) { if (Long.MAX_VALUE - sum < p) { return Long.MAX_VALUE; } sum += p; if (Long.MAX_VALUE / p < radix) { p = Long.MAX_VALUE; } else { p *= radix; } } return sum; }}class Solution {public: string smallestGoodBase(string n) { long v = stol(n); int mx = floor(log(v) / log(2)); for (int m = mx; m > 1; --m) { int k = pow(v, 1.0 / m); long mul = 1, s = 1; for (int i = 0; i < m; ++i) { mul *= k; s += mul; } if (s == v) { return to_string(k); } } return to_string(v - 1); }};class Solution: def smallestGoodBase(self, n: str) -> str: def cal(k, m): p = s = 1 for i in range(m): p *= k s += p return s num = int(n) for m in range(63, 1, - 1): l, r = 2, num - 1 while l < r: mid = (l + r) >> 1 if cal(mid, m) >= num: r = mid else: l = mid + 1 if cal(l, m) == num: return str(l) return str(num - 1)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.