LeetCode: Trees I DFS

Table Of Contents
543. Diameter of Binary Tree ::2:: - Easy
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [DFS] Recursive DFS Post Order Nonlocal CurrMax Pass Up - Tree/DFS Post Order Recursive Two Sided Bottom Up
- Solution 2: [DFS] Recursive DFS Post Order Tuple Pass Up (Tree Max Diameter, Tree Length) - Tree/DFS Post Order Recursive Two Sided Bottom Up
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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 resSolution 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 res144. 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 resSolution 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 res145. 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 resSolution 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 res543. 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| DFS Traversal | O(n) | O(log n) / O(n) | Visit every node once | Recursion call stack |
| Per Node Work | O(1) | O(1) | maxWidth comparison and addition | Single nonlocal write |
| Overall | O(n) | O(log n) / O(n) | Visit every node once | Balanced 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 nodeToAllTotal2872. 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 Input | Output |
|---|---|
| n = 5, edges = [[0,2],[1,2],[1,3],[2,4]], values = [1,8,1,4,4], k = 6 | 2 |
| n = 7, edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]], values = [3,0,6,1,5,2,1], k = 3 | 3 |
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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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.components427. 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:
- 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 Input | Output |
|---|---|
| grid = [[0,1],[1,0]] | [[0,1],[1,0],[1,1],[1,1],[1,0]] |
| look at question | look 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|