Facebook Pixel

3955. Valid Binary Strings With Cost Limit

MediumBit ManipulationStringBacktrackingEnumeration
LeetCode ↗

Problem Description

You are given two integers n and k.

The cost of a binary string s is defined as the sum of all indices i (0-based) such that s[i] == '1'. In other words, for every position where the character is 1, you add that position's index to the total cost.

A binary string is considered valid if it satisfies both of the following conditions:

  • It does not contain two consecutive '1' characters (no two 1s are adjacent).
  • Its cost is less than or equal to k.

Your task is to return a list of all valid binary strings of length n in any order.

Example walkthrough of the cost:

For the string "0101", the characters 1 appear at indices 1 and 3, so the cost is 1 + 3 = 4. This string also has no two consecutive 1s, so it is valid as long as 4 <= k.

For the string "110", the characters 1 appear at indices 0 and 1, but since these two 1s are adjacent, the string is not valid regardless of its cost.

The result must include every binary string of length n that meets both conditions, and the strings may be returned in any order.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Brute force / Backtracking?

This problem maps to Brute force / Backtracking through a short path in the full flowchart.

Smallconstraints?yesBruteforceenough?yesBrute force /Backtracking

Enumerating configurations with backtracking works for the input size.

Open in Flowchart
Show step-by-step reasoning

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: The problem deals with generating binary strings, not nodes and edges connected in a graph structure.

Need to solve for kth smallest/largest?

  • No: We are not looking for a kth smallest or largest element; we need to enumerate all valid strings.

Involves Linked Lists?

  • No: The problem works with binary strings and indices, not linked list nodes.

Does the problem have small constraints?

  • Yes: Since we are asked to return all valid binary strings of length n, the value of n must be small enough that listing every result is feasible. This signals a search over all possibilities.

Brute force / Backtracking?

  • Yes: To enumerate every valid binary string, we build the string character by character, choosing 0 or 1 at each position while respecting the "no two consecutive 1s" and "cost <= k" constraints. Backtracking lets us undo choices and explore all combinations systematically.

Conclusion: The flowchart guides us toward a Backtracking approach, where we recursively place each character, prune branches that violate the constraints, and collect all valid strings of length n.

Intuition

The key observation is that each position in the binary string can only hold one of two values: 0 or 1. This naturally suggests building the string one character at a time, exploring both choices at every position. Whenever we make all n choices, we have a complete candidate string.

But not every combination is valid. Two rules limit which strings we can keep:

  1. No two consecutive 1s — this is a local constraint. When we are about to place a 1 at position i, we only need to check the character we just placed at position i - 1. If that previous character is 1 (or, more precisely, if the last character placed was a 1), we cannot place another 1 here.

  2. Cost must not exceed k — placing a 1 at index i adds i to the running cost. So if we keep track of the total cost accumulated so far (call it tot), placing a 1 at position i is only allowed when tot + i <= k.

Both of these constraints can be checked the moment we are about to place a 1. This is the heart of the idea: instead of generating all 2^n strings and then filtering, we prune invalid branches early. If placing a 1 would break a rule, we simply skip that branch and never explore the strings beneath it.

This leads directly to a recursive, depth-first construction:

  • At each position i, always try placing a 0, since 0 never violates either rule (it adds nothing to the cost and never creates consecutive 1s).
  • Then, only if it is safe, also try placing a 1. It is safe when the previous character is 0 (or there is no previous character) and tot + i <= k.

By carrying tot along through the recursion, we always know the current cost without recomputing it. And by maintaining a shared path list, we can append a character, recurse, and then pop it off to restore the state — this backtracking step lets us reuse the same structure to explore every valid combination. Once the recursion reaches i >= n, the path forms a complete valid string that we add to the answer.

Pattern Learn more about Backtracking patterns.

Solution Approach

We implement the idea using DFS (Depth-First Search) with backtracking. The core is a recursive function dfs(i, tot), where:

  • i represents the current position being processed in the string;
  • tot represents the sum of the indices of all 1s placed so far (the running cost).

We also maintain two shared structures outside the recursion:

  • path: a list used as a stack that holds the characters chosen so far. Building the string this way lets us cheaply append and pop characters as we move forward and backtrack.
  • ans: the list that collects every completed valid string.

1. Base Case (Termination Condition)

When i >= n, the string has reached its full length n. At this point we join the characters in path into a single string and append it to ans:

if i >= n:
    ans.append("".join(path))
    return

2. Choosing 0

A 0 can always be placed at the current position, because it never creates two consecutive 1s and adds nothing to the cost. So we append "0", recurse to the next position with the same tot, and then pop it off to restore the state:

path.append("0")
dfs(i + 1, tot)
path.pop()

