LeetCode #474 Medium

Ones and Zeroes

Given an array of binary strings strs and two integers m and n, return the size of the largest subset of strs such that the subset contains at most m zeros and n ones in total.

dynamic-programmingstrings
Open on LeetCode ↗
02

Intuition

This is a 0/1 knapsack with two weight dimensions — zeros and ones — instead of one. Each string has a 'cost' in zeros and a 'cost' in ones, and a 'value' of 1 (you want to maximise the count of strings selected). The DP table dp[z][o] represents the maximum number of strings you can select using at most z zeros and o ones. For each string, you decide whether to include it (paying its zero and one cost) or skip it — exactly the knapsack choice.

How to spot this pattern

The tell is: you are selecting items from a list, each with a cost in two dimensions, and you want to maximise the count (or total value) under two budget constraints. This is the 2D knapsack. The single-dimension version is the classic 0/1 knapsack; adding a second dimension just adds another loop. The reverse-iteration trick for 0/1 (not unbounded) knapsack applies to each dimension.

03

Approach

1

Count the zeros and ones in each string upfront

For each string in strs, count how many 0s and 1s it contains. Store these counts — they are the two 'weights' for the knapsack. This avoids recounting during the DP transitions.

2

Fill a 2D DP table with reverse iteration

Initialize dp[z][o] = 0 for all 0 <= z <= m and 0 <= o <= n. For each string with z0 zeros and o1 ones, iterate z from m down to z0 and o from n down to o1. Update dp[z][o] = max(dp[z][o], dp[z - z0][o - o1] + 1). The reverse iteration ensures each string is used at most once (0/1 knapsack, not unbounded).

3

Read the answer from `dp[m][n]`

After processing all strings, dp[m][n] holds the maximum subset size. Time is O(len(strs) m n). Space is O(m * n) for the DP table.

04

Solution

1class Solution:
2 def findMaxForm(self, strs, m, n):
3 dp = [[0] * (n + 1) for _ in range(m + 1)]
4 for s in strs:
5 z0 = s.count('0')
6 o1 = s.count('1')
7 for z in range(m, z0 - 1, -1):
8 for o in range(n, o1 - 1, -1):
9 dp[z][o] = max(dp[z][o], dp[z - z0][o - o1] + 1)
10 return dp[m][n]
05

Common pitfalls

Iterating forward instead of backward in the DP loops

✗ Wrong
for z in range(z0, m + 1):
    for o in range(o1, n + 1):
✓ Right
for z in range(m, z0 - 1, -1):
    for o in range(n, o1 - 1, -1):

Forward iteration lets the same string be counted multiple times in a single pass, turning this into an unbounded knapsack. Reverse iteration ensures each string's contribution propagates only once.

Confusing zeros and ones counts in the string

✗ Wrong
z0 = s.count('1')
o1 = s.count('0')
✓ Right
z0 = s.count('0')
o1 = s.count('1')

Swapping the counts assigns the zero budget to ones and vice versa. The DP table misallocates capacity, giving a wrong answer whenever m != n.

Initialising dp to -infinity instead of 0

✗ Wrong
dp = [[-float('inf')] * (n + 1) for _ in range(m + 1)]
✓ Right
dp = [[0] * (n + 1) for _ in range(m + 1)]

The base state is 'zero strings selected using zero capacity', which has value 0, not -infinity. Negative infinity propagates through max operations and corrupts the table.

06

Edge cases

A string is all zeros

Its o1 = 0, so the inner loop over o runs for all values from n down to 0. The string only consumes zero-capacity.

A string exceeds both m and n

The reverse loops start at m and n. If z0 > m or o1 > n, the loop condition z >= z0 or o >= o1 fails immediately, and the string is effectively skipped.

m = 0 and n = 0

dp[0][0] = 0. No string can be selected because every non-empty string has at least one zero or one one.

07

Complexity

Time
O(L * m * n)
Space
O(m * n)
L is the number of strings. The DP table is m * n. Each string is processed once.