Jc-alt logo
jc
data structures and algorithms

LeetCode: Trees I DFS

LeetCode: Trees I DFS
43 min read
#data structures and algorithms
Table Of Contents

Tree Reroot Intro

What is a Tree

Trees are hierarchical data structures representing relationships between entities, often in a parent-child format.

94. Binary Tree Inorder Traversal ::3:: - Easy

Topics: Stack, Tree, Depth First Search, Binary Tree

Intro

Given the root of a binary tree, return the preorder traversal of its nodes' values. Follow up: Recursive solution is trivial, could you do it iteratively?

Example InputOutput
root = [1,null,2,3][1,2,3]
root = [1,2,3,4,5,null,8,null,null,6,7,9][1,2,4,5,6,7,3,8,9]
root = [][]
root = [1][1]

Constraints:

The number of nodes in the tree is in the range [0, 100]

-100 ≤ Node.val ≤ 100

Abstraction

Traverse a binary tree by Inorder.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Recursive In Order Traversal - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
                
        # Inorder Traversal: (left -> root -> right)
        # Recursion stack will serve as queue to explore the most recent node
        #   - In order requires to explore left first, recurse as far left as possible
        #   - Finished exploring left, 'Visit' node
        #   - Step into right node
        #   - Repeat
        
        res = []
        
        def dfs(node):

            # Base case: 
            # reached a leaf's child 'None'
            if not node:
                return
            
            # In order requires to explore left first, 
            # so push as many left nodes to stack for this root
            dfs(node.left)

            # Recurse until exhausted
            
            # Finished exploring left, 'Visit' node
            res.append(node.val)
            
            # Step into right node
            dfs(node.right)

            # Repeat
        
        # Start DFS from root
        dfs(root)
        
        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return res

Solution 2: [DFS] Iterative DFS In Order Traversal With Stack - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
                
        # Inorder Traversal: (left -> root -> right) using a Stack
        # Stack will serve as queue to explore the most recent node
        #   - In order requires to explore left first, so push as many left nodes to stack for this root
        #   - Finished exploring left, 'Visit' node
        #   - Step into right node
        #   - Repeat
        
        # Initial Helper Root Node
        # We need a helper root node unlike the other 2 iterative solutions
        # because Inorder requires us to go left first,
        # which requires a node, but since we can't push anything
        # to the stack without modifying the Inorder order,
        # we need a helper
        # sc: O(1)
        node = root
        
        res = []

        # Iterative Node Stack
        # sc: O(n)
        stack = []

        # Traverse until
        #   - curr node is a a leaf child 'None'
        #   - there are no nodes in stack queue to traverse
        while node or stack:
            
            # In order requires to explore left first, 
            # so push as many left nodes to stack for this root
            while node:
                stack.append(node)
                node = node.left
            
            # Reached a 'None' child leaf, 
            # we've reached as far left as we can go

            # Pop the leftmost node at top of queue
            node = stack.pop()
            
            # 'Visit' node
            res.append(node.val)
            
            # Step into right node
            node = node.right

            # Repeat
        
        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return res

144. Binary Tree Preorder Traversal ::2:: - Easy

Topics: Stack, Tree, Depth First Search, Binary Tree

Intro

Given the root of a binary tree, return the preorder traversal of its nodes' values.

Example InputOutput
root = [1,null,2,3][1,2,3]
root = [1,2,3,4,5,null,8,null,null,6,7,9][1,2,4,5,6,7,3,8,9]
root = [][]
root = [1][1]

Constraints:

The number of nodes in the tree is in the range [0, 100]

-100 ≤ Node.val ≤ 100

Abstraction

Traverse a binary tree by Preorder.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Recursive Pre Order Traversal - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
                
        # Recursive Preorder Traversal: (root -> left -> right)
        # Recursion stack will serve as queue to explore the most recent node
        #   - In order requires to explore root first, 'Visit' node
        #   - Recurse into left node
        #   - start over
        #   - Run out of left nodes, recurse to right node
        #   - start over
        
        res = []
        
        def dfs(node: Optional[TreeNode]):
            
            # Base case: 
            # reached a leaf's child 'None'
            if not node:
                return
            
            # In order requires to explore root first, 'Visit' node
            res.append(node.val)
            
            # Recurse into left node
            dfs(node.left)

            # Recurse until exhausted
            
            # Finished exploring left, now explore right
            dfs(node.right)

            # Repeat
        
        # Start DFS from root
        dfs(root)
        
        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return res

