Dynamic Programming • 0/1 Knapsack

Ones and Zeroes
Recursion + Memoization

Each binary string has a “cost”: how many 0s and how many 1s it consumes. We want the maximum number of strings we can pick without spending more than m zeros and n ones.

Choice: take / skip State: (index, zerosLeft, onesLeft) Memoize repeated states

1. Reframe the problem

Think of every string as an item with a 2-dimensional cost.

"10" costs (1 zero, 1 one)
"0001" costs (3 zeros, 1 one)
"1" costs (0 zeros, 1 one)

Unlike ordinary knapsack, we do not maximize money or value. Every chosen string is worth exactly 1, so our goal is simply to maximize how many strings are selected.

This is a 0/1 knapsack with two capacities: zeros and ones.

2. Define the recursive state

Let:

dfs(i, z, o) = best answer using strings from index i onward, with z zeros and o ones remaining.

At each string, there are only two possibilities:

SKIP → move to dfs(i+1, z, o)

TAKE → if it fits, get 1 + dfs(...remaining capacity...)

3. Interactive recursion demo

Example: strs = ["10","0001","111001","1","0"], m = 5, n = 3

Zeros used0 / 5
Ones used0 / 3

4. Why memoization matters

Pure recursion can reach the same state through different take/skip histories. Once we have solved (i, z, o), solving it again is wasted work.

Memoization stores the result for that state and reuses it immediately.

Number of possible states ≈ len(strs) × (m+1) × (n+1)

So the time complexity is O(L × m × n + total characters), where L is the number of strings.

Memo cache preview

Watch repeated subproblems become cache hits during the demo.

5. Python — recursion + memoization

class Solution:
    def findMaxForm(self, strs, m, n):
        counts = []

        for s in strs:
            zeros = s.count("0")
            ones = s.count("1")
            counts.append((zeros, ones))

        memo = {}

        def dfs(i, zeros_left, ones_left):
            # Base case: no strings left
            if i == len(strs):
                return 0

            state = (i, zeros_left, ones_left)

            # Reuse an already solved state
            if state in memo:
                return memo[state]

            z, o = counts[i]

            # Option 1: skip current string
            skip = dfs(i + 1, zeros_left, ones_left)

            # Option 2: take current string, if it fits
            take = 0
            if z <= zeros_left and o <= ones_left:
                take = 1 + dfs(
                    i + 1,
                    zeros_left - z,
                    ones_left - o
                )

            memo[state] = max(skip, take)
            return memo[state]

        return dfs(0, m, n)

6. The recurrence

dfs(i,z,o) = max( dfs(i+1,z,o), 1 + dfs(i+1,z-zᵢ,o-oᵢ) )

The second branch is allowed only when the current string fits inside the remaining zero/one budget.

Base case: if i == len(strs), there is nothing left to choose, so return 0.

7. Key interview insight

The important observation is not “count the zeros and ones.” The key is recognizing that each item consumes two limited resources. That turns the problem into a two-capacity 0/1 knapsack.

Once the state is written as (index, zerosLeft, onesLeft), recursion becomes natural, and memoization removes duplicate work.