3. Choosing 1

A 1 may only be placed when both conditions hold:

  • The previous character does not exist (path is empty) or is 0 — checked by not path or path[-1] == "0". This guarantees no two consecutive 1s.
  • Adding index i keeps the cost within the limit — checked by tot + i <= k.

When safe, we append "1", recurse with the updated cost tot + i, and pop it off afterward:

if (not path or path[-1] == "0") and tot + i <= k:
    path.append("1")
    dfs(i + 1, tot + i)
    path.pop()

4. Backtracking

After each recursive call returns, the matching path.pop() undoes the choice just made, restoring path to its previous state. This lets the algorithm explore the other branch (or branches of the parent call) without leftover characters contaminating the result.

Putting it together

We kick off the search from the first position with zero cost:

ans = []
path = []
dfs(0, 0)
return ans

Because we check both constraints before descending into a 1 branch, invalid paths are pruned early rather than generated and filtered later. The data structures involved are simple — a list acting as a stack (path) and a result list (ans) — and the pattern is classic backtracking over a binary decision tree, where each level corresponds to one position in the string and each node branches into placing a 0 or (conditionally) a 1.

Example Walkthrough

Let's trace through the algorithm with a small example: n = 3 and k = 2.

We want all binary strings of length 3 that have no two consecutive 1s and a cost ≤ 2 (where cost = sum of indices holding a 1).

We start the search with dfs(0, 0) — position 0, running cost 0, and an empty path.


Position 0 (i = 0)

  • Place "0"path = ["0"], recurse into dfs(1, 0).
  • Try "1"? Previous char doesn't exist (path empty after pop) and tot + i = 0 + 0 = 0 ≤ 2. Safe → path = ["1"], recurse into dfs(1, 0).

Let's follow the "0" branch first.


Branch path = ["0"]dfs(1, 0)

Position 1 (i = 1)

  • Place "0"path = ["0","0"], recurse into dfs(2, 0).
    • Position 2 (i = 2)
      • Place "0"path = ["0","0","0"], recurse into dfs(3, 0).
        • i >= n → record "000" ✓ (cost 0)
      • Try "1"? Last char is "0" ✓ but tot + i = 0 + 2 = 2 ≤ 2 ✓ → safe.
        • path = ["0","0","1"], dfs(3, 2) → record "001" ✓ (cost 2)
  • Try "1" at position 1? Last char is "0" ✓ and tot + i = 0 + 1 = 1 ≤ 2 ✓ → safe.
    • path = ["0","1"], recurse into dfs(2, 1).
      • Position 2 (i = 2)
        • Place "0"path = ["0","1","0"], dfs(3, 1) → record "010" ✓ (cost 1)
        • Try "1"? Last char is "1" ✗ → pruned (would create consecutive 1s).

After this branch finishes, all the pops restore path back to [].


Branch path = ["1"]dfs(1, 0) (cost so far is 0, since index 0 adds nothing)

Position 1 (i = 1)

  • Place "0"path = ["1","0"], recurse into dfs(2, 0).
    • Position 2 (i = 2)
      • Place "0"path = ["1","0","0"], dfs(3, 0) → record "100" ✓ (cost 0)
      • Try "1"? Last char is "0" ✓ and tot + i = 0 + 2 = 2 ≤ 2 ✓ → safe.
        • path = ["1","0","1"], dfs(3, 2) → record "101" ✓ (cost 2)
  • Try "1" at position 1? Last char is "1" ✗ → pruned.

Final result

Collecting everything we recorded:

["000", "001", "010", "100", "101"]

Why some strings are missing:

StringConsecutive 1s?CostValid?
000no0
001no2
010no1
011yes (idx 1,2)✗ pruned
100no0
101no2
110yes (idx 0,1)✗ pruned
111yes✗ pruned

Notice the algorithm never generated 011, 110, or 111 — the consecutive-1 check pruned those branches the moment a second adjacent 1 was attempted. The cost constraint didn't eliminate anything here because k = 2 was generous enough, but had we set k = 1, the string "001" (cost 2) and "101" (cost 2) would also have been pruned at the tot + i ≤ k check.

This demonstrates the key efficiency of the approach: invalid candidates are cut off early during construction rather than being built fully and filtered out afterward.

Solution Implementation