Solution 2: [DFS] Iterative Pre Order Traversal With Stack - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
                
        # Iterative Preorder Traversal: (root -> left -> right) using a Stack
        # Stack will serve as queue to explore the most recent node
        #   - In order requires to explore left first, so push as many left nodes to stack
        #   - Pop from stack, 'Visit' node
        #   -
        #   - Pop node from stack, visit it
        #   - Push right child first, then left child so left is processed first
        
        # Early Exit:
        # tree is empty, return empty array
        if not root:
            return []
        
        res = []

        # Iterative Node Stack
        # sc: O(n)
        stack = [root]
        
        # Traverse until
        #   - There are no nodes in stack queue to traverse
        while stack:

            # In order requires to explore root first, 'Visit' node
            node = stack.pop()            
            res.append(node.val)
            
            # Queue RIGHT before LEFT so LEFT is popped first (LIFO)

            # Eventually, we will repeat on right
            if node.right:
                stack.append(node.right)

            # Eventually, we will iterate until exhausted
            if node.left:
                stack.append(node.left)
        
        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return res

145. Binary Tree Postorder Traversal ::2:: - Easy

Topics: Stack, Tree, Depth First Search, Binary Tree

Intro

Given the root of a binary tree, return the postorder traversal of its nodes' values. Follow up: Recursive solution is trivial, could you do it iteratively?

Example InputOutput
root = [1,null,2,3][3,2,1]
root = [1,2,3,4,5,null,8,null,null,6,7,9][4,6,7,5,2,9,8,3,1]
root = [][]
root = [1][1]

Constraints:

The number of nodes in the tree is in the range [0, 100]

-100 ≤ Node.val ≤ 100

Abstraction

Traverse a binary tree by Postorder.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Recursive Post Order Traversal - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        
        # Postorder Traversal: (left -> right -> root)
        # Recursion stack will serve as queue to explore the most recent node
        #   - Post order requires to explore left first, recurse as far left as possible
        #   - Go up recursive calls, visit node
        #   - start over
        #   - Run out of left nodes, recurse to the right node
        #   - start over
        #   - Run out of right nodes, 'Visit' root node
        #   - start over
        
        res = []
        
        def dfs(node: Optional[TreeNode]):

            # Base case: 
            # reached a leaf's child 'None'
            if not node:
                return
            
            # In order requires to explore left first, 
            # so push as many left nodes to stack for this root
            dfs(node.left)
            
            # Finished exploring left, now explore right
            dfs(node.right)
            
            # 'Visit' node
            res.append(node.val)
        
        # Start DFS from root
        dfs(root)
        
        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return res

Solution 2: [DFS] Iterative Post Order Traversal With Stack - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        
        # Post Traversal: (left -> right -> root) using a Stack
        # Stack will serve as queue to explore the most recent node
        #   - Post order requires to explore left first, so push as many left nodes to stack for this root
        #   - Use a modified preorder: root -> right -> left
        #   - Reverse the result at the end to get correct postorder
        
        # Early Exit:
        # tree is empty, return empty array
        if not root:
            return []
        
        res = []
        
        # Iterative Node Stack
        # sc: O(n)
        stack = [root]
        
        # Traverse until
        #   - There are no nodes in stack queue to traverse
        while stack:

            # 'Visit' node
            node = stack.pop()
            res.append(node.val)
            
            # Postorder: push LEFT first so RIGHT is processed first (LIFO)
            if node.left:
                stack.append(node.left)
            if node.right:
                stack.append(node.right)
        
        # Puts this on stack:       root -> right -> left
        # When popped off stack is: left -> right -> root
        res.reverse()
        
        # overall: tc O(n)
        # overall: sc O(h + n), O(h) for stack, O(n) for result array
        return res

543. Diameter of Binary Tree ::2:: - Easy

Topics: Tree, Depth First Search, Binary Tree

Intro

Given the root of a binary tree, return the length of the diameter of the tree. The diameter of a binary tree is the length of the longest path between any two nodes in a tree. This path may or may not pass through the root. The length of a path between two nodes is represented by the number of edges between them.

Example InputOutput
root = [1,2,3,4,5]3
root = [1,2]1

Constraints:

The number of nodes in the tree is in the range [0, 104]

-100 ≤ Node.val ≤ 100

Abstraction

