Generate Binary Strings Without Adjacent Zeros
MedPrompt
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.
Example 1:
Input: n = 3
Output: ["010","011","101","110","111"]
Explanation:
The valid strings of length 3 are: "010", "011", "101", "110", and "111".
Example 2:
Input: n = 1
Output: ["0","1"]
Explanation:
The valid strings of length 1 are: "0" and "1".
Constraints:
1 <= n <= 18
Approaches
2 approaches with complexity analysis and trade-offs.
This straightforward approach generates every possible binary string of length n and then filters them based on the given validity condition. A string is considered valid if it does not contain the substring "00". While simple to conceive, this method is inefficient because it processes many strings that will ultimately be discarded.
Algorithm
- Initialize an empty list,
result, to store the valid strings. - Iterate through all integers from
0to2^n - 1. Each integer represents a unique binary string of lengthn. - For each integer, convert it to its
n-bit binary string representation. This can be done by checking each bit of the integer. - Check if the generated binary string contains the forbidden substring "00".
- If the string is valid (does not contain "00"), add it to the
resultlist. - After checking all
2^npossibilities, return theresultlist.
Walkthrough
The core of this method is to map the integers in the range [0, 2^n - 1] to all possible binary strings of length n. For example, if n=3, the integer 5 (binary 101) maps to the string "101". We can implement this by iterating from i = 0 to (1 << n) - 1. Inside the loop, we build a StringBuilder of length n. We iterate from j = n-1 down to 0 to check the bits of i from most significant to least significant. The expression (i & (1 << j)) != 0 checks if the j-th bit of i is set. If it is, we append '1'; otherwise, we append '0'. Once the n-character string is formed, we use a built-in contains method to check for the "00" substring. If it's not found, the string is added to our list of results.
class Solution { public List<String> validStrings(int n) { List<String> result = new ArrayList<>(); // Total number of binary strings of length n is 2^n int limit = 1 << n; for (int i = 0; i < limit; i++) { StringBuilder sb = new StringBuilder(); for (int j = n - 1; j >= 0; j--) { // Check if the j-th bit is 1 or 0 if ((i & (1 << j)) != 0) { sb.append('1'); } else { sb.append('0'); } } String currentString = sb.toString(); // Check for "00" substring if (!currentString.contains("00")) { result.add(currentString); } } return result; }}Complexity
Time
O(n * 2^n). The outer loop runs `2^n` times. Inside the loop, building the string takes `O(n)` and checking `contains("00")` takes `O(n)`. Thus, the total time complexity is `O(n * 2^n)`.
Space
O(n * F_{n+2}). The space required to store the output list dominates. There are `F_{n+2}` (Fibonacci number) valid strings of length `n`, and each has length `n`. The auxiliary space used within the loop is `O(n)` for the `StringBuilder`.
Trade-offs
Pros
Conceptually simple and easy to implement without recursion.
Cons
Highly inefficient as it generates and processes a large number of invalid strings, especially for larger
n.The time complexity grows exponentially with
nat a faster rate (2^n) than the optimal solution.
A more optimized approach is to use backtracking to construct the valid strings directly. This method avoids generating invalid strings by making decisions at each step that maintain the validity constraint. We build the string character by character, and at each position, we only add a '0' or '1' if it doesn't violate the rule of having no adjacent zeros.
Algorithm
- Define a recursive function, let's call it
backtrack, that takes the current string being built (e.g., as aStringBuilder) as a parameter. - Base Case: If the length of the current string equals
n, it's a complete valid string. Add it to the results list and return. - Recursive Step: Based on the last character of the current string:
- If the last character is '1', we can append either '0' or '1'. Make a recursive call for each possibility, backtracking after each call.
- If the last character is '0', we can only append '1'. Make a recursive call and backtrack.
- The process is initiated by making separate calls for initial strings "0" and "1".
Walkthrough
This approach uses a recursive depth-first search to explore the space of valid binary strings. We define a helper function, backtrack(current, n, result), which takes the current prefix current (as a StringBuilder), the target length n, and the list of result strings.
The recursion is initiated by making two separate calls for the starting characters: one for "0" and one for "1". This handles the strings of length 1 and seeds the recursion for longer strings.
Inside the backtrack function:
- Base Case: The recursion stops when
current.length()equalsn. At this point, we have constructed a complete, valid string, which is added to theresultlist. - Recursive Step: The function checks the last character of the
currentstring to decide the next valid moves:- If the last character is '1', we have two choices for the next character: '0' or '1'. The function recursively calls itself for both cases. First, it appends '0', makes the recursive call, and then backtracks by removing the '0'. Then, it does the same for '1'.
- If the last character is '0', the only valid next character is '1' to avoid a "00" sequence. The function appends '1', makes the recursive call, and then backtracks.
Using a StringBuilder and deleting the last character for backtracking is crucial for efficiency, as it avoids creating numerous intermediate string objects.
class Solution { public List<String> validStrings(int n) { List<String> result = new ArrayList<>(); if (n <= 0) return result; // Start the recursion for strings beginning with "0" and "1" backtrack(new StringBuilder("0"), n, result); backtrack(new StringBuilder("1"), n, result); return result; } private void backtrack(StringBuilder current, int n, List<String> result) { // Base case: a valid string of length n is found if (current.length() == n) { result.add(current.toString()); return; } char lastChar = current.charAt(current.length() - 1); // If the last character was '0', the next must be '1' if (lastChar == '0') { current.append('1'); backtrack(current, n, result); current.deleteCharAt(current.length() - 1); // Backtrack } // If the last character was '1', the next can be '0' or '1' else { // lastChar == '1' // Option 1: Append '0' current.append('0'); backtrack(current, n, result); current.deleteCharAt(current.length() - 1); // Backtrack // Option 2: Append '1' current.append('1'); backtrack(current, n, result); current.deleteCharAt(current.length() - 1); // Backtrack } }}Complexity
Time
O(n * F_{n+2}). The number of valid strings is given by the Fibonacci number `F_{n+2}`. The recursion tree has this many leaf nodes. Generating each string of length `n` involves a path of length `n` in the tree. The total work is proportional to the total number of nodes in the tree, which is on the order of `F_{n+2}`, and converting the `StringBuilder` to a `String` at each leaf node takes `O(n)`.
Space
O(n * F_{n+2}). The primary space cost is for storing the `result` list. The recursion depth is `n`, so the call stack and the `StringBuilder` use `O(n)` auxiliary space.
Trade-offs
Pros
Highly efficient and optimal, as it only explores the search space of valid strings.
Scales well within the given constraints (
n <= 18).
Cons
The logic is slightly more complex than the brute-force method due to recursion and backtracking.
Solutions
Solution
class Solution { private List < String > ans = new ArrayList <>(); private StringBuilder t = new StringBuilder (); private int n ; public List < String > validStrings ( int n ) { this . n = n ; dfs ( 0 ); return ans ; } private void dfs ( int i ) { if ( i >= n ) { ans . add ( t . toString ()); return ; } for ( int j = 0 ; j < 2 ; ++ j ) { if (( j == 0 && ( i == 0 || t . charAt ( i - 1 ) == '1' )) || j == 1 ) { t . append ( j ); dfs ( i + 1 ); t . deleteCharAt ( t . length () - 1 ); } } } }class Solution { public: vector < string > validStrings ( int n ) { vector < string > ans ; string t ; auto dfs = [ & ]( auto && dfs , int i ) { if ( i >= n ) { ans . emplace_back ( t ); return ; } for ( int j = 0 ; j < 2 ; ++ j ) { if (( j == 0 && ( i == 0 || t [ i - 1 ] == '1' )) || j == 1 ) { t . push_back ( '0' + j ); dfs ( dfs , i + 1 ); t . pop_back (); } } }; dfs ( dfs , 0 ); return ans ; } };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 ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.