1class Solution:
2    def generateValidStrings(self, n: int, k: int) -> list[str]:
3        ans: list[str] = []  # Stores all valid binary strings
4        path: list[str] = []  # Current string being built, character by character
5
6        def dfs(index: int, weighted_sum: int) -> None:
7            # Base case: a complete string of length n has been built
8            if index >= n:
9                ans.append("".join(path))
10                return
11
12            # Choice 1: place '0' at the current index (always allowed)
13            path.append("0")
14            dfs(index + 1, weighted_sum)
15            path.pop()
16
17            # Choice 2: place '1' at the current index, allowed only if:
18            #   - the previous character is '0' (or this is the first char),
19            #     ensuring no two adjacent '1's
20            #   - adding index to the weighted sum keeps it within the limit k
21            if (not path or path[-1] == "0") and weighted_sum + index <= k:
22                path.append("1")
23                dfs(index + 1, weighted_sum + index)
24                path.pop()
25
26        dfs(0, 0)
27        return ans
28
1class Solution {
2    // Target length of each binary string.
3    private int n;
4    // Threshold used to bound the accumulated index sum of placed '1' characters.
5    private int k;
6    // Collects all valid generated strings.
7    private List<String> ans;
8    // Mutable buffer representing the current candidate string being built.
9    private StringBuilder path;
10
11    public List<String> generateValidStrings(int n, int k) {
12        this.n = n;
13        this.k = k;
14        this.ans = new ArrayList<>();
15        this.path = new StringBuilder();
16
17        // Start the depth-first search from index 0 with an accumulated total of 0.
18        dfs(0, 0);
19
20        return ans;
21    }
22
23    /**
24     * Builds candidate strings character by character.
25     *
26     * @param index the current position to fill in the string
27     * @param total the running sum of indices at which '1' characters were placed
28     */
29    private void dfs(int index, int total) {
30        // A full-length string has been formed; record it.
31        if (index >= n) {
32            ans.add(path.toString());
33            return;
34        }
35
36        // Branch 1: place a '0' at the current position (always allowed).
37        path.append('0');
38        dfs(index + 1, total);
39        path.deleteCharAt(path.length() - 1);
40
41        // Branch 2: place a '1' only if the previous character is not '1'
42        // (preventing two consecutive '1's) and the index-sum constraint holds.
43        if ((path.length() == 0 || path.charAt(path.length() - 1) == '0')
44                && total + index <= k) {
45            path.append('1');
46            dfs(index + 1, total + index);
47            path.deleteCharAt(path.length() - 1);
48        }
49    }
50}
51
1class Solution {
2public:
3    vector<string> generateValidStrings(int n, int k) {
4        vector<string> result;  // Stores all valid generated strings
5        string path;            // Current string being built during DFS
6
7        // Recursive depth-first search.
8        // index    : current position in the string (0-based)
9        // weightSum: accumulated weighted sum of positions where '1' was placed
10        auto dfs = [&](this auto&& dfs, int index, int weightSum) -> void {
11            // Base case: a full-length string has been built
12            if (index >= n) {
13                result.push_back(path);
14                return;
15            }
16
17            // Branch 1: place '0' at the current position (always allowed)
18            path.push_back('0');
19            dfs(index + 1, weightSum);
20            path.pop_back();
21
22            // Branch 2: place '1' at the current position, allowed only when:
23            //   - the previous character is '0' (or the string is empty),
24            //     i.e. no two consecutive '1's,
25            //   - and adding the current position index keeps the weighted
26            //     sum within the limit k.
27            if ((path.empty() || path.back() == '0') && weightSum + index <= k) {
28                path.push_back('1');
29                dfs(index + 1, weightSum + index);
30                path.pop_back();
31            }
32        };
33
34        // Start the search from position 0 with an empty weighted sum
35        dfs(0, 0);
36
37        return result;
38    }
39};
40
1// Stores all valid binary strings that satisfy the constraints
2const result: string[] = [];
3
4// Tracks the current path (sequence of characters) during DFS traversal
5const currentPath: string[] = [];
6
7/**
8 * Depth-first search to build valid strings character by character.
9 *
10 * @param index           The current position being filled (0-based).
11 * @param indexSumOfOnes  The accumulated sum of indices where '1' has been placed.
12 */
13const dfs = (index: number, indexSumOfOnes: number): void => {
14    // Base case: a complete string of length n has been built
15    if (index >= n) {
16        result.push(currentPath.join(''));
17        return;
18    }
19
20    // Option 1: always allowed to place a '0' at the current position
21    currentPath.push('0');
22    dfs(index + 1, indexSumOfOnes);
23    currentPath.pop();
24
25    // Option 2: place a '1' only if two conditions hold:
26    //   a) the previous character is '0' (or path is empty), preventing consecutive '1's
27    //   b) adding this index keeps the running sum within the limit k
28    const previousIsZero =
29        currentPath.length === 0 ||
30        currentPath[currentPath.length - 1] === '0';
31
32    if (previousIsZero && indexSumOfOnes + index <= k) {
33        currentPath.push('1');
34        dfs(index + 1, indexSumOfOnes + index);
35        currentPath.pop();
36    }
37};
38
39/**
40 * Generates all valid binary strings of length n under the given constraints:
41 *   - No two consecutive '1' characters.
42 *   - The sum of indices at which '1' appears must not exceed k.
43 *
44 * @param n  The length of each generated string.
45 * @param k  The upper bound for the sum of indices where '1' is placed.
46 * @returns  An array of all valid binary strings.
47 */
48function generateValidStrings(n: number, k: number): string[] {
49    // Reset shared state in case the function is invoked multiple times
50    result.length = 0;
51    currentPath.length = 0;
52
53    // Start the recursive search from position 0 with an empty index sum
54    dfs(0, 0);
55
56    return result;
57}
58