Find the diameter of a binary tree.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Recursive DFS Post Order Nonlocal CurrMax Pass Up - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        
        # Tree Widths:
        # Compare the max width of each subtree while traversing the tree
        # (in, pre, and post should all work for this problem)
        # while adding +1 for each depth level while traversing

        # Postorder Traversal: (left -> right -> root)
        #   - Explore left: get width of tree
        #   - Explore right: get width of tree
        #   - Process root: connect left and right subtrees via curr node by adding widths
        #   - Compare to global max
        #   - Create length for curr node by grabbing max between left and right

        # Global max 
        maxWidth = 0
        
        def dfs(node):

            # nonlocal says maxWidth refers to the variable in the outer enclosing scope, 
            # don't create a new local one
            # res.append doesn't reassign the variable, 
            # it mutates the object it points to, so the preorder traversal
            # doesn't need nonlocal
            nonlocal maxWidth

            # Base case: 
            # reached a leaf's child 'None', width value of a leaf is 0
            if not node:
                return 0
            
            # Explore left
            leftWidth = dfs(node.left)

            # Explore right
            rightWidth = dfs(node.right)
            
            # Process root: connect left and right subtrees via curr node by adding widths
            checkTreeAtNode = leftWidth + rightWidth

            # Check maxWidth
            # nonlocal avoids this creating a new local variable within inner curr scope
            maxWidth = max(maxWidth, checkTreeAtNode)

            # Pick a side to continuing adding to length
            rootWidth = 1 + max(leftWidth, rightWidth)
            return rootWidth
        
        # Start at root
        dfs(root)

        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return maxWidth
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
DFS TraversalO(n)O(log n) / O(n)Visit every node onceRecursion call stack
Per Node WorkO(1)O(1)maxWidth comparison and additionSingle nonlocal write
OverallO(n)O(log n) / O(n)Visit every node onceBalanced vs skewed trees

Solution 2: [DFS] Recursive DFS Post Order Tuple Pass Up (Tree Max Diameter, Tree Length) - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        
        # Tree Widths:
        # Compare the max width of each subtree while traversing the tree
        # while adding +1 for each depth level while traversing

        # Postorder Traversal: (left -> right -> root)
        #   - Explore left: get width and max diameter of subtree
        #   - Explore right: get width and max diameter of subtree
        #   - Process root: connect left and right subtrees via curr node by adding widths
        #   - Compare connected width to left and right max diameters
        #   - Create length for curr node by grabbing max between left and right

        def dfs(node):

            # Base case:
            # reached a leaf's child 'None', width and max diameter of a leaf is 0
            if not node:
                return (0, 0)
            
            # Explore left
            (leftWidth, leftMaxDiameter) = dfs(node.left)

            # Explore right
            (rightWidth, rightMaxDiameter) = dfs(node.right)
            
            # Process root: connect left and right subtrees via curr node by adding widths
            connectedTree = leftWidth + rightWidth
            
            # Check max diameter against connected width and subtree max diameters
            rootMaxDiameter = max(connectedTree, leftMaxDiameter, rightMaxDiameter)
            
            # Create length for curr node by grabbing max between left and right
            rootWidth = 1 + max(leftWidth, rightWidth)
            
            # Pass curr node width and max diameter up recursion calls
            return (rootWidth, rootMaxDiameter)

        # Start at root
        res = dfs(root)[1]

        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return res
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

110. Balanced Binary Tree ::1:: - Easy

Topics: Tree, Depth First Search, Binary Tree

Intro

Given a binary tree, determine if it is height-balanced. A height-balanced binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one.

Example InputOutput
root = [3,9,20,null,null,15,7]true
root = [1,2,2,3,3,null,null,4,4]false
root = []true

Constraints:

The number of nodes in the tree is in the range [0, 5000]

-104 ≤ Node.val ≤ 104

Abstraction

Determine if a binary tree is height balanced. A height-balanced binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Post Order DFS Recursive Exception Throw - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def isBalanced(self, root: Optional[TreeNode]) -> bool:
       
        # Note:
        # DFS post order: left -> right -> root
        # 1. Process left -> right -> :
        # 2. Process -> root : validate if balanced, raise exception if imbalanced
        # Results: detect imbalance, short-circuit on first imbalance found

        def dfs(node):

            # Base case: 
            # Reached leaf node, height of 0
            if node == None:
                return 0
            
            # Grab left height
            leftHeight = dfs(node.left)

            # Grab right height
            rightHeight = dfs(node.right)
            
            # Check:
            # Unbalanced node has left and right heights that differ by more than 1,
            # if node is unbalanced, raise exception
            if abs(leftHeight - rightHeight) > 1:
                raise ValueError("unbalanced")
            
            # Node is balanced, 
            # grab height and pass up
            rootHeight = 1 + max(leftHeight, rightHeight)
            return rootHeight
        
        # Catch exception
        try:
            dfs(root)
            return True
        except ValueError:
            return False

        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

572. Subtree of Another Tree ::5:: - Easy

Topics: Tree, Depth First Search, String Matching, Binary Tree, Hash Function

Intro

Given the roots of two binary trees root and subRoot, return true if there is a subtree of root with the same structure and node values of subRoot and false otherwise. A subtree of a binary tree tree is a tree that consists of a node in tree and all of this node's descendants. The tree tree could also be considered as a subtree of itself.

