2

I am trying to understand the backtracking code below:

enter image description here

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:

        stack = []
        res = []

        def backtrack(openN, closedN):
            if openN == closedN == n:
                res.append("".join(stack))
                return

            if openN < n:
                stack.append("(")
                backtrack(openN + 1, closedN)
                print ("stack openN < n", stack)
                stack.pop()
            if closedN < openN:
                stack.append(")")
                backtrack(openN, closedN + 1)
                print ("stack closedN < openN", stack)
                stack.pop()
                

        backtrack(0, 0)
        return res

I am having a hard time conceptually grasping where the stack.pop() is hitting. For example, in the screen shot below how does the code get from the top yellow highlight to the bottom yellow highlight?

How is the code able to pop off three ")" without needing to append them back?

enter image description here

rudolfovic
  • 3,163
  • 2
  • 14
  • 38
PineNuts0
  • 4,740
  • 21
  • 67
  • 112
  • Add a print statement ***before*** each recursive call to backtrack (as well as after, which you already have) and you'll see what's happening more clearly... – MatBailie Mar 14 '23 at 18:36
  • k, let me try that ... I'm so lost in the sauce atm – PineNuts0 Mar 14 '23 at 18:37
  • Demo with full and indent tracing: https://trinket.io/python3/887ad62e9c – MatBailie Mar 14 '23 at 19:12
  • That `openN == closedN == n` works here is non-obvious to me, and wouldn't do the right thing in other languages. It might be worth writing as `openN == n and closedN == n` as this works for more than 2 pairs. – Sam Mason Mar 14 '23 at 21:20
  • @SamMason The way it's written is the correct pythonic form. Comprehensions don't exist in other languages either, but we don't forgo those for familiarity purposes. With this form, python also allows expressions such as `a < b > c` – MatBailie Mar 15 '23 at 00:28

2 Answers2

1

The code is recursive, it traverses the tree and the traversal is constrained by some invariants:

  • The number of open braces can never exceed n
  • The number of closed braces can never exceed the number of open braces

Therefore, you have up to two potential possibilities to look into at every step:

  • adding an open braces
  • adding a closed brace

In addition to this, after exhausting all valid suffixes for a given prefix all you can do is pop another brace off the stack to check if there are any unaccounted suffixes left for this shorter prefix.

For example, you can start by adding ( and ). Then, you'll consider all possible combinations of n-1 pairs. Then, you'll "go back" and pop off the first ) leaving it open until the end.

This is called backtracking. It's just a fancy term for going back up the tree you are traversing.

I suggest you print the stack every time you add or remove a brace to better understand the stages of the traversal:

def generateParenthesis(n):

    stack = []
    res = []

    def backtrack(openN, closedN):
        if openN == closedN == n:
            res.append("".join(stack))
            return

        if openN < n:
            stack.append("(")
            print ("openN < n; +:", stack)
            backtrack(openN + 1, closedN)
            stack.pop()
            print ("openN < n; -:", stack)
        if closedN < openN:
            stack.append(")")
            print ("closedN < openN, +:", stack)
            backtrack(openN, closedN + 1)
            stack.pop()
            print ("closedN < openN, -:", stack)
    backtrack(0, 0)
    return res

Now let's go through the output:

