474. Ones and Zeroes - Explanation
Description
You are given an array of binary strings strs and two integers m and n.
Return the size of the largest subset of strs such that there are at most m 0's and n 1's in the subset.
A set x is a subset of a set y if all elements of x are also elements of y.
Example 1:
Input: strs = ["10","0001","111001","1","0"], m = 5, n = 3
Output: 4Explanation: The largest subset with at most 5 0's and 3 1's is {"10", "0001", "1", "0"}, so the answer is 4.
Other valid but smaller subsets include {"0001", "1"} and {"10", "1", "0"}.
{"111001"} is an invalid subset because it contains 4 1's, greater than the maximum of 3.
Example 2:
Input: strs = ["10","0","1"], m = 1, n = 1
Output: 2Explanation: The largest subset is {"0", "1"}, so the answer is 2.
Constraints:
1 <= strs.length <= 6001 <= strs[i].length <= 100strs[i]consists only of digits0and1.1 <= m, n <= 100
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Understanding how to break problems into smaller subproblems and define base cases
- Dynamic Programming (0/1 Knapsack) - This problem is a variant of the classic knapsack with two constraints instead of one
- Memoization - Caching results of subproblems to avoid redundant computations
- Multidimensional DP - Working with 2D and 3D DP tables for problems with multiple state variables
1. Recursion
Intuition
This problem is a variant of the 0/1 knapsack problem with two constraints instead of one. For each binary string, we must decide whether to include it in our subset or not.
We can try all possible combinations by exploring two branches at each string: include it (if we have enough zeros and ones remaining) or skip it. The goal is to maximize the count of strings we can include while staying within the budget of m zeros and n ones.
Algorithm
- Preprocess each string to count its zeros and ones, storing in an array
arr. - Define a recursive function
dfs(i, m, n)that returns the maximum strings we can select starting from indexiwithmzeros andnones remaining. - Base case: If
ireaches the end of the array, return0. - At each index, we have two choices:
- Skip the current string:
dfs(i + 1, m, n). - Include the current string (if affordable):
1 + dfs(i + 1, m - zeros, n - ones).
- Skip the current string:
- Return the maximum of both choices.
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
arr = [[0] * 2 for _ in range(len(strs))]
for i, s in enumerate(strs):
for c in s:
arr[i][ord(c) - ord('0')] += 1
def dfs(i, m, n):
if i == len(strs):
return 0
res = dfs(i + 1, m, n)
if m >= arr[i][0] and n >= arr[i][1]:
res = max(res, 1 + dfs(i + 1, m - arr[i][0], n - arr[i][1]))
return res
return dfs(0, m, n)Time & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
Where represents the number of binary strings, and and are the maximum allowable counts of zeros and ones, respectively.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems. The same state (i, m, n) can be reached through different paths, leading to redundant computations.
We add memoization to cache results for each unique state. The state is defined by three variables: the current string index, remaining zeros budget, and remaining ones budget. Once we compute the answer for a state, we store it and return immediately on future calls.
Algorithm
- Preprocess each string to count its zeros and ones.
- Create a 3D memoization table indexed by
(i, m, n). - Define
dfs(i, m, n)as before, but check the cache first and store results before returning. - Early termination: If both
mandnare0, we cannot include any more strings. - Return
dfs(0, m, n).
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
arr = [[0] * 2 for _ in range(len(strs))]
for i, s in enumerate(strs):
for c in s:
arr[i][ord(c) - ord('0')] += 1
dp = {}
def dfs(i, m, n):
if i == len(strs):
return 0
if m == 0 and n == 0:
return 0
if (i, m, n) in dp:
return dp[(i, m, n)]
res = dfs(i + 1, m, n)
if m >= arr[i][0] and n >= arr[i][1]:
res = max(res, 1 + dfs(i + 1, m - arr[i][0], n - arr[i][1]))
dp[(i, m, n)] = res
return res
return dfs(0, m, n)Time & Space Complexity
- Time complexity:
- Space complexity:
Where represents the number of binary strings, and and are the maximum allowable counts of zeros and ones, respectively.
3. Dynamic Programming (Bottom-Up)
Intuition
We can convert the top-down approach to bottom-up by building the solution iteratively. We process strings one by one and, for each combination of remaining zeros and ones budget, compute the maximum strings achievable.
The DP table dp[i][j][k] represents the maximum strings from the first i strings using at most j zeros and k ones.
Algorithm
- Preprocess each string to count its zeros and ones.
- Create a 3D DP table of size
(len(strs) + 1) x (m + 1) x (n + 1), initialized to0. - For each string
ifrom1tolen(strs):- For each zeros budget
jfrom0tom:- For each ones budget
kfrom0ton:- Copy the value from the previous string:
dp[i][j][k] = dp[i-1][j][k]. - If we can afford the current string (
j >= zerosandk >= ones):- Update:
dp[i][j][k] = max(dp[i][j][k], 1 + dp[i-1][j-zeros][k-ones]).
- Update:
- Copy the value from the previous string:
- For each ones budget
- For each zeros budget
- Return
dp[len(strs)][m][n].
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
arr = [[0] * 2 for _ in range(len(strs))]
for i, s in enumerate(strs):
for c in s:
arr[i][ord(c) - ord('0')] += 1
dp = [[[0] * (n + 1) for _ in range(m + 1)] for _ in range(len(strs) + 1)]
for i in range(1, len(strs) + 1):
for j in range(m + 1):
for k in range(n + 1):
dp[i][j][k] = dp[i - 1][j][k]
if j >= arr[i - 1][0] and k >= arr[i - 1][1]:
dp[i][j][k] = max(dp[i][j][k], 1 + dp[i - 1][j - arr[i - 1][0]][k - arr[i - 1][1]])
return dp[len(strs)][m][n]Time & Space Complexity
- Time complexity:
- Space complexity:
Where represents the number of binary strings, and and are the maximum allowable counts of zeros and ones, respectively.
4. Dynamic Programming (Space Optimized)
Intuition
Notice that when computing dp[i], we only need values from dp[i-1]. This means we can reduce the 3D table to a 2D table.
The key trick is to iterate the budgets in reverse order. When we update dp[j][k], we need the old values of dp[j-zeros][k-ones]. By iterating backward, we ensure these values have not been overwritten yet in the current iteration.
Algorithm
- Preprocess each string to count its zeros and ones.
- Create a 2D DP table of size
(m + 1) x (n + 1), initialized to0. - For each string with
zeroszeros andonesones:- For
jfrommdown tozeros:- For
kfromndown toones:- Update:
dp[j][k] = max(dp[j][k], 1 + dp[j-zeros][k-ones]).
- Update:
- For
- For
- Return
dp[m][n].
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
arr = [[0, 0] for _ in range(len(strs))]
for i, s in enumerate(strs):
for c in s:
arr[i][ord(c) - ord('0')] += 1
dp = [[0] * (n + 1) for _ in range(m + 1)]
for zeros, ones in arr:
for j in range(m, zeros - 1, -1):
for k in range(n, ones - 1, -1):
dp[j][k] = max(dp[j][k], 1 + dp[j - zeros][k - ones])
return dp[m][n]Time & Space Complexity
- Time complexity:
- Space complexity:
Where represents the number of binary strings, and and are the maximum allowable counts of zeros and ones, respectively.
Common Pitfalls
Iterating Forward Instead of Backward in Space-Optimized DP
When using the 2D space-optimized solution, iterating from small values to large values causes the same string to be counted multiple times in one iteration. You must iterate j from m down to zeros and k from n down to ones to ensure each string is used at most once per subset.
Confusing Zeros and Ones Counts
Mixing up which index stores the count of zeros versus ones leads to incorrect budget checks. Consistently use index 0 for zeros and index 1 for ones (or vice versa) throughout the solution, and ensure the comparison matches this convention.
Treating This as Unbounded Knapsack
This is a 0/1 knapsack problem where each string can be selected at most once. Solutions that allow selecting the same string multiple times will overcount and return incorrect results. Each string must be processed exactly once, either included or excluded.
Sign in to join the discussion