Count K-Reducible Numbers Less Than N
HardPrompt
You are given a binary string s representing a number n in its binary form.
You are also given an integer k.
An integer x is called k-reducible if performing the following operation at most k times reduces it to 1:
- Replace
xwith the count of set bits in its binary representation.
For example, the binary representation of 6 is "110". Applying the operation once reduces it to 2 (since "110" has two set bits). Applying the operation again to 2 (binary "10") reduces it to 1 (since "10" has one set bit).
Return an integer denoting the number of positive integers less than n that are k-reducible.
Since the answer may be too large, return it modulo 109 + 7.
Example 1:
Input: s = "111", k = 1
Output: 3
Explanation:
n = 7. The 1-reducible integers less than 7 are 1, 2, and 4.
Example 2:
Input: s = "1000", k = 2
Output: 6
Explanation:
n = 8. The 2-reducible integers less than 8 are 1, 2, 3, 4, 5, and 6.
Example 3:
Input: s = "1", k = 3
Output: 0
Explanation:
There are no positive integers less than n = 1, so the answer is 0.
Constraints:
1 <= s.length <= 800shas no leading zeros.sconsists only of the characters'0'and'1'.1 <= k <= 5
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses digit dynamic programming to count the numbers without iterating through all numbers up to n, which is infeasible given the constraints. The core idea is to build the binary representation of numbers digit by digit and count how many valid numbers can be formed. The validity check (whether a number is k-reducible) is performed on-the-fly within the recursion's base case. This method is conceptually simpler but less performant due to repeated calculations.
Algorithm
- Define a helper function
is_p_reducible(p, j)that checks if a numberpisj-reducible by applyingpopcountjtimes. - Define a memoized recursive function
solve(index, pop_count, less). - The state of the recursion is defined by the current bit
index, thepop_countof the prefix, and a booleanis_lessindicating if the number is already smaller thann's prefix. - The base case for the recursion is
index == s.length(). Here, check if the finalpop_countis(k-1)-reducible using the helper function. Return 1 if it is, 0 otherwise. - In the recursive step, iterate through possible digits ('0' or '1') for the current
indexand make recursive calls forindex + 1with updated parameters. - The initial call is
solve(0, 0, false). - The result counts k-reducible numbers in
[0, n]. Subtract 1 ifnitself is k-reducible to get the final answer for numbers< n.
Walkthrough
The solution revolves around a recursive function, let's call it solve(index, pop_count, is_less), which counts the number of k-reducible integers that can be formed from a given state.
-
State Definition:
index: The current bit position (from left, 0-indexed) we are filling.pop_count: The number of set bits ('1's) in the prefix of the number constructed so far.is_less: A boolean flag that is true if the prefix we have built is already smaller than the corresponding prefix ofn(given by strings).
-
k-Reducibility Check: A number
xis k-reducible if and only if its popcount,p = popcount(x), is(k-1)-reducible. A helper functionis_p_reducible(p, j)is created to check this. It works by applying thepopcountoperationjtimes topand checking if the final result is 1. -
Recursion and Base Case:
- The recursion explores placing a '0' or a '1' at the current
index. The upper bound for the digit is '1' ifis_lessis true (we can use any digit), ors[index]ifis_lessis false (we are restricted to matchn's prefix). - The base case for the recursion is when
index == s.length(). At this point, a full number has been formed. We check if itspop_countmakes itk-reducible. That is, we callis_p_reducible(current_popcount, k-1). If it is, we have found one valid number and return 1; otherwise, we return 0.
- The recursion explores placing a '0' or a '1' at the current
-
Memoization: To avoid recomputing results for the same state
(index, pop_count, is_less), we use a 3D arraymemoto store the results. -
Final Calculation: The initial call is
solve(0, 0, false). This counts all k-reducible numbers in[0, n]. Since we need to count positive integers strictly less thann, we must subtract 1 from the total ifnitself is k-reducible. The number 0 has a popcount of 0, which is never reducible, so it's correctly excluded.
import java.util.Arrays; class Solution { private long[][][] memo; private String s; private int n; private int k; private static final int MOD = 1_000_000_007; private boolean isPReducible(int p, int j) { if (p <= 0) return false; if (j < 0) return false; int currentVal = p; for (int i = 0; i < j; i++) { currentVal = Integer.bitCount(currentVal); } return currentVal == 1; } public int countKReducible(String s, int k) { this.s = s; this.n = s.length(); this.k = k; this.memo = new long[n][n + 1][2]; for (long[][] arr2D : memo) { for (long[] arr1D : arr2D) { Arrays.fill(arr1D, -1); } } long totalCount = solve(0, 0, false); int popcountN = 0; for (char c : s.toCharArray()) { if (c == '1') popcountN++; } if (isPReducible(popcountN, k - 1)) { totalCount = (totalCount - 1 + MOD) % MOD; } return (int) totalCount; } private long solve(int idx, int count, boolean isLess) { if (idx == n) { return isPReducible(count, k - 1) ? 1 : 0; } if (memo[idx][count][isLess ? 1 : 0] != -1) { return memo[idx][count][isLess ? 1 : 0]; } long ans = 0; int upperBound = isLess ? 1 : s.charAt(idx) - '0'; for (int digit = 0; digit <= upperBound; digit++) { boolean newIsLess = isLess || (digit < upperBound); ans = (ans + solve(idx + 1, count + digit, newIsLess)) % MOD; } return memo[idx][count][isLess ? 1 : 0] = ans; }}Complexity
Time
O(L^2 * k * log L), where L is the length of `s`. The DP has `O(L^2)` states. The transition is O(1), but the base case involves a call to `is_p_reducible`, which takes `O(k * log L)` time as `popcount` is applied `k-1` times on values up to `L`.
Space
O(L^2) for the memoization table.
Trade-offs
Pros
Relatively simple to implement as it avoids complex precomputation logic.
Uses less memory compared to the precomputation approach as it doesn't need an extra table for target popcounts.
Cons
Less efficient due to redundant computations. The
is_p_reduciblefunction is called many times for the same popcount values.The time complexity has a factor of
k, making it slower for largerk.
This is the most efficient approach, building upon the digit DP strategy. It significantly improves performance by precomputing all the necessary information before starting the DP process. Specifically, we precompute which popcount values result in a k-reducible number. This turns the expensive check in the DP's base case into a simple O(1) lookup.
Algorithm
- Precompute the set of target popcounts. A popcount
pis a target ifpis(k-1)-reducible. - This can be done with a DP table
is_reducible[j][p], whereis_reducible[j][p]is true ifpisj-reducible. - The base case is
is_reducible[0][1] = true. - The transition is
is_reducible[j][p] = is_reducible[j-1][popcount(p)]. - Store the final
is_reducible[k-1]array as thetargetPopcountslookup table. - Use the same memoized recursive function
solve(index, pop_count, is_less)as in the less efficient approach. - In the base case (
index == s.length()), perform anO(1)lookup in thetargetPopcountstable to check validity. - The final result is obtained by calling
solve(0, 0, false)and adjusting fornitself.
Walkthrough
This optimized solution consists of two main phases: precomputation and digit DP.
-
Precomputation of Target Popcounts: The key insight is that a number
xisk-reducibleif and only if itspopcount(x)is(k-1)-reducible. The numbers we are counting are less thann, so their popcount is at mostL = s.length(). We can determine all popcount valuesp in [1, L]that are(k-1)-reducible. We can use dynamic programming for this precomputation. Letis_reducible[j][p]be a boolean indicating ifpisj-reducible.- Base case:
is_reducible[0][p]is true only forp=1. - Transition:
is_reducible[j][p]is true ifis_reducible[j-1][popcount(p)]is true. We compute this table forjfrom 1 tok-1. The set of target popcounts,T, will be allpfor whichis_reducible[k-1][p]is true.
- Base case:
-
Digit DP with Memoization: With the target popcounts precomputed in a boolean array
targetPopcounts, the digit DP structure is the same as the previous approach. We use a recursive functionsolve(index, pop_count, is_less)with the same state definition and memoization. The crucial difference is in the base case (index == s.length()). Instead of calling a helper function, we simply checktargetPopcounts[pop_count]. This is anO(1)lookup. -
Final Calculation: The main logic remains the same. We call
solve(0, 0, false)to get the count of k-reducible numbers in[0, n]. Then, we subtract 1 ifnitself is k-reducible (i.e., ifpopcount(n)is in our precomputed set of target popcounts).
import java.util.Arrays; class Solution { private long[][][] memo; private String s; private boolean[] targetPopcounts; private int n; private static final int MOD = 1_000_000_007; public int countKReducible(String s, int k) { this.s = s; this.n = s.length(); // Step 1: Precompute target popcounts this.targetPopcounts = new boolean[n + 1]; if (k > 0) { boolean[] isPReducible = new boolean[n + 1]; isPReducible[1] = true; // 1 is 0-reducible for (int j = 1; j < k; j++) { boolean[] nextIsPReducible = new boolean[n + 1]; for (int p = 1; p <= n; p++) { int pop = Integer.bitCount(p); if (pop <= n && isPReducible[pop]) { nextIsPReducible[p] = true; } } isPReducible = nextIsPReducible; } this.targetPopcounts = isPReducible; } // Step 2: DP with memoization this.memo = new long[n][n + 1][2]; for (long[][] arr2D : memo) { for (long[] arr1D : arr2D) { Arrays.fill(arr1D, -1); } } long totalCount = solve(0, 0, false); int popcountN = 0; for (char c : s.toCharArray()) { if (c == '1') { popcountN++; } } if (popcountN > 0 && targetPopcounts[popcountN]) { totalCount = (totalCount - 1 + MOD) % MOD; } return (int) totalCount; } private long solve(int idx, int count, boolean isLess) { if (idx == n) { return (count > 0 && targetPopcounts[count]) ? 1 : 0; } if (memo[idx][count][isLess ? 1 : 0] != -1) { return memo[idx][count][isLess ? 1 : 0]; } long ans = 0; int upperBound = isLess ? 1 : s.charAt(idx) - '0'; for (int digit = 0; digit <= upperBound; digit++) { boolean newIsLess = isLess || (digit < upperBound); ans = (ans + solve(idx + 1, count + digit, newIsLess)) % MOD; } return memo[idx][count][isLess ? 1 : 0] = ans; }}Complexity
Time
O(L^2 + k*L), where L is the length of `s`. The precomputation of target popcounts takes `O(k*L)`. The digit DP part has `O(L^2)` states, and each transition takes `O(1)` time. The total time is dominated by the DP, so it's effectively `O(L^2)`.
Space
O(L^2 + k*L). `O(L^2)` for the memoization table and `O(k*L)` for the reducibility precomputation table (can be optimized to `O(L)`).
Trade-offs
Pros
Highly efficient, with a time complexity independent of
kfor the DP part.The expensive reducibility check is factored out into a single precomputation step.
Optimal solution for the given constraints.
Cons
More complex to implement due to the initial precomputation phase.
Requires additional space for the precomputation table (
O(k*L)).
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.