Problem overview
The method generateParentheses receives an integer n, from 1 to 8, and returns a List[String] with every well-formed string made of n pairs of parentheses. A string is well-formed when every ( is closed by a later ) and no prefix closes more than it has opened.
For n = 3 the answer is the five strings ((())), (()()), (())(), ()(()) and ()()(). For n = 1 it is just ().
Approach: build the string one character at a time
A string with n pairs has 2n characters, each either ( or ). Trying all 4 to the power n combinations and filtering the valid ones wastes most of the effort. Instead, build the string left to right and give up on a prefix as soon as it cannot lead to a valid result. Two facts make that possible.
- Closing too early. If a prefix contains more
)than(, some parenthesis closes something that was never opened, and no later characters can repair that. Stop. - Opening too many. If the prefix has more unclosed
(than there are positions left, it can never be balanced. Stop.
A prefix that passes both checks is extended with ( and with ), two recursive calls. When the length reaches 2n, the string has survived both checks and is guaranteed to be balanced, so it is saved.
Step by step
- Declare a shared buffer. A field
var solutions: ArrayBuffer[String] = nulllets the recursive helper collect results without returning them. - Reset it in
generateParentheseswith a freshArrayBuffer[String](), so strings from earlier calls can't remain. - Start the search with an empty string, the target length
2 * nand both counters at 0. - Return
solutions.toList, because the method promises an immutableList[String]. - Declare
generate(current, n, open, closed)returningUnit. Inside the helper,nis the full length of the string, twice the number of pairs.openandclosedcount the parentheses of each kind incurrent. - First prune: return when
open < closed, because the prefix closed more than it opened. - Second prune: return when
open > closed + (n - current.length + 1). The slots left aren - current.length, and even if all of them are),closedgrows by at most that much. - Complete string: when
current.length == n, append it withsolutions += currentand return. - Extend both ways: recurse with
current + "("andopen + 1, then withcurrent + ")"andclosed + 1.
The solution in Scala
class Solution {
def generate(current: String, n: Int, open: Int, closed: Int): Unit = {
if (open < closed) {
return
}
if (open > closed + (n - current.length + 1)) {
return
}
if (current.length == n) {
solutions += current
return
}
generate(current + "(", n, open + 1, closed)
generate(current + ")", n, open, closed + 1)
}
var solutions: ArrayBuffer[String] = null
def generateParentheses(n: Int): List[String] = {
solutions = ArrayBuffer[String]()
generate("", 2 * n, 0, 0)
solutions.toList
}
}
Scala details:
- The type
ArrayBuffer. It is a growable array fromscala.collection.mutable. A file of your own needsimport scala.collection.mutable.ArrayBuffer. - String building. Strings are immutable, so
current + "("creates a new string and leavescurrentuntouched. That is why each branch of the recursion keeps its own prefix and no undo step is needed. +=on a buffer appends an element.solutions += currentchanges the buffer in place.toListcopies the buffer into an immutableList, the declared return type. It is the last expression, so noreturnis needed.- The field is shared.
solutionsbelongs to theSolutionobject and is replaced at the start of every call togenerateParentheses. - The
+ 1in the second prune. It never changes the outcome. The numbers of(and)in a prefix add up to its length, soopen - closedhas the same parity as the length, and so as the number of slots left (the total length is even). The surplus can therefore never be exactly one more than the slots left.
Complexity
Time. The number of results is the n-th Catalan number: 1, 2, 5, 14, 42, 132, 429 and 1430 for n from 1 to 8. The pruning keeps the search close to that number. For n = 3, generate is called 35 times, against 127 for the full tree of all (/) choices. For n = 8 it is called 10,975 times, against 131,071. Each call builds a string of up to 2n characters, so the work is on the order of the number of results times n, which grows roughly like 4 to the power n divided by a power of n.
Space. The recursion is at most 2n + 1 calls deep. The stored strings take most of the memory, because there are Catalan(n) of them with 2n characters each.
Edge cases and common mistakes
- n = 1. The only result is
(). - Result order. The search tries
(before), so the results come out in alphabetical order with(first, as in then = 3list above. - Counts. The result lengths for n from 1 to 8 are 1, 2, 5, 14, 42, 132, 429 and 1430.
- Not resetting the buffer. If
solutionsisn't replaced at the start of each call, a second call returns its results appended to the first call's. - Confusing
nin the helper. The helper'snis the string length, 2 * pairs. Passing the number of pairs would make the search stop at strings of half the right length. - Checking only at the end. Generating all strings and testing each one at the end is correct but explores far more.
- Counting wrong. Equal numbers of
(and)are not enough:)(has one of each but isn't well-formed. That is why the first prune watches every prefix.
Testing
n = 1givesList("()").n = 3givesList("((()))", "(()())", "(())()", "()(())", "()()()").n = 4gives 14 strings, andn = 8gives 1430.
