3211. Generate Binary Strings Without Adjacent Zeros
Problem Description
You are given a positive integer n
and need to generate all valid binary strings of length n
.
A binary string is considered valid if every substring of length 2 contains at least one "1"
. In other words, you cannot have two consecutive "0"
s anywhere in the string.
For example:
"101"
is valid because all substrings of length 2 ("10"
and"01"
) contain at least one"1"
"100"
is invalid because the substring"00"
contains no"1"
"111"
is valid because all substrings of length 2 ("11"
and"11"
) contain at least one"1"
"1010"
is valid because all substrings of length 2 ("10"
,"01"
,"10"
) contain at least one"1"
The solution uses a depth-first search (DFS) approach to build valid strings character by character. At each position i
, it tries to place either "0"
or "1"
. When placing a "0"
, it checks if the previous character is "1"
(to avoid consecutive zeros). When placing a "1"
, it can always proceed since "1"
followed by any character will form a valid substring. The algorithm recursively builds all possible valid strings and collects them in the result list.
The function returns all valid binary strings of length n
in any order.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves generating binary strings, not graph traversal with nodes and edges.
Need to solve for kth smallest/largest?
- No: We need to generate all valid strings, not find a specific kth element.
Involves Linked Lists?
- No: The problem is about generating strings, not manipulating linked list structures.
Does the problem have small constraints?
- Yes: The problem asks us to generate all valid binary strings of length
n
. Since we need to enumerate all possibilities and return them as a list,n
is typically small (otherwise the output would be exponentially large). For a string of lengthn
, there can be at most2^n
possible strings.
Brute force / Backtracking?
- Yes: We need to explore all possible valid binary strings by trying different choices at each position. This is a classic backtracking scenario where we:
- Build the string character by character
- At each position, try placing either '0' or '1'
- Check validity constraints (no consecutive zeros)
- Backtrack if invalid or continue building
- Collect complete valid strings
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This matches the solution perfectly - we recursively build strings position by position, validate constraints (checking if we can place a '0' based on the previous character), and backtrack when needed to explore all valid possibilities.
Intuition
When we need to generate all valid binary strings with a specific constraint, we can think of building the string one character at a time. At each position, we have two choices: place a '0'
or place a '1'
.
The key constraint here is that we cannot have two consecutive '0'
s. This means whenever we want to place a '0'
at position i
, we need to ensure that the character at position i-1
is not '0'
(it must be '1'
). On the other hand, we can always place a '1'
at any position without violating the constraint.
This naturally leads to a backtracking approach where we:
- Start with an empty string and try to build it character by character
- At each position, explore both possibilities (
'0'
and'1'
) - Before placing a
'0'
, check if it's valid (either it's the first character, or the previous character is'1'
) - When we reach length
n
, we've found a valid string - add it to our result - Backtrack by removing the last character to explore other possibilities
The beauty of this approach is that we prune invalid paths early. If placing a '0'
would create consecutive zeros, we don't explore that branch at all, making the algorithm efficient despite exploring all possibilities.
Think of it like navigating a decision tree where each level represents a position in the string, and each branch represents choosing either '0'
or '1'
. We traverse this tree depth-first, collecting all paths that lead to valid strings of length n
.
Learn more about Backtracking patterns.
Solution Approach
The solution implements a depth-first search (DFS) with backtracking to generate all valid binary strings. Let's walk through the implementation:
Core Algorithm Structure:
The solution uses a recursive DFS function dfs(i)
where i
represents the current position we're building in the string. We maintain a temporary list t
to build the current string candidate and an answer list ans
to collect all valid strings.
Building Process:
-
Base Case: When
i >= n
, we've successfully built a string of lengthn
. We join the characters int
to form a string and add it toans
. -
Recursive Case: For each position
i
, we try both possible values (j = 0
andj = 1
):- If
j = 0
: We can only place a'0'
if either:- It's the first position (
i == 0
), OR - The previous character is
'1'
(t[i - 1] == "1"
)
- It's the first position (
- If
j = 1
: We can always place a'1'
without any checks
- If
Backtracking Mechanism:
After exploring a choice (adding a character to t
and recursing), we backtrack by removing the last character (t.pop()
). This allows us to explore the alternative choice at the same position.
Implementation Details:
def dfs(i: int):
if i >= n:
ans.append("".join(t)) # Found a valid string
return
for j in range(2): # Try both 0 and 1
if (j == 0 and (i == 0 or t[i - 1] == "1")) or j == 1:
t.append(str(j)) # Make choice
dfs(i + 1) # Explore further
t.pop() # Backtrack
The condition (j == 0 and (i == 0 or t[i - 1] == "1")) or j == 1
ensures we only place valid characters:
- Place
'0'
only if it's the first character or preceded by'1'
- Always allow placing
'1'
Time and Space Complexity:
- Time:
O(2^n)
in the worst case, as we might generate up to2^n
valid strings - Space:
O(n)
for the recursion stack and temporary string building
This approach efficiently generates all valid strings by pruning invalid branches early in the recursion tree.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through generating all valid binary strings for n = 3
.
Initial Setup:
n = 3
t = []
(temporary string builder)ans = []
(result list)
DFS Traversal:
Starting at position i = 0
:
Path 1: Start with '1'
i = 0
: Add '1' →t = ['1']
i = 1
: Previous is '1', can add '0' or '1'- Path 1.1: Add '0' →
t = ['1', '0']
i = 2
: Previous is '0', can only add '1'- Add '1' →
t = ['1', '0', '1']
i = 3
: Base case reached, add "101" toans
- Backtrack →
t = ['1', '0']
- Add '1' →
- Backtrack →
t = ['1']
- Path 1.2: Add '1' →
t = ['1', '1']
i = 2
: Previous is '1', can add '0' or '1'- Path 1.2.1: Add '0' →
t = ['1', '1', '0']
i = 3
: Base case reached, add "110" toans
- Backtrack →
t = ['1', '1']
- Path 1.2.2: Add '1' →
t = ['1', '1', '1']
i = 3
: Base case reached, add "111" toans
- Backtrack →
t = ['1', '1']
- Path 1.2.1: Add '0' →
- Backtrack →
t = ['1']
- Path 1.1: Add '0' →
- Backtrack →
t = []
Path 2: Start with '0'
i = 0
: Add '0' (allowed as first character) →t = ['0']
i = 1
: Previous is '0', can only add '1'- Add '1' →
t = ['0', '1']
i = 2
: Previous is '1', can add '0' or '1'- Path 2.1: Add '0' →
t = ['0', '1', '0']
i = 3
: Base case reached, add "010" toans
- Backtrack →
t = ['0', '1']
- Path 2.2: Add '1' →
t = ['0', '1', '1']
i = 3
: Base case reached, add "011" toans
- Backtrack →
t = ['0', '1']
- Path 2.1: Add '0' →
- Backtrack →
t = ['0']
- Add '1' →
- Backtrack →
t = []
Final Result:
ans = ["101", "110", "111", "010", "011"]
All 5 strings satisfy the constraint of having no consecutive zeros. Notice how the algorithm automatically avoided generating invalid strings like "100" or "001" by checking the previous character before placing a '0'.
Solution Implementation
1class Solution:
2 def validStrings(self, n: int) -> List[str]:
3 """
4 Generate all valid binary strings of length n where no two consecutive 0s are allowed.
5
6 Args:
7 n: The length of binary strings to generate
8
9 Returns:
10 List of all valid binary strings
11 """
12 def backtrack(position: int) -> None:
13 """
14 Recursively build valid binary strings using backtracking.
15
16 Args:
17 position: Current position in the string being built
18 """
19 # Base case: if we've built a string of length n, add it to results
20 if position >= n:
21 result.append("".join(current_string))
22 return
23
24 # Try adding each binary digit (0 or 1) at current position
25 for digit in range(2):
26 # Check if we can add this digit:
27 # - We can add '0' only if this is the first position or previous digit was '1'
28 # - We can always add '1'
29 can_add_zero = (digit == 0 and (position == 0 or current_string[position - 1] == "1"))
30 can_add_one = (digit == 1)
31
32 if can_add_zero or can_add_one:
33 # Add the digit to our current string
34 current_string.append(str(digit))
35
36 # Recursively build the rest of the string
37 backtrack(position + 1)
38
39 # Backtrack: remove the digit to try other possibilities
40 current_string.pop()
41
42 # Initialize result list and temporary string builder
43 result = []
44 current_string = []
45
46 # Start the backtracking process from position 0
47 backtrack(0)
48
49 return result
50
1class Solution {
2 // List to store all valid binary strings
3 private List<String> result = new ArrayList<>();
4 // StringBuilder to build current string during DFS
5 private StringBuilder currentString = new StringBuilder();
6 // Length of the binary string to generate
7 private int stringLength;
8
9 /**
10 * Generates all valid binary strings of length n where no two adjacent characters are '0'
11 * @param n the length of binary strings to generate
12 * @return list of all valid binary strings
13 */
14 public List<String> validStrings(int n) {
15 this.stringLength = n;
16 dfs(0);
17 return result;
18 }
19
20 /**
21 * Depth-first search to generate valid binary strings
22 * @param currentPosition the current position in the string being built
23 */
24 private void dfs(int currentPosition) {
25 // Base case: if we've built a complete string of length n
26 if (currentPosition >= stringLength) {
27 result.add(currentString.toString());
28 return;
29 }
30
31 // Try both binary digits (0 and 1)
32 for (int digit = 0; digit < 2; ++digit) {
33 // Check if current digit can be placed at this position
34 boolean canPlaceDigit = false;
35
36 if (digit == 0) {
37 // '0' can only be placed if this is the first position
38 // or the previous character is '1'
39 if (currentPosition == 0 || currentString.charAt(currentPosition - 1) == '1') {
40 canPlaceDigit = true;
41 }
42 } else {
43 // '1' can always be placed
44 canPlaceDigit = true;
45 }
46
47 if (canPlaceDigit) {
48 // Add the digit to current string
49 currentString.append(digit);
50 // Recursively build the rest of the string
51 dfs(currentPosition + 1);
52 // Backtrack: remove the last added digit
53 currentString.deleteCharAt(currentString.length() - 1);
54 }
55 }
56 }
57}
58
1class Solution {
2public:
3 vector<string> validStrings(int n) {
4 vector<string> result;
5 string currentString;
6
7 // Recursive DFS function to generate all valid binary strings
8 auto dfs = [&](this auto&& dfs, int position) {
9 // Base case: if we've built a string of length n, add it to result
10 if (position >= n) {
11 result.emplace_back(currentString);
12 return;
13 }
14
15 // Try adding '0' or '1' at current position
16 for (int digit = 0; digit < 2; ++digit) {
17 // Check if current digit can be placed:
18 // - '0' can only be placed if this is the first position OR previous digit is '1'
19 // - '1' can always be placed
20 bool canPlaceZero = (digit == 0 && (position == 0 || currentString[position - 1] == '1'));
21 bool canPlaceOne = (digit == 1);
22
23 if (canPlaceZero || canPlaceOne) {
24 // Add current digit to string
25 currentString.push_back('0' + digit);
26
27 // Recursively build the rest of the string
28 dfs(position + 1);
29
30 // Backtrack: remove the digit we just added
31 currentString.pop_back();
32 }
33 }
34 };
35
36 // Start DFS from position 0
37 dfs(0);
38 return result;
39 }
40};
41
1/**
2 * Generates all valid binary strings of length n where no two consecutive zeros appear
3 * @param n - The length of binary strings to generate
4 * @returns Array of all valid binary strings
5 */
6function validStrings(n: number): string[] {
7 // Array to store all valid binary strings
8 const result: string[] = [];
9
10 // Temporary array to build current binary string during DFS
11 const currentString: string[] = [];
12
13 /**
14 * Depth-first search to generate valid binary strings
15 * @param currentIndex - Current position in the string being built
16 */
17 const dfs = (currentIndex: number): void => {
18 // Base case: if we've built a string of length n, add it to results
19 if (currentIndex >= n) {
20 result.push(currentString.join(''));
21 return;
22 }
23
24 // Try adding '0' or '1' at current position
25 for (let digit = 0; digit < 2; digit++) {
26 // Add '0' only if it's the first position or previous digit is '1'
27 // Add '1' without any restrictions
28 const canAddZero = digit === 0 && (currentIndex === 0 || currentString[currentIndex - 1] === '1');
29 const isOne = digit === 1;
30
31 if (canAddZero || isOne) {
32 // Add current digit to the string
33 currentString.push(digit.toString());
34
35 // Recursively build the rest of the string
36 dfs(currentIndex + 1);
37
38 // Backtrack: remove the digit to try other possibilities
39 currentString.pop();
40 }
41 }
42 };
43
44 // Start DFS from index 0
45 dfs(0);
46
47 return result;
48}
49
Time and Space Complexity
Time Complexity: O(n × 2^n)
The algorithm uses depth-first search to generate all valid binary strings of length n
. At each position, we have at most 2 choices (0 or 1), but the choice of 0 is restricted when the previous character is 0. In the worst case, we generate approximately 2^n
valid strings. For each valid string, we need O(n)
time to join the characters and append to the result. Therefore, the total time complexity is O(n × 2^n)
.
Space Complexity: O(n)
The space complexity analysis excludes the space used for the answer array. The recursive call stack can go up to depth n
, and the temporary list t
stores at most n
characters at any point. Both contribute O(n)
space. Therefore, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Validation Logic for Consecutive Zeros
Pitfall: A common mistake is incorrectly checking for consecutive zeros by either:
- Forgetting to handle the edge case when
position == 0
(first character can be '0') - Using wrong index access like
current_string[position]
instead ofcurrent_string[position - 1]
- Checking the wrong condition, such as allowing '0' when the previous character is also '0'
Incorrect Example:
# Wrong: This allows consecutive zeros if digit == 0 and current_string[-1] == "0": # Incorrect logic current_string.append("0") # Wrong: Index out of bounds when position == 0 if digit == 0 and current_string[position - 1] == "1": # Crashes when position == 0 current_string.append("0")
Solution: Always check if it's the first position before accessing previous elements:
if digit == 0: if position == 0 or current_string[position - 1] == "1": current_string.append("0") backtrack(position + 1) current_string.pop()
2. Forgetting to Backtrack
Pitfall: Failing to remove the added character after recursion, which causes the current_string
to grow incorrectly and produce wrong results.
Incorrect Example:
current_string.append(str(digit))
backtrack(position + 1)
# Missing: current_string.pop()
Solution: Always pair append with pop in backtracking:
current_string.append(str(digit))
backtrack(position + 1)
current_string.pop() # Essential for backtracking
3. String Concatenation Instead of List Building
Pitfall: Using string concatenation in recursion, which creates new string objects at each step and significantly degrades performance.
Inefficient Example:
def backtrack(position: int, current: str) -> None:
if position >= n:
result.append(current)
return
# Inefficient: creates new string objects
backtrack(position + 1, current + "0")
backtrack(position + 1, current + "1")
Solution: Use a list for building strings and join only when complete:
current_string = [] # Use list
current_string.append(str(digit)) # O(1) operation
# ... recursion ...
result.append("".join(current_string)) # Join only at the end
4. Off-by-One Errors in Base Case
Pitfall: Using incorrect comparison in the base case, such as position > n
instead of position >= n
, which either generates strings of wrong length or causes infinite recursion.
Incorrect Example:
if position == n - 1: # Wrong: stops too early result.append("".join(current_string)) return
Solution: Use the correct boundary check:
if position >= n: # Correct: we've filled all n positions (0 to n-1) result.append("".join(current_string)) return
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
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!