Example InputOutput
root = [3,4,5,1,2], subRoot = [4,1,2]true
root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]false

Constraints:

The number of nodes in the root tree is in the range [1, 2000].

The number of nodes in the subRoot tree is in the range [1, 1000].

-104 ≤ root.val ≤ 104

-104 ≤ subRoot.val ≤ 104

Abstraction

Determine binary tree tree2 is a subtree of binary tree tree1.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Pre Order DFS Node By Node Recursive Comparison - Tree/DFS Pre Order Recursive One Sided Top Down

    def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
        
        # Note:
        # DFS pre order: root -> left -> right
        # 1. Process root -> : 
        #    validate subtree matches
        # 2. Process -> left -> right :
        # Result: validate if subtree is subtree of tree

        # Recursive abstraction call to validate if same tree
        def isSameTree(s, t):

            # Pre Order: Validate that root nodes match

            # If both nodes are Leaves, trees match
            if not s and not t:
                return True

            # If only one node is a leaf, trees don't match
            if not s or not t:
                return False

            # If node values don't match, trees don't match
            if s.val != t.val:
                return False
            
            # Nodes match:
            # Check the rest of children to ensure complete match
            leftRes = isSameTree(s.left, t.left)
            rightRes = isSameTree(s.right, t.right)
            completeTreeMatch = leftRes and rightRes
            return completeTreeMatch
        
        # Empty check:
        # Empty subtree always matches
        if not subRoot:
            return True

        # Empty check:
        # Empty root tree never matches non-empty subtree
        if not root:
            return False
        
        # Full Match:
        # Check if trees are a complete match
        if isSameTree(root, subRoot):
            return True
        
        # Subtree Match:
        # Check if trees are a complete match at some inner subtree
        else:
            leftRes = self.isSubtree(root.left, subRoot)
            rightRes = self.isSubtree(root.right, subRoot) 
            innerSubtreeMatch = leftRes or rightRes
            return innerSubtreeMatch

        # overall: tc O(n)
        # overall: sc O(n)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

105. Construct Binary Tree from Pre Order and In Order Traversal ::1:: - Medium

Topics: Array, Hash Table, Depth First Search, Divide and Conquer, Tree, Binary Tree

Intro

Given two integer arrays pre order and in order where pre order is the pre order traversal of a binary tree and inorder is the inorder traversal of the same tree, construct and return the binary tree.

Example InputOutput
preorder = [3,9,20,15,7], inorder = [9,3,15,20,7][3,9,20,null,null,15,7]
preorder = [-1], inorder = [-1][-1]

Constraints:

1 ≤ preorder.length ≤ 3000

inorder.length == preorder.length

-3000 ≤ preorder[i], inorder[i] ≤ 3000

Each value or inorder also appears in preorder.

preorder is guaranteed to be the preorder traversal of the tree.

inorder is guaranteed to be the inorder traversal of the tree.

Abstraction

Create original free given pre and post order traversals.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: DFS Pre Order Recursive In Order Hash Map - Tree/DFS Pre order Traversal

    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        

        def preAndInArrayToTree(left, right):

            # Pre Order:
            # Holds the current root of the current tree we are building 
            nonlocal preIndex

            # Base case: 
            # no elements remaining, return leaf
            if left > right:
                return None

            # Use Pre Order pointer to grab root value
            root_val = preorder[preIndex]
            root = TreeNode(root_val)

            # Use root value, to grab root index relative to In Order array
            root_index = inorder_index_map[root_val]

            # [left ... root ... right]
            # the root index will always split the in order array into left and right subtrees

            # [left ... root - 1, root, root + 1 ... right]
            
            # So the left subtree gets the left section: [left ... root - 1]
            left_subtree_left = left
            left_subtree_right = root_index - 1

            # And the right subtree gets the right section: [root + 1 ... right]
            right_subtree_left = root_index + 1
            right_subtree_right = right

            # Iterate to next root from pre order (root -> left -> right),
            # so we build left and then right subtrees
            preIndex += 1

            # Recurse to assign the left and right subtree range
            root.left = preAndInArrayToTree(left_subtree_left, left_subtree_right)
            root.right = preAndInArrayToTree(right_subtree_left, right_subtree_right)

            # Use Pre Order pointer to grab the next root value
            return root


        # PreRootIndex: 
        #   - root of tree we currently building
        #   - trees will be build in order of pre order (root -> left -> right) 
        
        # Left/Right InOrderTreeBounds: 
        #   - left/right bounds of tree we are currnetly building
        #   - bounds will be in order of in order (left -> root -> right)

        # InOrderHashMap:
        #   - translates root from PreOrder be relative to InOrder, (left -> root -> right)

        # Tree Root Building Tracker:
        preIndex = 0

        # Left/Right Bounds Building Tracker:
        leftTreeBounds = 0 
        rightTreeBounds = len(inorder)-1

        # LookUp for (value -> index) for In Order Array
        inorder_index_map = {}
        for idx, val in enumerate(inorder):
            inorder_index_map[val] = idx

        # Start building tree from root
        fullTree = preAndInArrayToTree(leftTreeBounds, rightTreeBounds)

        # overall: tc O(n)
        # overall: sc O(n)
        return fullTree
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

