Facebook Pixel

3211. Generate Binary Strings Without Adjacent Zeros

MediumBit ManipulationStringBacktracking
Leetcode Link

Problem Description

You are given a positive integer n.

A binary string x is valid if all substrings of x of length 2 contain at least one "1".

Return all valid strings with length n, in any order.

Intuition

To solve this problem, we use a Depth-First Search (DFS) approach to explore every possible binary string combination of length n.

The key is to ensure that every substring of length 2 within the generated binary string contains at least one "1". This requirement implies:

  • If the current bit is 0, the previous bit must be 1 to ensure the substring formed by this and the previous bit contains at least one 1.
  • If the current bit is 1, we can freely choose the previous bit as the condition is met inherently.

Starting from an empty string, we iteratively build each position with the above conditions using recursion until the string reaches length n. This ensures we explore all valid permutations. Each time we reach a string of length n, we add it to the list of valid strings. This form of backtracking ensures we cover all possible options, checking validity at each step.

Learn more about Backtracking patterns.

Solution Approach

The solution uses Depth-First Search (DFS) to generate all valid binary strings of length n. Let's delve into the specifics:

  1. Recursive Function (dfs): We define a recursive helper function dfs(i) where i is the current index in the string being formed. The goal is to fill up the string one bit at a time until it reaches length n.

  2. Base Case: If i equals n, we have a complete binary string of the desired length. At this point, we join the list t (holding the current string) into a string and add it to the list ans.

  3. Exploring Each Bit:

    • We consider both 0 and 1 at each position i.
    • If the current bit j is 0, it can only be placed if it follows a 1. This check is vital since a 0 needs to satisfy the valid substring condition together with its predecessor.
    • If the bit j is 1, it is always valid to place, as any substring ending with 1 meets the criteria.
  4. Backtracking: After placing a bit and recursively filling the next position, we remove the bit (using pop) and backtrack to try other possibilities if needed. This backtracking is crucial to explore different combinations efficiently.

  5. Data Structures:

    • A list t is used to build and store the current configuration of the binary string during each recursive call.
    • A list ans stores all valid string configurations of length n.

Here is the code for the approach:

class Solution:
    def validStrings(self, n: int) -> List[str]:
        def dfs(i: int):
            if i >= n:
                ans.append("".join(t))
                return
            for j in range(2):
                if (j == 0 and (i == 0 or t[i - 1] == "1")) or j == 1:
                    t.append(str(j))
                    dfs(i + 1)
                    t.pop()

        ans = []
        t = []
        dfs(0)
        return ans

This algorithm efficiently generates all possible valid combinations by recursively constructing each possibility and ensuring it adheres to the condition that every pair of consecutive bits in the string contains at least one 1.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution approach with a small example where n = 3.

The task is to generate all valid binary strings of length 3, ensuring every substring of length 2 contains at least one "1".

Step-by-Step Walkthrough using DFS Approach:

  1. Initialization:

    • We start with an empty list t to build our string and an empty list ans to store all valid strings.
  2. Recursive Exploration:

    • We begin by calling dfs(0) to start filling the string from index 0.
  3. Index 0 Exploration:

    • Option 1: Place "1" at index 0.
      • Update t to ['1'].
      • Call dfs(1) to move to the next index.
    • Option 2: Skip "0" at index 0 since the rule requires the preceding bit to be "1".
  4. Index 1 Exploration with t = ['1']:

    • Option 1: Place "0" at index 1 (valid after "1").
      • Update t to ['1', '0'].
      • Call dfs(2).
    • Option 2: Place "1" at index 1.
      • Update t to ['1', '1'].
      • Call dfs(2).
  5. Index 2 Exploration:

    • With t = ['1', '0']:
      • Only Option: Place "1" at index 2 to make t = ['1', '0', '1'].
      • Call dfs(3).
      • Result: Add "101" to ans.
    • With t = ['1', '1']:
      • Option 1: Place "0" at index 2 for a valid string, resulting in t = ['1', '1', '0'].
      • Option 2: Place "1" at index 2 for another valid string, resulting in t = ['1', '1', '1'].
      • Call dfs(3) in both scenarios.
      • Results: Add "110" and "111" to ans.
  6. Conclusion:

    • After backtracking and exploring all possibilities, the ans list contains: ["101", "110", "111"].

Thus, the valid strings of length 3 that meet the criteria are 101, 110, and 111.

Solution Implementation

