474. Ones and Zeroes
Problem Description
You have an array of binary strings (strings containing only '0's and '1's) and two integer limits: m
for zeros and n
for ones.
Your task is to find the maximum number of strings you can select from the array such that the total count of '0's across all selected strings doesn't exceed m
, and the total count of '1's doesn't exceed n
.
For example, if you have strings ["10", "0001", "111001", "1", "0"]
with m = 5
and n = 3
:
- String "10" has 1 zero and 1 one
- String "0001" has 3 zeros and 1 one
- String "111001" has 2 zeros and 4 ones
- String "1" has 0 zeros and 1 one
- String "0" has 1 zero and 0 ones
You need to select a subset of these strings where the total zeros ≤ 5 and total ones ≤ 3. The goal is to maximize the number of strings in your selection.
The solution uses dynamic programming with a 3D table f[i][j][k]
where:
i
represents considering the firsti
stringsj
represents using at mostj
zerosk
represents using at mostk
onesf[i][j][k]
stores the maximum number of strings that can be selected
For each string, the algorithm counts its zeros and ones, then decides whether to include it or not by comparing the value of including it versus excluding it, ensuring the constraints on zeros and ones are satisfied.
Intuition
This problem is essentially a variant of the classic 0/1 knapsack problem, but with two constraints instead of one. In the traditional knapsack, we have items with weights and values, and we want to maximize value while staying within a weight limit. Here, we want to maximize the number of items (strings) selected while staying within two limits: the number of zeros and the number of ones.
The key insight is that for each string, we face a binary decision: either include it in our subset or don't. This "take it or leave it" nature immediately suggests dynamic programming as a solution approach.
Why can't we use a greedy approach? We might think to select strings with the fewest total characters first, or those with the best ratio of characters to constraints. However, this fails because the optimal solution depends on the combination of strings selected. A string that seems "expensive" (uses many 0s or 1s) might actually be part of the optimal solution if other selected strings complement it well by using mostly the other type of character.
The natural state representation emerges when we think about what information we need to make decisions:
- Which strings we've already considered (position in the array)
- How many zeros we've used so far (or have remaining)
- How many ones we've used so far (or have remaining)
This leads us to define f[i][j][k]
as the maximum number of strings we can select from the first i
strings using at most j
zeros and k
ones.
The recurrence relation follows naturally: for each string, we can either:
- Skip it:
f[i][j][k] = f[i-1][j][k]
- Take it (if we have enough capacity):
f[i][j][k] = f[i-1][j-a][k-b] + 1
wherea
andb
are the counts of zeros and ones in the current string
We choose the maximum of these two options, building up our solution from smaller subproblems to larger ones until we reach f[sz][m][n]
, which gives us the answer.
Solution Approach
The solution implements a 3D dynamic programming approach to solve this knapsack-like problem with two constraints.
Data Structure Setup:
We create a 3D DP table f
with dimensions (sz + 1) × (m + 1) × (n + 1)
where:
- First dimension: represents considering strings from index 0 to i
- Second dimension: represents using at most j zeros
- Third dimension: represents using at most k ones
f = [[[0] * (n + 1) for _ in range(m + 1)] for _ in range(sz + 1)]
The table is initialized with all zeros, which represents the base case: when we have no strings to consider, the maximum subset size is 0.
Main Algorithm:
We iterate through each string in the array using enumerate(strs, 1)
to start indexing from 1 (this aligns with our DP table indexing):
-
Count characters in current string:
a, b = s.count("0"), s.count("1")
For each string, we first count how many zeros (
a
) and ones (b
) it contains. -
Fill the DP table: For each possible capacity of zeros
j
(from 0 to m) and onesk
(from 0 to n):f[i][j][k] = f[i - 1][j][k] # Don't take current string
First, we assume we don't take the current string, inheriting the value from the previous state.
if j >= a and k >= b: f[i][j][k] = max(f[i][j][k], f[i - 1][j - a][k - b] + 1)
If we have enough capacity (j ≥ a zeros and k ≥ b ones), we consider taking the string. We compare:
- Not taking it:
f[i - 1][j][k]
- Taking it:
f[i - 1][j - a][k - b] + 1
(previous state with reduced capacity plus one string)
We keep the maximum of these two options.
- Not taking it:
State Transition: The recurrence relation can be expressed as:
f[i][j][k] = f[i-1][j][k]
if we can't afford the current string (j < a or k < b)f[i][j][k] = max(f[i-1][j][k], f[i-1][j-a][k-b] + 1)
if we can afford it
Final Result:
After processing all strings, f[sz][m][n]
contains the maximum number of strings we can select using at most m
zeros and n
ones.
The time complexity is O(len(strs) × m × n)
as we fill a 3D table, and the space complexity is also O(len(strs) × m × n)
for storing the DP table.
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 walk through a small example to illustrate the solution approach.
Input: strs = ["10", "0", "1"]
, m = 1
(max zeros), n = 1
(max ones)
First, let's count zeros and ones in each string:
- "10": 1 zero, 1 one
- "0": 1 zero, 0 ones
- "1": 0 zeros, 1 one
We create a 3D DP table f[4][2][2]
(dimensions: 4×2×2) initialized with zeros.
Initial state: f[0][j][k] = 0
for all j, k (no strings considered = 0 selected)
Processing string 1: "10" (1 zero, 1 one)
For each capacity combination (j zeros, k ones):
f[1][0][0] = f[0][0][0] = 0
(can't take "10", need 1 zero and 1 one)f[1][0][1] = f[0][0][1] = 0
(can't take "10", need 1 zero)f[1][1][0] = f[0][1][0] = 0
(can't take "10", need 1 one)f[1][1][1] = max(f[0][1][1], f[0][0][0] + 1) = max(0, 0 + 1) = 1
(can take "10")
Processing string 2: "0" (1 zero, 0 ones)
f[2][0][0] = f[1][0][0] = 0
(can't take "0", need 1 zero)f[2][0][1] = f[1][0][1] = 0
(can't take "0", need 1 zero)f[2][1][0] = max(f[1][1][0], f[1][0][0] + 1) = max(0, 0 + 1) = 1
(can take "0")f[2][1][1] = max(f[1][1][1], f[1][0][1] + 1) = max(1, 0 + 1) = 1
(take either "10" or "0")
Processing string 3: "1" (0 zeros, 1 one)
f[3][0][0] = f[2][0][0] = 0
(can't take "1", need 1 one)f[3][0][1] = max(f[2][0][1], f[2][0][0] + 1) = max(0, 0 + 1) = 1
(can take "1")f[3][1][0] = max(f[2][1][0], f[2][1][0] + 1) = 1
(can't take "1", need 1 one)f[3][1][1] = max(f[2][1][1], f[2][1][0] + 1) = max(1, 1 + 1) = 2
(take "0" and "1")
Final answer: f[3][1][1] = 2
The optimal selection is {"0", "1"} which uses exactly 1 zero and 1 one, giving us 2 strings total.
This example demonstrates how the DP table builds up the solution by considering each string and tracking the maximum number of strings we can select for each combination of zero and one capacities.
Solution Implementation
1class Solution:
2 def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
3 """
4 Find the maximum number of strings that can be formed with given m 0's and n 1's.
5 This is a 0-1 knapsack problem with two constraints.
6
7 Args:
8 strs: List of binary strings
9 m: Maximum number of 0's available
10 n: Maximum number of 1's available
11
12 Returns:
13 Maximum number of strings that can be formed
14 """
15 num_strings = len(strs)
16
17 # dp[i][j][k] represents the maximum number of strings that can be formed
18 # using first i strings with at most j zeros and k ones
19 dp = [[[0] * (n + 1) for _ in range(m + 1)] for _ in range(num_strings + 1)]
20
21 # Iterate through each string
22 for string_idx, current_string in enumerate(strs, 1):
23 # Count zeros and ones in current string
24 zeros_count = current_string.count("0")
25 ones_count = current_string.count("1")
26
27 # Iterate through all possible states of zeros (0 to m)
28 for zeros_limit in range(m + 1):
29 # Iterate through all possible states of ones (0 to n)
30 for ones_limit in range(n + 1):
31 # Option 1: Don't include current string
32 dp[string_idx][zeros_limit][ones_limit] = dp[string_idx - 1][zeros_limit][ones_limit]
33
34 # Option 2: Include current string if we have enough zeros and ones
35 if zeros_limit >= zeros_count and ones_limit >= ones_count:
36 dp[string_idx][zeros_limit][ones_limit] = max(
37 dp[string_idx][zeros_limit][ones_limit],
38 dp[string_idx - 1][zeros_limit - zeros_count][ones_limit - ones_count] + 1
39 )
40
41 # Return the maximum strings that can be formed with m zeros and n ones
42 return dp[num_strings][m][n]
43
1class Solution {
2 /**
3 * Find the maximum number of strings that can be formed with given m 0s and n 1s
4 * This is a 0/1 knapsack problem with two constraints (number of 0s and 1s)
5 *
6 * @param strs Array of binary strings
7 * @param m Maximum number of 0s available
8 * @param n Maximum number of 1s available
9 * @return Maximum number of strings that can be formed
10 */
11 public int findMaxForm(String[] strs, int m, int n) {
12 int stringCount = strs.length;
13
14 // dp[i][j][k] represents the maximum number of strings that can be formed
15 // using first i strings with at most j zeros and k ones
16 int[][][] dp = new int[stringCount + 1][m + 1][n + 1];
17
18 // Iterate through each string
19 for (int i = 1; i <= stringCount; i++) {
20 // Count zeros and ones in current string
21 int[] zerosAndOnes = count(strs[i - 1]);
22 int zeros = zerosAndOnes[0];
23 int ones = zerosAndOnes[1];
24
25 // Iterate through all possible combinations of zeros and ones
26 for (int j = 0; j <= m; j++) {
27 for (int k = 0; k <= n; k++) {
28 // Option 1: Don't include current string
29 dp[i][j][k] = dp[i - 1][j][k];
30
31 // Option 2: Include current string if we have enough zeros and ones
32 if (j >= zeros && k >= ones) {
33 dp[i][j][k] = Math.max(
34 dp[i][j][k],
35 dp[i - 1][j - zeros][k - ones] + 1
36 );
37 }
38 }
39 }
40 }
41
42 return dp[stringCount][m][n];
43 }
44
45 /**
46 * Count the number of 0s and 1s in a binary string
47 *
48 * @param s Binary string containing only '0' and '1'
49 * @return Array where index 0 contains count of '0's and index 1 contains count of '1's
50 */
51 private int[] count(String s) {
52 int[] zerosAndOnes = new int[2];
53
54 for (int i = 0; i < s.length(); i++) {
55 // Convert character to digit (0 or 1) and increment corresponding counter
56 zerosAndOnes[s.charAt(i) - '0']++;
57 }
58
59 return zerosAndOnes;
60 }
61}
62
1class Solution {
2public:
3 int findMaxForm(vector<string>& strs, int m, int n) {
4 int numStrings = strs.size();
5
6 // dp[i][j][k] represents the maximum number of strings that can be formed
7 // using the first i strings with at most j zeros and k ones
8 int dp[numStrings + 1][m + 1][n + 1];
9 memset(dp, 0, sizeof(dp));
10
11 // Iterate through each string
12 for (int i = 1; i <= numStrings; ++i) {
13 // Count zeros and ones in the current string
14 auto [zeros, ones] = countZerosAndOnes(strs[i - 1]);
15
16 // Iterate through all possible limits of zeros
17 for (int j = 0; j <= m; ++j) {
18 // Iterate through all possible limits of ones
19 for (int k = 0; k <= n; ++k) {
20 // Option 1: Don't include the current string
21 dp[i][j][k] = dp[i - 1][j][k];
22
23 // Option 2: Include the current string if we have enough zeros and ones
24 if (j >= zeros && k >= ones) {
25 dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - zeros][k - ones] + 1);
26 }
27 }
28 }
29 }
30
31 // Return the maximum number of strings that can be formed with m zeros and n ones
32 return dp[numStrings][m][n];
33 }
34
35private:
36 // Helper function to count the number of zeros and ones in a string
37 pair<int, int> countZerosAndOnes(string& str) {
38 int zeroCount = count_if(str.begin(), str.end(), [](char c) { return c == '0'; });
39 int oneCount = str.size() - zeroCount;
40 return {zeroCount, oneCount};
41 }
42};
43
1/**
2 * Finds the maximum number of strings that can be formed with given m 0s and n 1s
3 * @param strs - Array of binary strings
4 * @param m - Maximum number of 0s available
5 * @param n - Maximum number of 1s available
6 * @returns Maximum number of strings that can be formed
7 */
8function findMaxForm(strs: string[], m: number, n: number): number {
9 const stringCount = strs.length;
10
11 // Initialize 3D DP array: dp[i][j][k] represents max strings using first i strings with j 0s and k 1s
12 const dp = Array.from({ length: stringCount + 1 }, () =>
13 Array.from({ length: m + 1 }, () =>
14 Array.from({ length: n + 1 }, () => 0)
15 )
16 );
17
18 /**
19 * Counts the number of 0s and 1s in a binary string
20 * @param str - Binary string to count
21 * @returns Tuple of [count of 0s, count of 1s]
22 */
23 const countZerosAndOnes = (str: string): [number, number] => {
24 let zeroCount = 0;
25
26 // Count all 0s in the string
27 for (const char of str) {
28 zeroCount += char === '0' ? 1 : 0;
29 }
30
31 // Calculate 1s count as total length minus 0s count
32 const oneCount = str.length - zeroCount;
33 return [zeroCount, oneCount];
34 };
35
36 // Fill the DP table
37 for (let i = 1; i <= stringCount; ++i) {
38 // Get count of 0s and 1s for current string
39 const [zerosNeeded, onesNeeded] = countZerosAndOnes(strs[i - 1]);
40
41 // Iterate through all possible states of available 0s
42 for (let availableZeros = 0; availableZeros <= m; ++availableZeros) {
43 // Iterate through all possible states of available 1s
44 for (let availableOnes = 0; availableOnes <= n; ++availableOnes) {
45 // Option 1: Don't include current string
46 dp[i][availableZeros][availableOnes] = dp[i - 1][availableZeros][availableOnes];
47
48 // Option 2: Include current string if we have enough 0s and 1s
49 if (availableZeros >= zerosNeeded && availableOnes >= onesNeeded) {
50 dp[i][availableZeros][availableOnes] = Math.max(
51 dp[i][availableZeros][availableOnes],
52 dp[i - 1][availableZeros - zerosNeeded][availableOnes - onesNeeded] + 1
53 );
54 }
55 }
56 }
57 }
58
59 // Return the maximum strings that can be formed using all strings with m 0s and n 1s
60 return dp[stringCount][m][n];
61}
62
Time and Space Complexity
Time Complexity: O(sz * m * n)
where sz
is the length of the input array strs
, m
is the maximum number of 0's allowed, and n
is the maximum number of 1's allowed.
The algorithm uses three nested loops:
- The outer loop iterates through all strings in
strs
(sz
iterations) - The middle loop iterates from 0 to
m
(m + 1
iterations) - The inner loop iterates from 0 to
n
(n + 1
iterations)
Additionally, for each string, we count the number of 0's and 1's using s.count()
, which takes O(len(s))
time. If we denote the average length of strings as L
, this adds O(sz * L)
to the complexity. However, since the triple nested loop dominates, the overall time complexity remains O(sz * m * n)
when L
is relatively small compared to m * n
.
Space Complexity: O(sz * m * n)
The algorithm creates a 3D DP array f
with dimensions (sz + 1) * (m + 1) * (n + 1)
. This array stores the maximum number of strings that can be formed for each combination of:
- First
i
strings considered (0 tosz
) - Using at most
j
zeros (0 tom
) - Using at most
k
ones (0 ton
)
The total space required is (sz + 1) * (m + 1) * (n + 1)
, which simplifies to O(sz * m * n)
.
Common Pitfalls
1. Memory Inefficiency in Space Complexity
The Pitfall:
The current solution uses a 3D DP table with dimensions (len(strs) + 1) × (m + 1) × (n + 1)
, which can consume excessive memory when dealing with large inputs. For instance, if you have 600 strings with m=100 and n=100, you'd need to allocate space for 600 × 101 × 101 ≈ 6 million integers, which could cause memory issues or TLE in competitive programming.
Why This Happens: The 3D approach stores all intermediate states for every string, but we only ever need the previous string's states to compute the current string's states. This makes most of the stored data unnecessary.
The Solution - Space Optimization to 2D: Since we only need the previous row (i-1) to compute the current row (i), we can reduce the space complexity from O(len(strs) × m × n) to O(m × n) by using a 2D DP table and updating it in reverse order:
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
# Use 2D DP instead of 3D
dp = [[0] * (n + 1) for _ in range(m + 1)]
for s in strs:
zeros = s.count('0')
ones = s.count('1')
# Iterate in reverse to avoid using updated values
for i in range(m, zeros - 1, -1):
for j in range(n, ones - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)
return dp[m][n]
Key Insight: By iterating backwards (from m to zeros and n to ones), we ensure that when we update dp[i][j]
, we're using the values from the "previous iteration" (before including the current string), not the updated values from the current iteration.
2. Incorrect Loop Order Leading to Wrong Results
The Pitfall: When optimizing to 2D DP, a common mistake is iterating in the forward direction:
# WRONG - This will give incorrect results!
for i in range(zeros, m + 1): # Forward iteration
for j in range(ones, n + 1):
dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)
Why This Fails: Forward iteration causes us to use already-updated values from the current string when calculating new states. This effectively allows us to use the same string multiple times, violating the constraint that each string can only be used once.
Example of the Bug: Consider string "01" (1 zero, 1 one) with m=2, n=2:
- Forward iteration: dp[1][1] gets updated to 1
- Then dp[2][2] = max(0, dp[1][1] + 1) = 2 (using the same string twice!)
- Backward iteration correctly gives dp[2][2] = 1
3. Off-by-One Errors in Boundary Conditions
The Pitfall: Incorrectly setting loop boundaries, especially when checking if we have enough capacity:
# WRONG - May miss valid combinations if zeros_limit > zeros_count and ones_limit > ones_count: # Should be >=
The Solution:
Always use >=
when checking capacity constraints, as we need to include the case where we have exactly enough zeros/ones:
if zeros_limit >= zeros_count and ones_limit >= ones_count: # Include the string
This ensures we don't miss valid selections where the string exactly uses all available zeros or ones.
Which type of traversal does breadth first search do?
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!