337. House Robber III ::2:: - Medium

Topics: Dynamic Programming, Tree, Depth First Search, Binary Tree

Intro

The thief has found himself a new place for his thievery again. There is only one entrance to this area, called root. Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that all houses in this place form a binary tree. It will automatically contact the police if two directly-linked houses were broken into on the same night. Given the root of the binary tree, return the maximum amount of money the thief can rob without alerting the police.

Example InputOutput
root = [3,2,3,null,3,null,1]7
root = [3,4,5,1,3,null,1]9

Constraints:

1 ≤ preorder.length ≤ 3000

0 ≤ Node.val ≤ 10^4

Abstraction

Create original free given pre and post order traversals.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Recursive Post Order DP - Tree/DFS Pre order Traversal

    def rob(self, root: Optional[TreeNode]) -> int:
                
        # House Tree Structure:
        # Each house is a node in a binary tree.
        # If we rob a node, we cannot rob its direct children.
        
        # Ex:
        #        5
        #       / \
        #      4   7
        #       \   \
        #        2   8
        
        # Key Idea (Tree DP):
        # At each node, we must decide between:
        #
        #   1. Rob this node
        #      -> Cannot rob left or right child
        #
        #   2. Skip this node
        #      -> We are allowed to either rob or skip children, 
        #         so we grab the max between two options
        #
        # Each node needs to return two states
        #   - rob
        #   - skip
        

        # Post Order traversal: (left -> right -> root)
        # We must process children first before calculating value for parent
        
        def dfs(node):
            
            # Base case: 
            # An empty node contributes nothing, mark its values are 0, 0 for rob/skip
            if not node:
                return (0, 0)
            
            # Postorder traversal
            leftRob, leftSkip = dfs(node.left)
            rightRob, rightSkip = dfs(node.right)
            
            # DP:
            # Need to generate the values for rob/skip for children of this node

            # Rob Node:
            # Grab value for node, skip both children
            robNode = node.val + leftSkip + rightSkip
            
            # Skip Node: 
            # Skip this node, grab max from each child (could be rob/skip)
            skipNode = max(leftRob, leftSkip) + max(rightRob, rightSkip)
            
            # Return both rob/skip DP states for this node
            return (robNode, skipNode)
        
        
        # Grab the rob/skip values for the root node
        (rootRob, rootSkip) = dfs(root)
        
        # overall: tc O(n)
        # overall: sc O(h)
        return max(rootRob, rootSkip)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

1325. Delete Leaves With a Given Value ::2:: - Medium

Topics: Dynamic Programming, Tree, Depth First Search, Binary Tree

Intro

Given a binary tree root and an integer target, delete all the leaf nodes with value target. Note that once you delete a leaf node with value target, if its parent node becomes a leaf node and has the value target, it should also be deleted (you need to continue doing that until you cannot).

Example InputOutput
root = [1,2,3,2,null,2,4], target = 2[1,null,3,null,4]
root = [1,3,3,3,2], target = 3[1,3,null,null,2]
root = [1,2,null,2,null,2], target = 2[1]

Constraints:

The number of nodes in the tree is in the range [1, 3000]

1 ≤ Node.val, target ≤ 1000

Abstraction

The number of nodes in the tree is in the range [1, 3000]

1 ≤ Node.val, target ≤ 1000

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Post Order Recursive Prune Upwards While Empty - Tree/DFS Pre order Traversal

    def removeLeafNodes(self, root: Optional[TreeNode], target: int) -> Optional[TreeNode]:
        
        # We only want remove LEAF nodes with value == target

        # Need to use Post Order (left -> right -> root),
        # as we can't decide if a Target Node is a leaf 
        # until we know if its children have been removed
        
        # Base case:
        # Current node is None, return None
        if not root:
            return None
        
        # Remove target nodes from left and right subtrees
        root.left = self.removeLeafNodes(root.left, target)        
        root.right = self.removeLeafNodes(root.right, target)
        
        # Remove current node if:
        #   - Node is a target value 
        #   - left and right are None, (current node is a leaf)
        if not root.left and not root.right and root.val == target:
            return None
        
        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return root
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

