Problem overview
The method GenerateParentheses receives an integer n, the number of pairs of parentheses, and must return every well-formed string that uses exactly n pairs, as a List<string>. A string is well-formed when every ( is closed by a matching ) that comes later, so no prefix ever contains more closing parentheses than opening ones, and the totals match at the end.
For n = 3 the answer is "((()))", "(()())", "(())()", "()(())" and "()()()". For n = 1 it is just "()". The lesson's limit is 1 <= n <= 8.
Approach: build the string one character at a time
A string with n pairs has length 2 * n, and each position holds either ( or ). Trying all 2^(2n) combinations and testing each would waste almost all the work. The better idea is to build the string from left to right and abandon a prefix as soon as it can no longer become a well-formed string. That is backtracking with pruning.
The recursion carries its whole state in its parameters: the string built so far, the target length, how many ( it holds (open) and how many ) (closed). Two rules throw a prefix away:
- Too many closing parentheses. If
closedexceedsopen, the prefix has a)with nothing to close, and appending more characters can't fix that. - Too many opening parentheses. Every unclosed
(needs a)later. Ifopen - closedis larger than the number of positions left, there isn't enough room to close them all.
A string that reaches the full length without being pruned is well-formed, so it goes into the result. Otherwise, the recursion branches twice: one call appends ( and the other appends ).
Take n = 1, so the target length is 2. The empty string branches into "(" and ")". The prefix ")" has one closing parenthesis and no opening one, so it is pruned. The prefix "(" branches into "((", which is pruned because the two openings can't both be closed within the length, and "()", which reaches the full length and is stored.
Step by step
- Add a field of type
List<string>namedsolutionsaboveGenerateParentheses, so the recursive helper can add results without passing a list through every call. - Reset it on every call.
GenerateParenthesesassigns a fresh empty list first, so results from an earlier call can't leak into this one. - Start the search with an empty string, a target length of
2 * nand both counters at 0. - Return
solutionswhen the search is done. - Declare
Generate. It is apublic voidhelper with the parameterscurrent,n,openandclosed. Inside it,nis the full string length, twice the number of pairs. - Prune closings. Return when
open < closed. - Prune openings. Return when
open > closed + (n - current.Length + 1), which is the check that too many parentheses are still waiting to be closed. - Store complete strings. When
current.Length == n, add the string tosolutionsand return. A full-length string that survived both prunes is balanced. - Branch on
(. Recurse with a(appended andopenincreased by one. - Branch on
). Recurse with a)appended andclosedincreased by one.
The solution in C#
class Solution {
public void Generate(string current, int n, int open, int closed) {
if (open < closed) {
return;
}
if (open > closed + (n - current.Length + 1)) {
return;
}
if (current.Length == n) {
solutions.Add(current);
return;
}
Generate(current + '(', n, open + 1, closed);
Generate(current + ')', n, open, closed + 1);
}
List<string> solutions;
public List<string> GenerateParentheses(int n) {
solutions = new();
Generate("", 2 * n, 0, 0);
return solutions;
}
}
The C# details worth a closer look:
- Strings are immutable.
current + '('doesn't modifycurrent; it builds a new string. Each recursive call therefore receives its own prefix, and nothing needs to be undone after the call returns. That is why this backtracking has no "remove the last character" step. - String plus char. When one operand is a
string,+appends the character as text:"(" + '('is"((". Without a string operand it behaves differently:'a' + 'b'adds the character codes and gives the integer 195. Starting from""keeps every+a string concatenation. - Target-typed
new(). Insolutions = new();the compiler takes the type from the field, which isList<string>. It is shorthand fornew List<string>()and is available in current C#. Lengthis a property. Strings useLength, without parentheses, while aListusesCount.- Why the
+ 1doesn't change the result. The differenceopen - closedand the number of remaining positions always have the same parity, since the total target length is even. The surplus can therefore never be exactly one more than the remaining positions, and the check behaves likeopen - closed > remaining.
Complexity
Time: proportional to the number of results. The number of well-formed strings with n pairs is the Catalan number: 1, 2, 5, 14, 42, 132, 429 and 1430 for n from 1 to 8. Both prunes together mean that every prefix the search keeps can still be completed into a well-formed string, so the recursion creates few wasted branches. Each result costs a string of length 2n to build, which gives roughly O(n * C(n)) work, where C(n) is the Catalan number. That growth is close to 4^n, so n has to stay small, and the limit of 8 does.
Space: O(n) besides the output. The recursion is 2n calls deep, and each level holds one prefix of up to 2n characters.
Edge cases and common mistakes
- Order of the results. Trying
(before)gives the strings in the order shown in the example, starting with"((()))"and ending with"()()()". - Confusing
nin the helper. InGenerate,nis the length2 * n, not the number of pairs. Passing the number of pairs would produce strings that are too short. - Not resetting
solutions. If the field is created only once, a second call returns the strings from the first call as well. - Checking only at the end. Generating all strings and filtering them still gives correct results, but visits
2^(2n)strings. Pruning cuts almost all of them. - Allowing a closing parenthesis too early. A string such as
"())("has equal counts but is not well-formed. Theopen < closedcheck applies at every prefix, which is what rules it out. - Using
charaddition by accident. Writing a character-only expression such as'(' + ')'yields an integer, not a string.
Testing
n = 1gives"()".n = 2gives"(())"and"()()".n = 3gives the five strings listed above.n = 8gives 1430 strings.