Time and Space Complexity

  • Time Complexity: O(n × 2^n). The dfs function explores a binary recursion tree where at each position i we attempt to place "0" and conditionally place "1". In the worst case the recursion generates up to O(2^n) valid strings (the constraint tot + i <= k may not prune significantly when k is large). For each complete string reaching the base case i >= n, the operation "".join(path) costs O(n) to construct the string of length n. Therefore the overall time complexity is O(n × 2^n).

  • Space Complexity: O(n). Ignoring the space used by the output list ans, the auxiliary space is dominated by the recursion call stack and the path list. The recursion depth reaches at most n (one level per position), and path holds at most n characters at any time. Thus the extra space usage is O(n).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall: Checking the "no two consecutive 1s" condition incorrectly after popping

The most common mistake lies in how the adjacency constraint is verified. In the given solution, the check not path or path[-1] == "0" works only because the 1 branch is explored after the 0 branch has fully returned and popped its character. Developers often unknowingly break this invariant by reordering the branches or by forgetting a path.pop(), which corrupts path[-1] and produces strings with adjacent 1s.

Why it's subtle:

When you reach the 1 branch, path reflects all characters placed at indices 0 .. index-1. The check path[-1] == "0" peeks at the character at index - 1. If a previous recursive call forgot to pop, path[-1] may point to a stale "1" from a sibling branch, silently allowing "11" to form — or worse, path may even be longer than index, throwing off the entire correspondence between list length and position.

Example of the broken version:

# BUG: missing path.pop() after the '0' branch
path.append("0")
dfs(index + 1, weighted_sum)
# path.pop()   <-- forgotten!

if (not path or path[-1] == "0") and weighted_sum + index <= k:
    path.append("1")
    dfs(index + 1, weighted_sum + index)
    path.pop()

Here path[-1] is always "0" (the leftover from the first branch), so the 1 branch fires even when the real previous character was a 1, and the produced strings are far longer than n.

Solution: Decouple state from the shared path by passing the previous character explicitly

A robust way to avoid relying on a fragile shared-stack invariant is to pass the relevant state (the previous character) as a function argument. This makes the adjacency check self-contained and immune to forgotten pops:

class Solution:
    def generateValidStrings(self, n: int, k: int) -> list[str]:
        ans: list[str] = []
        path: list[str] = []

        def dfs(index: int, weighted_sum: int, prev: str) -> None:
            if index >= n:
                ans.append("".join(path))
                return

            # place '0'
            path.append("0")
            dfs(index + 1, weighted_sum, "0")
            path.pop()

            # place '1' — adjacency now depends on the explicit `prev`,
            # not on the possibly-stale path[-1]
            if prev == "0" and weighted_sum + index <= k:
                path.append("1")
                dfs(index + 1, weighted_sum + index, "1")
                path.pop()

        # start with prev = "0" so the first position may hold a '1'
        dfs(0, 0, "0")
        return ans

Why this is safer:

  • The adjacency decision no longer reads from path, so even if a pop were missing the correctness of the 1-placement check is preserved (the string length bug would still surface, but the two failure modes are now decoupled and easier to diagnose).
  • Initializing prev = "0" cleanly handles the first position without the special not path case, eliminating one branch of conditional logic.
  • Passing state down the recursion is a general defensive pattern in backtracking: it reduces hidden coupling between sibling calls.

Bonus pitfall: Early pruning misconception with tot + i <= k

One might assume that once tot + i > k at some index i, all deeper placements are hopeless and the entire subtree can be abandoned. That is incorrect here, because placing a 0 at index i still allows valid strings to be completed — only the 1 at that specific index is forbidden. The code correctly guards only the 1 branch with the cost check and never prunes the 0 branch, which is exactly right. Mistakenly adding a global if weighted_sum > k: return at the top would still be safe (since cost never decreases), but adding if weighted_sum + index > k: return would wrongly discard valid strings that place 0 at the expensive index.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More