1from typing import List
2
3class Solution:
4    def validStrings(self, n: int) -> List[str]:
5        def dfs(i: int):
6            # Base case: if the current index is equal to or greater than n, 
7            # append the current sequence to the results
8            if i >= n:
9                ans.append("".join(temp_sequence))
10                return
11
12            # Try appending '0' or '1' to the sequence
13            for j in range(2):
14                # Ensure that '0' can only follow '1' or be at the start
15                if (j == 0 and (i == 0 or temp_sequence[i - 1] == "1")) or j == 1:
16                    temp_sequence.append(str(j))
17                    dfs(i + 1)  # Move to the next index
18                    temp_sequence.pop()  # Backtrack and remove the last character
19
20        ans = []  # This will hold all valid strings
21        temp_sequence = []  # A temporary sequence used during DFS
22        dfs(0)  # Start the DFS from index 0
23        return ans
24
1import java.util.ArrayList;
2import java.util.List;
3
4class Solution {
5    // List to store the resulting valid strings
6    private List<String> ans = new ArrayList<>();
7    // StringBuilder to construct the current string
8    private StringBuilder t = new StringBuilder();
9    // The target length of each string
10    private int n;
11
12    // Method to generate all valid strings of length n
13    public List<String> validStrings(int n) {
14        this.n = n; // Initialize the target length
15        dfs(0); // Start the depth-first search from index 0
16        return ans; // Return the list of valid strings
17    }
18
19    // Depth-first search helper method to build valid strings
20    private void dfs(int i) {
21        // If the current string length equals the target length
22        if (i >= n) {
23            ans.add(t.toString()); // Add the current string to the result list
24            return; // Backtrack
25        }
26
27        // Iterate over possible characters '0' and '1'
28        for (int j = 0; j < 2; ++j) {
29            // Check the condition for appending '0': it must follow '1' or be the first character
30            if (j == 0 && (i == 0 || t.charAt(i - 1) == '1') || j == 1) {
31                t.append(j); // Append the character
32                dfs(i + 1); // Recursively build the next character
33                t.deleteCharAt(t.length() - 1); // Backtrack by removing the last character
34            }
35        }
36    }
37}
38
1class Solution {
2public:
3    vector<string> validStrings(int n) {
4        vector<string> ans; // Vector to store the valid strings
5        string currentString; // Current string being constructed
6
7        // Define a recursive lambda function for depth-first search
8        auto dfs = [&](auto&& dfs, int index) {
9            // Base case: if the current index is equal to n, we have a valid string
10            if (index >= n) {
11                ans.emplace_back(currentString); // Add the current string to the results
12                return;
13            }
14          
15            // Loop to decide the character to append at the current index
16            for (int j = 0; j < 2; ++j) {
17                // Check if we can place '0' or '1' at the current position
18                if ((j == 0 && (index == 0 || currentString[index - 1] == '1')) || j == 1) {
19                    currentString.push_back('0' + j); // Append '0' or '1' to the current string
20                    dfs(dfs, index + 1); // Recursively call dfs for the next index
21                    currentString.pop_back(); // Backtrack: remove the last character
22                }
23            }
24        };
25
26        dfs(dfs, 0); // Initial call to dfs
27        return ans; // Return all valid strings
28    }
29};
30
1// This function generates all valid binary strings of length `n`
2// where the string doesn't contain consecutive zeroes.
3function validStrings(n: number): string[] {
4    const ans: string[] = []; // This will store all valid strings.
5    const t: string[] = []; // This is used to build strings during the depth-first search.
6
7    // Depth-First Search function to build and validate strings.
8    const dfs = (i: number) => {
9        // Base case: if the current index `i` is equal to `n`, push the built string into `ans`.
10        if (i >= n) {
11            ans.push(t.join('')); // Combine the character array into a string and store it.
12            return;
13        }
14      
15        // Loop over the possible characters: '0' and '1'.
16        for (let j = 0; j < 2; ++j) {
17            // Check if '0' can be used (it can only follow '1') or if it's '1'
18            if ((j === 0 && (i === 0 || t[i - 1] === '1')) || j === 1) {
19                t.push(j.toString()); // Add character to the current string.
20                dfs(i + 1);  // Recurse to the next position.
21                t.pop();     // Backtrack: remove the last character added.
22            }
23        }
24    };
25  
26    dfs(0); // Start the recursive process from the first character.
27    return ans; // Return the list of valid strings.
28}
29

Time and Space Complexity

The time complexity of the code is O(n * 2^n). This arises because there are 2^n possible combinations of binary strings of length n. For each valid string, the code constructs the string which takes O(n) time, resulting in O(n * 2^n) overall time complexity.

The space complexity is O(n), which is primarily used for the recursive call stack and the temporary list t. The t list holds characters up to a maximum of n at any recursive depth, and the recursive depth itself can also reach n, resulting in a space complexity of O(n).

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


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Which of these properties could exist for a graph but not a tree?


Recommended Readings

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


Load More