3211. Generate Binary Strings Without Adjacent Zeros
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 be1
to ensure the substring formed by this and the previous bit contains at least one1
. - 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:
-
Recursive Function (
dfs
): We define a recursive helper functiondfs(i)
wherei
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 lengthn
. -
Base Case: If
i
equalsn
, we have a complete binary string of the desired length. At this point, we join the listt
(holding the current string) into a string and add it to the listans
. -
Exploring Each Bit:
- We consider both
0
and1
at each positioni
. - If the current bit
j
is0
, it can only be placed if it follows a1
. This check is vital since a0
needs to satisfy the valid substring condition together with its predecessor. - If the bit
j
is1
, it is always valid to place, as any substring ending with1
meets the criteria.
- We consider both
-
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. -
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 lengthn
.
- A list
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 EvaluatorExample 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:
-
Initialization:
- We start with an empty list
t
to build our string and an empty listans
to store all valid strings.
- We start with an empty list
-
Recursive Exploration:
- We begin by calling
dfs(0)
to start filling the string from index 0.
- We begin by calling
-
Index 0 Exploration:
- Option 1: Place "1" at index 0.
- Update
t
to['1']
. - Call
dfs(1)
to move to the next index.
- Update
- Option 2: Skip "0" at index 0 since the rule requires the preceding bit to be "1".
- Option 1: Place "1" at index 0.
-
Index 1 Exploration with
t = ['1']
:- Option 1: Place "0" at index 1 (valid after "1").
- Update
t
to['1', '0']
. - Call
dfs(2)
.
- Update
- Option 2: Place "1" at index 1.
- Update
t
to['1', '1']
. - Call
dfs(2)
.
- Update
- Option 1: Place "0" at index 1 (valid after "1").
-
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"
toans
.
- Only Option: Place "1" at index 2 to make
- 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"
toans
.
- Option 1: Place "0" at index 2 for a valid string, resulting in
- With
-
Conclusion:
- After backtracking and exploring all possibilities, the
ans
list contains:["101", "110", "111"]
.
- After backtracking and exploring all possibilities, the
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.
Which of these properties could exist for a graph but not a tree?
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!