124. Binary Tree Maximum Path Sum ::1:: - Hard

Topics: Dynamic Programming, Tree, Depth First Search, Binary Tree

Intro

A path in a binary tree is a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them. A node can only appear in the sequence at most once. Note that the path does not need to pass through the root. The path sum of a path is the sum of the node's values in the path. Given the root of a binary tree, return the maximum path sum of any non-empty path.

Example InputOutput
root = [1,2,3]6
root = [-10,9,20,null,null,15,7]42

Constraints:

The number of nodes in the tree is in the range [1, 3 * 104]

-1000 ≤ Node.val ≤ 1000

Abstraction

Given a tree, find the path that produces the max sum.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Post Order Negative Path 0 Flattening Reset Separating Positive Subtrees - Tree/DFS Pre order Traversal

    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        
        # Negative Path 0 Flattening Reset Separating Positive Subtrees:
        #  - We have negative values in the tree,
        #    so we treat negative values are reset points for our sum paths.
        #    Clamp to 0 resets the path sum, 
        #    which breaks the tree into sections of only positive sums,
        #    so maxSum becomes the max positive path

        # Ex:
        #        1
        #       / \
        #     -2   5
        #    /  \   \
        #   6    2   8
        #    \
        #     3
        
        # Postorder Traversal (DFS): (left -> right -> root)
        #   - Need to process left and right subtrees 
        #     before to determine where the positive tree paths are

        # Global max path sum across all nodes
        maxSum = float('-inf')

        def dfs(node):
            nonlocal maxSum

            # Base case:
            # reached a leaf's child 'None'
            if not node:
                return 0

            # Clamp negative paths to 0, so we only consider positive paths
            leftGain = max(dfs(node.left), 0)
            rightGain = max(dfs(node.right), 0)

            # Connect left and right paths through current node
            currTree = node.val + leftGain + rightGain

            # Check global max
            maxSum = max(maxSum, currTree)

            # Continue passing up better branch upwards
            currMax = node.val + max(leftGain, rightGain)
            return currMax

        dfs(root)

        # overall: tc O(n)
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return maxSum
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

834. Sum of Distances in Tree ::1:: - Hard

Topics: Dynamic Programming, Tree, Depth First Search, Graph Theory

Intro

There is an undirected connected tree with n nodes labeled from 0 to n - 1 and n - 1 edges. You are given the integer n and the array edges where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the tree. Return an array answer of length n where answer[i] is the sum of the distances between the ith node in the tree and all other nodes.

Example InputOutput
n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]][8,12,6,10,10,10]
n = 1, edges = [][0]
n = 2, edges = [[1,0]][1,1]

Constraints:

1 ≤ n ≤ 3 * 10^4

edges.length == n - 1

edges[i].length == 2

0 ≤ ai, bi < n

ai != bi

The given input represents a valid tree

Abstraction