openN < n; +: ['(']
openN < n; +: ['(', '(']
openN < n; +: ['(', '(', '(']
closedN < openN, +: ['(', '(', '(', ')']
closedN < openN, +: ['(', '(', '(', ')', ')']
closedN < openN, +: ['(', '(', '(', ')', ')', ')']
closedN < openN, -: ['(', '(', '(', ')', ')']
closedN < openN, -: ['(', '(', '(', ')']
closedN < openN, -: ['(', '(', '(']
openN < n; -: ['(', '(']
closedN < openN, +: ['(', '(', ')']
openN < n; +: ['(', '(', ')', '(']
closedN < openN, +: ['(', '(', ')', '(', ')']
closedN < openN, +: ['(', '(', ')', '(', ')', ')']
closedN < openN, -: ['(', '(', ')', '(', ')']
closedN < openN, -: ['(', '(', ')', '(']
openN < n; -: ['(', '(', ')']
closedN < openN, +: ['(', '(', ')', ')']
openN < n; +: ['(', '(', ')', ')', '(']
closedN < openN, +: ['(', '(', ')', ')', '(', ')']
closedN < openN, -: ['(', '(', ')', ')', '(']
openN < n; -: ['(', '(', ')', ')']
closedN < openN, -: ['(', '(', ')']
closedN < openN, -: ['(', '(']
openN < n; -: ['(']
closedN < openN, +: ['(', ')']
openN < n; +: ['(', ')', '(']
openN < n; +: ['(', ')', '(', '(']
closedN < openN, +: ['(', ')', '(', '(', ')']
closedN < openN, +: ['(', ')', '(', '(', ')', ')']
closedN < openN, -: ['(', ')', '(', '(', ')']
closedN < openN, -: ['(', ')', '(', '(']
openN < n; -: ['(', ')', '(']
closedN < openN, +: ['(', ')', '(', ')']
openN < n; +: ['(', ')', '(', ')', '(']
closedN < openN, +: ['(', ')', '(', ')', '(', ')']
closedN < openN, -: ['(', ')', '(', ')', '(']
openN < n; -: ['(', ')', '(', ')']
closedN < openN, -: ['(', ')', '(']
openN < n; -: ['(', ')']
closedN < openN, -: ['(']
openN < n; -: []
['((()))', '(()())', '(())()', '()(())', '()()()']

It's true that when you remove ) for the first time you continue by removing the other two. You must because before trying any other arrangement you have to remove at least one open brace.

If you follow the trace closely, you'll see that you remove as many braces as you need to in order to remove an open brace you have not removed until now.

Hopefully that helps, let me know if I can clarify this further.

rudolfovic
  • 3,163
  • 2
  • 14
  • 38
  • Thank you so much for this. I'm going to examine your outputs carefully and let you know if I have any other points of confusion. – PineNuts0 Mar 14 '23 at 19:12
1

Previous answer is absolutely correct. But I think the point you're missing is that part of the contract of backtrack is that it leaves the stack in precisely the state it found it.

@rudolfovic correctly says what the code does. But implicit is that backtrack can call itself recursively, and when the recursive call returns, the stack is exactly as it was before.

Hence the pop(). Each of the if branches pushes something onto the stack. It must remove that item before returning.

Frank Yellin
  • 9,127
  • 1
  • 12
  • 22
  • It looks like the code pops the stack 3 times every time a satisfactory string is built to append to "res" variable ... is this true? If so, where in the code is it setup to run 3 consecutive pops at a time? This is the part I'm really confused about – PineNuts0 Mar 14 '23 at 18:34
  • I'm not sure why you think it pops the stack 3 times, but even if it does, that's incidental. The important thing is that the code pushes an item on to the stack, calls itself recursively, and then pops that item off the stack. It leaves the stack exactly as it found it. That's what the person who wrote this code intended. The code only works because each caller to `backtrack` knows that it only has to clean up after itself, not the recursive calls. – Frank Yellin Mar 14 '23 at 19:02
  • @PineNuts0 It pops every time it finishes a level of recursion. So, if it pops three times in a row (without appending anything in between pops), it's because three levels of nesting have just finished. The first six nestings/recursions go; first call, `+(`, nest, `+(`, nest, `+(`, nest, `+)`, nest, `+)`, nest, `+)`, nest, add `((()))` to the results, return, then three nested calls also finish, the three `+)`; pop, return, pop, return, pop, return. (Removing the three `+)` => one pop per nested execution that completes), ending up midway through the third nesting, which added the third `+(`. – MatBailie Mar 14 '23 at 21:09
  • @MatBailie this is really helpful. And thank you so much for the trinket demo. Follow-up Q: If it pops every time it finishes a recursion then why doesn't it pop 6x for "((()))"? Doesn't that go through six levels of recursion? Why is it that (in your demo) after "((()))" is appended to res array, the code pops the stack 4x? – PineNuts0 Mar 14 '23 at 23:32
  • @PineNuts0 Because the code says to do something else first; add a closing parenthesis, and then recurse again. The code isn't if-else, it's if-if; both code branches Can happen, depending on the two parameters passed in. Get yourself a debugger set up in vscode of pycharm or whatever you use, and step through the code. And/or read an online tutorial about recursion. – MatBailie Mar 15 '23 at 00:27