something!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] ReRooting DP On Trees - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]:
        
        # Re-rooting DP:
        # common idea in competitive programming,
        # somewhat easy to spot as such problems typically ask something like
        # "find some value for each root"

        # Re-rooting DP (Dynamic Programming On Trees):
        # Challenge is the problem asks for answer at EVERY node,
        # pretending as if that node is the root, so:
        #   1. pick a node as root
        #   2. run a DFS to compute its answer
        #   3. repeat for the rest n nodes
        # Which would lead to O(n^2)

        # Re-rooting DP avoids recomputing everything from scratch and allows O(n)
        # We first compute the answer for one root (node 0),
        # then efficiently "move" the root across each edge

        # Key Point:
        # When moving the root from parent -> child:
        #   - child subtree nodes become 1 step closer
        #   - all other nodes become 1 step further

        # So using the previously computed DP values, we can derive
        # the child's answer in O(1) instead of running another DFS

        # General Pattern:
        #   Pass 1 (Postorder):
        #       - gather information from children -> parent

        #   Pass 2 (Preorder):
        #       - Propagate answers from parent -> children

        # Build undirected adjacency list
        # sc: O(n)
        tree = defaultdict(list)
        for u, v in edges:
            tree[u].append(v)
            tree[v].append(u)


        # Sum of distances from node 0 to all other nodes (postorder result):

        # After Pass 1:
        #   nodeToAllTotal[0] = answer for root node 0
        # After Pass 2:
        #   nodeToAllTotal[i] = answer for root node i
        # ex:
        #   nodeToAllTotal[3] = sum of distances from node 3 to all nodes
        # sc: O(n)
        nodeToAllTotal = [0] * n


        # Subtree sizes rooted starting at node 0 to all children
        
        # Ex:
        #        0
        #       / \
        #      1   2
        #         / \
        #        3   4
        #
        # count[2] = 3  (nodes 2,3,4)
        # count[0] = 5  (entire tree)

        # count[node] = size of nodes's subtree

        # Re-rooting needs subtree sizes because when we move
        # the root from parent -> child, we must know:
        #   - how many nodes get closer?
        #   - how many nodes get farther?

        # sc: O(n)
        subtreeSize = [1] * n

        # Pass 1: Postorder DFS
        # Solve the tree assuming node 0 is the root

        # Postorder Traversal (DFS): (left -> right -> root)
        #   - Process all children first, accumulate subtree sizes and distances
        def dfs_postOrder(node, parent):

            # First process children
            for leftOrRight in tree[node]:

                # Skip original parent to avoid cycles
                if leftOrRight == parent:
                    continue

                # Process child subtree first
                dfs_postOrder(leftOrRight, node)

                # Add children distances to parent
                subtreeSize[node] += subtreeSize[leftOrRight]

                # Sub distances from node = distances from child + all nodes in child subtree
                nodeToAllTotal[node] += nodeToAllTotal[leftOrRight] + subtreeSize[leftOrRight]


        # Pass 2: Preorder DFS
        # Having the answer for node 0, derive the answer for every other node

        # Preorder Traversal (DFS): (root -> left -> right)
        #   - Process root first, propagate re-rooted distances downward to children    
        def dfs_preOrder(node, parent):

            for leftOrRight in tree[node]:

                # Skip parent to avoid cycles
                if leftOrRight == parent:
                    continue

                # Re-root from node -> leftOrRight:
                # leftOrRight subtree (subtreeSize[leftOrRight] nodes) each get 1 closer
                # outside subtree (n - subtreeSize[leftOrRight] nodes) each get 1 further
                nodeToAllTotal[leftOrRight] = 
                    nodeToAllTotal[node] 
                    - subtreeSize[leftOrRight] 
                    + (n - subtreeSize[leftOrRight])

                # Propagate to leftOrRight's children
                dfs_preOrder(leftOrRight, node)


        # Pass 1: postorder — compute subtree sizes and distances from root 0
        dfs_postOrder(0, -1)

        # Pass 2: preorder — re-root and propagate distances to all nodes
        dfs_preOrder(0, -1)

        # overall: tc O(n)
        # overall: sc O(n)
        return nodeToAllTotal

2872. Maximum Number of K-Divisible Components ::1:: - Hard

Topics: Tree, Depth First Search

Intro

There is an undirected tree with n nodes labeled from 0 to n - 1. You are given the integer n and a 2D integer array edges of length n - 1, where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the tree. You are also given a 0-indexed integer array values of length n, where values[i] is the value associated with the ith node, and an integer k. A valid split of the tree is obtained by removing any set of edges, possibly empty, from the tree such that the resulting components all have values that are divisible by k, where the value of a connected component is the sum of the values of its nodes. Return the maximum number of components in any valid split.

Example InputOutput
n = 5, edges = [[0,2],[1,2],[1,3],[2,4]], values = [1,8,1,4,4], k = 62
n = 7, edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]], values = [3,0,6,1,5,2,1], k = 33

Constraints:

1 ≤ n ≤ 3 * 10^4

edges.length == n - 1

edges[i].length == 2

0 ≤ ai, bi < n

values.length == n

0 ≤ values[i] ≤ 10^9

1 ≤ k ≤ 10^9

Sum of values is divisible by k

The input is generated such that edges represents a valid tree

Abstraction

something!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Greedy] [DFS] DFS And BFS Pruning Optimization - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def maxKDivisibleComponents(self, n: int, edges: List[List[int]], values: List[int], k: int) -> int:

        # Postorder Traversal (DFS): (left -> right -> root) + Greedy Tree Cutting
        #   - Process all children first, accumulate subtree sum
        #   - Process root: if subtree sum % k == 0, cut edge and count as component
        #   - Returning 0 to parent simulates cutting the edge (excludes from parent sum)
        #   - Greedy: cut as early (as deep) as possible to maximize components

        # Greedy Proof: cutting at the deepest valid point is always safe
        #
        # claim: if subtree_sum % k == 0, we can safely cut the edge to parent
        #        without affecting whether the parent sum is divisible by k
        #
        # proof:
        #   let parent_sum = parent's accumulated sum before adding this subtree
        #   let subtree_sum % k == 0  (our condition)
        #
        #   without cut: (parent_sum + subtree_sum) % k
        #                = (parent_sum % k + subtree_sum % k) % k
        #                = (parent_sum % k + 0) % k
        #                = parent_sum % k
        #
        #   with cut:    (parent_sum + 0) % k
        #                = parent_sum % k
        #
        #   both cases produce the same remainder at the parent
        #   - cutting never breaks a valid component higher up
        #   - greedy cut is always safe

        # Build undirected adjacency list
        # sc: O(n)
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        # Global component count
        # sc: O(1)
        self.components = 0

        def dfs(node, parent):

            # Base case:
            # start subtree sum with current node's value
            subtree_sum = values[node]

            # Process left -> right ->
            # accumulate subtree sums from all children
            for nei in graph[node]:

                # Skip parent to avoid cycles
                if nei == parent:
                    continue

                subtree_sum += dfs(nei, node)

            # Process -> root: check if subtree is divisible by k
            if subtree_sum % k == 0:

                # Greedy: 
                # Cut immediately at the deepest valid point.
                # Cutting early is always safe, if a subtree sum is divisible by k,
                # including it in the parent can never create more components than cutting it now
                # proof: parent_sum % k is unchanged whether we add (subtree_sum) or (0)
                #        since subtree_sum % k == 0, both contribute the same remainder
                self.components += 1

                # Return 0 to parent to simulate cutting the edge
                return 0

            # Subtree not divisible — pass sum up to parent
            return subtree_sum

        # Start DFS from root
        dfs(0, -1)

        # overall: tc O(n)
        # overall: sc O(n)
        return self.components

427. Construct Quad Tree ::1:: - Medium

Topics: Array, Depth First Search, Divide and Conquer, Tree, Matrixe

Intro

Given a n * n matrix grid of 0's and 1's only. We want to represent grid with a Quad-Tree. Return the root of the Quad-Tree representing grid. A Quad-Tree is a tree data structure in which each internal node has exactly four children. Besides, each node has two attributes:

  • val: True if the node represents a grid of 1's or False if the node represents a grid of 0's. Notice that you can assign the val to True or False when isLeaf is False, and both are accepted in the answer.
  • isLeaf: True if the node is a leaf node on the tree or False if the node has four children. class Node public boolean val; public boolean isLeaf; public Node topLeft; public Node topRight; public Node bottomLeft; public Node bottomRight; We can construct a Quad-Tree from a two-dimensional area using the following steps:
  1. If the current grid has the same value (i.e all 1's or all 0's) set isLeaf True and set val to the value of the grid and set the four children to Null and stop. If the current grid has different values, set isLeaf to False and set val to any value and divide the current grid into four sub-grids as shown in the photo. Recurse for each of the children with the proper sub-grid.
Example InputOutput
grid = [[0,1],[1,0]][[0,1],[1,0],[1,1],[1,1],[1,0]]
look at questionlook at question

Constraints:

n == grid.length == grid[i].length

n == 2^x where 0 ≤ x ≤ 6

Abstraction

This intro section is a thesis, im just going to memorize the solution lol.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] Recursive Divide And Conquer Quad Tree Construction - Tree/DFS Pre order Traversal

    def construct(self, grid: List[List[int]]) -> 'Node':

        # Recursive Quad-Tree Construction (Divide and Conquer)
        #   - Divide grid into 4 equal sub-grids, recurse on each
        #   - Process children first (postorder), then merge at root
        #   - If all 4 children are leaves with the same value, merge into one leaf
        #   - Otherwise create an internal node with 4 children

        # Grid size
        # sc: O(1)
        n = len(grid)

        def dfs(r0: int, c0: int, size: int) -> Node:

            # Base case:
            # 1x1 grid is always a leaf
            if size == 1:
                return Node(val=bool(grid[r0][c0]), isLeaf=True)

            # Divide grid into 4 equal sub-grids
            half = size // 2

            # Process all 4 children first (postorder)
            topLeft     = dfs(r0,        c0,        half)
            topRight    = dfs(r0,        c0 + half, half)
            bottomLeft  = dfs(r0 + half, c0,        half)
            bottomRight = dfs(r0 + half, c0 + half, half)

            # Process -> root: check if all children are leaves with same value
            all_leaves    = all(child.isLeaf for child in [topLeft, topRight, bottomLeft, bottomRight])
            all_same_val  = topLeft.val == topRight.val == bottomLeft.val == bottomRight.val

            # Merge into single leaf — uniform subgrid
            if all_leaves and all_same_val:
                return Node(val=topLeft.val, isLeaf=True)

            # Cannot merge — create internal node with 4 children
            return Node(
                val=True,
                isLeaf=False,
                topLeft=topLeft,
                topRight=topRight,
                bottomLeft=bottomLeft,
                bottomRight=bottomRight
            )

        # Start recursive construction from full grid
        dfs(0, 0, n)

        # overall: tc O(n^2)
        # overall: sc O(log n)
        return dfs(0, 0, n)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks