LeetCode: Trees I DFS

DFS Intro
What is DFS
Trees are hierarchical data structures representing relationships between entities, often in a parent-child format.
Depth First Search is a way of traversing those trees.
Depth First Search Order Diagrams
Pre Order (Root -> Left -> Right)
4
/ \
2 5
/ \
1 3
DFS visit order: 4 → 2 → 1 → 3 → 5
(go as deep as possible before backtracking)In Order (Left -> Root -> Right)
4
/ \
2 5
/ \
1 3
DFS visit order: 1 → 2 → 3 → 4 → 5
(visit left subtree, then root, then right subtree)Post Order (Left -> Right -> Root)
4
/ \
2 5
/ \
1 3
DFS visit order: 1 → 3 → 2 → 5 → 4
(visit left subtree, then right subtree, then root)DFS Specific Tree Problems
DFS commits to a path and follows it to completion. Good for problems that depend on fully exploring a path before evaluating, such as situations that the left and right subtrees must be processed before the root node.
Backtracking usually needs a single current path for undoing, which is why is uses DFS mostly. BFS has multiple paths at the same time which makes it harder for backtracking.
94. Binary Tree Inorder Traversal ::3:: - Easy
Topics: Tree Traversal, 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 (left -> root -> right).
Pseudocode
Sol 1: Recursive In Order Traversal
1. dfs(node):
a. if not node: return
b. dfs(node.left)
c. res.append(node.val)
d. dfs(node.right)
2. dfs(root)
3. Return res
Sol 2: Iterative DFS In Order Traversal With Stack
1. (node = root)
2. stack = []
3. While node or stack:
a. while node:
stack.append(node)
node = node.left
b. node = stack.pop()
c. res.append(node.val)
d. node = node.right
4. Return res
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: Tree Traversal, 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 (root -> left -> right).
Pseudocode
Sol 1: Recursive Pre Order Traversal
1. dfs(node):
a. if not node: return
b. res.append(node.val)
c. dfs(node.left)
d. dfs(node.right)
2. dfs(root)
3. Return res
Sol 2: Iterative Pre Order Traversal With Stack
1. if not root: return []
2. stack = [root]
3. While stack:
a. node = stack.pop()
b. res.append(node.val)
c. if node.right: stack.append(node.right)
d. if node.left: stack.append(node.left)
4. Return res
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 class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
// 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
vector<int> res;
dfs(root, res);
// overall: tc O(n)
// overall: sc O(log n) for balanced / O(n) for skewed trees
return res;
}
private:
void dfs(TreeNode* node, vector<int>& res) {
// Base case:
// reached a leaf's child 'nullptr'
if (node == nullptr) {
return;
}
// In order requires to explore root first, 'Visit' node
res.push_back(node->val);
// Recurse into left node
dfs(node->left, res);
// Recurse until exhausted
// Finished exploring left, now explore right
dfs(node->right, res);
// Repeat
}
};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 res145. Binary Tree Postorder Traversal ::2:: - Easy
Topics: Tree Traversal, 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 (left -> right -> root).
Pseudocode
Sol 1: Recursive Post Order Traversal
1. dfs(node):
a. if not node: return
b. dfs(node.left)
c. dfs(node.right)
d. res.append(node.val)
2. dfs(root)
3. Return res
Sol 2: Iterative Post Order Traversal With Stack
1. if not root: return []
2. stack = [root]
3. While stack:
a. node = stack.pop()
b. res.append(node.val)
c. if node.left: stack.append(node.left)
d. if node.right: stack.append(node.right)
4. res.reverse()
5. Return res
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 Structure Analysis, 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. At each node, sum the diameter of the left and right subtree by connecting them at the root to compare it to the global max, and continue to pass up the larger of the two sides up to the root.
Pseudocode
Sol 1: Recursive DFS Post Order Nonlocal CurrMax Pass Up
1. (maxWidth = 0)
2. dfs(node):
a. if not node: return 0
b. leftWidth = dfs(node.left)
c. rightWidth = dfs(node.right)
d. checkTreeAtNode = leftWidth + rightWidth
e. maxWidth = max(maxWidth, checkTreeAtNode)
f. Return 1 + max(leftWidth, rightWidth)
3. dfs(root)
4. Return maxWidth
Sol 2: Recursive DFS Post Order Tuple Pass Up (Width, MaxDiameter)
1. dfs(node):
a. if not node: return (0, 0)
b. (leftWidth, leftMaxDiameter) = dfs(node.left)
c. (rightWidth, rightMaxDiameter) = dfs(node.right)
d. connectedTree = leftWidth + rightWidth
e. rootMaxDiameter = max(connectedTree, leftMaxDiameter, rightMaxDiameter)
f. rootWidth = 1 + max(leftWidth, rightWidth)
g. Return (rootWidth, rootMaxDiameter)
2. Return dfs(root)[1]
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 Structure Analysis, 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.
At each node, compare the left and right subtree heights to ensure the difference is never greater than 1, if so throw and error.
Pseudocode
Sol 1: Post Order DFS Recursive Exception Throw
1. dfs(node):
a. if node is None: return 0
b. leftHeight = dfs(node.left)
c. rightHeight = dfs(node.right)
d. if abs(leftHeight - rightHeight) > 1:
raise ValueError
e. Return 1 + max(leftHeight, rightHeight)
2. try:
dfs(root)
Return True
3. except ValueError:
Return False
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 ::2:: - Easy
Topics: Tree Structure Analysis, Tree, Depth First Search, String Matching, Binary Tree, Hash Function, Hashing Data Structure
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 if binary tree 2 is a subtree of binary tree 1.
Traverse both trees simultaneously. Make a hash representation of the tree 1 and their subtrees, and compare the full hash of tree 2 against all possibilities.
Pseudocode
Sol 1: Pre Order DFS Node By Node Recursive Comparison
1. isSameTree(s, t):
a. if s == None and t == None: return True
b. if s == None or t == None: return False
c. if s.val != t.val: return False
d. Return isSameTree(s.left, t.left) and isSameTree(s.right, t.right)
2. if subRoot == None: return True
3. if root == None: return False
4. if isSameTree(root, subRoot): return True
5. Return isSubtree(root.left, subRoot) or isSubtree(root.right, subRoot)
Sol 2: Post Order Merkle Tree Via Subtree Hash Fingerprinting
1. rootHashes = set()
2. merkleHash(node, addToRootHash):
a. if node == None: return "#"
b. leftHash = merkleHash(node.left, addToRootHash)
c. rightHash = merkleHash(node.right, addToRootHash)
d. nodeSignature = f"{leftHash},{node.val},{rightHash}"
e. nodeHash = hash(nodeSignature)
f. if addToRootHash: rootHashes.add(nodeHash)
g. Return nodeHash
3. merkleHash(root, True)
4. subRootHash = merkleHash(subRoot, False)
5. Return subRootHash in rootHashes
Solution 1: [DFS] Pre Order DFS Node By Node Recursive Comparison [SC Opt] - 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
# 2 Match Cases:
# - both are laves
# - values match
# 2 Failure Cases:
# - only one is a leaf
# - values do not match
if s == None and t == None:
return True
if s == None or t == None:
return False
if s.val != t.val:
return False
# Match:
# Validate the rest of children to ensure a full match
return isSameTree(s.left, t.left) and isSameTree(s.right, t.right)
# Empty check:
# An empty subtree matches any tree
if subRoot == None:
return True
# Empty check:
# Nothing can match an empty root tree
if root == None:
return False
# Full Match:
# Check if trees are a complete match starting at root of original
if isSameTree(root, subRoot):
return True
# Subtree Match:
# Check if trees are a complete match at some inner subtree of original
return self.isSubtree(root.left, subRoot) or self.isSubtree(root.right, subRoot)
# overall: tc O(n * m)
# overall: sc O(h1 + h2)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [DFS] Post Order Merkle Tree Via Subtree Hash Fingerprinting [TC Opt] - Tree/DFS Pre Order Recursive One Sided Top Down
def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
# Note:
# DFS post order: left -> right -> root
# 1. Process -> left -> right :
# hash children first (bottom up)
# 2. Process root -> :
# combine children hashes into node hash (Merkle style)
# Result: if subRoot hash appears anywhere in root's hash set, its a subtree
import hashlib
# Hash set to store all subtree hashes in root,
# will be used to compare subtree vs merkle tree of original full tree
# sc: O(n)
rootHashes = set()
# Recursive abstraction call to hash a subtree bottom up
# tc: O(n)
# sc: O(n)
def merkleHash(node, addToRootHash):
# Post Order: Hash children before hashing root
# Base case:
# Null node gets a consistent null sentinel hash
if node == None:
return "#"
# Post Order: Hash children first (bottom up)
leftHash = merkleHash(node.left, addToRootHash)
rightHash = merkleHash(node.right, addToRootHash)
# Combine children hashes + node value into single hash
# Delimiters prevent value collision (e.g. val 1,2 vs val 12)
nodeSignature = f"{leftHash},{node.val},{rightHash}"
nodeHash = hashlib.md5(nodeSignature.encode()).hexdigest()
# This subtree's hash belongs to original root tree
if addToRootHash:
rootHashes.add(nodeHash)
return nodeHash
# Build all subtree hashes for root (bottom up)
merkleHash(root, True)
# Build subRoot hash using same hashing logic
subRootHash = merkleHash(subRoot, False)
# Check if subRoot fingerprint exists anywhere in root
res = subRootHash in rootHashes
# overall: tc O(n + m)
# overall: sc O(n + m)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
105. Construct Binary Tree from Pre Order and In Order Traversal ::1:: - Medium
Topics: Tree Traversal, 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
Given a pre and in order traversal with no null markers, construct the original tree.
The problem gives 2 lists because there are no null markers in either of the lists. Null markers solve "where does each subtree end". Pre/post order solve "where does the root sit" for free. In order with a root location solve "everything to the left and right belong to the corresponding subtrees".
Since we are given no null markers, we need either pre/post order + in order.
Pseudocode
Sol 1: DFS Pre Order Recursive In Order Hash Map
1. inOrderIndexMapping = {val: idx for idx, val in enumerate(inorder)}
2. (preIndex = 0)
3. preAndInArrayToTree(left, right):
a. if left > right: return None
b. rootVal = preorder[preIndex]
c. root = TreeNode(rootVal)
d. rootIndex = inOrderIndexMapping[rootVal]
e. preIndex += 1
f. root.left = preAndInArrayToTree(left, rootIndex - 1)
g. root.right = preAndInArrayToTree(rootIndex + 1, right)
h. Return root
4. Return preAndInArrayToTree(0, len(inorder)-1)
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]:
# Pre Order + In Order Array (no null markers):
# To reconstruct the tree we need to solve these two requirements:
# - "where does the root sit"
# - "where does each subtree end" (i.e. the left/right split)
# Pre Order + In Order or Post Order + In Order Covers Both:
# - Pre/post order solve "where does the root sit" for free,
# root is always first (pre-order) or always last (post-order),
# a fixed position regardless of tree shape.
# - In order + known root, the root being provided from Pre/post order, solves
# "everything to the left of root belongs to the left subtree and vice versa for right"
# - (So for this problem), Pre/post order + in order is fully self-sufficient as we get
# a fixed root position through pre order and explicit subtree boundaries through in order
# - (for contrast) Null markers solve "where does each subtree end",
# an explicit sentinel marks every missing child, so the exact size of every
# subtree is recoverable from the token stream itself.
# Note:
# We are given 2 arrays, pre order and in order, which are traversals of the same tree.
# The pre order array will pointer will represent the root of the tree
# The in order will represent the left and right subtrees of that root
# 1
# / \
# 2 3
# / \
# 4 5
# preorder (root -> left -> right): [1, 2, 4, 5, 3]
# inorder (left -> root -> right): [4, 2, 5, 1, 3]
# 1. Grab root value from pre order pointer
# 2. Since the values are unique, we can use root value to find root index in in order array
# 3. The root index in the in order array will split the in order array into left and right subtrees
# 4. We will then recursively build the left and right subtrees using the left and right sections of the in order array
# 5. Move the pre order pointer to the next root which because (root -> left -> right),
# will be the left and right subtrees we just marked into sections
# Recursive abstraction to build tree from pre and in order arrays
# left and right are relative to the in order array,
# and represent the bounds of the current subtree we are building
def preAndInArrayToTree(left, right):
# Pre Order:
# Holds the current root of the current tree we are building
nonlocal preIndex
# Base case:
# no elements remain for in order, return leaf
if left > right:
return None
# 1. Grab root value from pre order pointer
rootVal = preorder[preIndex]
root = TreeNode(rootVal)
# 2. Since the values are unique, we can use root value to find root index in in order array
rootIndex = inOrderIndexMapping[rootVal]
# 3. The root index in the in order array will split the in order array into left and right subtrees
# [left ... root ... right] =>
# [left ... root - 1, root, root + 1 ... right]
# | left subtree | r | right subtree |
# Left subtree gets the left section: [left ... root - 1]
leftSubtreeLeftNode = left
leftSubtreeRightNode = rootIndex - 1
# Right subtree gets the right section: [root + 1 ... right]
rightSubtreeLeftNode = rootIndex + 1
rightSubtreeRightNode = right
# Iterate to next root from pre order (root -> left -> right),
# so the left and right subtrees we just marked are about to be built
preIndex += 1
# 4. We will then recursively build the left and right subtrees using the left and right sections of the in order array
# Recurse to assign the left and right subtree range
root.left = preAndInArrayToTree(leftSubtreeLeftNode, leftSubtreeRightNode)
root.right = preAndInArrayToTree(rightSubtreeLeftNode, rightSubtreeRightNode)
# 5. Move the pre order pointer to the next root
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 currently 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
inOrderIndexMapping = {}
for idx, val in enumerate(inorder):
inOrderIndexMapping[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 ::1:: - Medium
Topics: Tree Structure Analysis, 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
For a binary tree with values, determine at each node, if you should grab the node value and skip both the children, or skip the node value and grab either of the children.
Pseudocode
Sol 1: Recursive Post Order DP
1. dfs(node):
a. if not node: return (0, 0)
b. (leftRob, leftSkip) = dfs(node.left)
c. (rightRob, rightSkip) = dfs(node.right)
d. robNode = node.val + leftSkip + rightSkip
e. skipNode = max(leftRob, leftSkip) + max(rightRob, rightSkip)
f. Return (robNode, skipNode)
2. (rootRob, rootSkip) = dfs(root)
3. Return max(rootRob, rootSkip)
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)
# Grab best choice from children to calculate best choice for root
def dfs(node):
# Base case:
# A leaf node has no value,
# mark both the rob and skip values as 0
if not node:
return (0, 0)
# Post Order traversal
# grab value for rob and skip from children
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 and skip children
robNode = node.val + leftSkip + rightSkip
# Skip node and max between rob or skip children
skipNode = max(leftRob, leftSkip) + max(rightRob, rightSkip)
# Return rob and skip values for node
return (robNode, skipNode)
# Grab the rob/skip values for the root node
(rootRob, rootSkip) = dfs(root)
# overall: tc O(n)
# overall: sc O(log(n)) for balanced / O(n) for skewed trees
return max(rootRob, rootSkip)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
1325. Delete Leaves With a Given Value ::1:: - Medium
Topics: Tree Structure Analysis, 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
Given a binary tree, remove all leaves that match the target value, including nodes that become leaves after their children have been removed.
Pseudocode
Sol 1: Post Order Recursive Prune Upwards While Empty
1. if not root: return None
2. root.left = removeLeafNodes(root.left, target)
3. root.right = removeLeafNodes(root.right, target)
4. if root.left == None and root.right == None and root.val == target:
Return None
5. Return root
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
# Post Order (left -> right -> root):
# Remove children first and determine if root will become a leaf
# Base case:
# Nothing to remove
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:
# - left and right are None, (current node is a leaf)
# - Node has the target value
if root.left == None and root.right == None 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 |
|---|---|---|---|---|
366. Find Leaves of Binary Tree ::1:: - Medium
Topics: Tree Structure Analysis, Tree, Depth First Search, Binary Tree
Intro
You are given the root of a binary tree. Your task is to collect the nodes of the tree in a specific way that simulates repeatedly removing leaf nodes. The process works as follows: First, identify and collect all current leaf nodes (nodes with no children) into a group. Remove these leaf nodes from the tree. After removal, some nodes that previously had children may now become new leaf nodes. Repeat steps 1-2, collecting each new set of leaf nodes into separate groups. Continue this process until the entire tree is empty The result should be a list of lists, where each inner list contains the values of nodes that were removed together in the same iteration.
| Example Input | Output |
|---|---|
| root = [1, 2, 3, 4, 5] | [[4, 5, 3], [2], [1]] |
Constraints:
The number of nodes in the tree is in the range [1, 100].
-100 ≤ Node.val ≤ 100
Abstraction
Given a binary tree, find the node to leaf height for each node, which is the height distance from itself to the farthest leaf in its left or right subtree. Then group nodes by that height.
Equivalent to removing all leaves layer by layer and grouping as we go.
Pseudocode
Sol 1: DFS Post Order Single Pass Height Bucketing
1. res = []
2. dfs(node):
a. if not node: return -1
b. leftHeight = dfs(node.left)
c. rightHeight = dfs(node.right)
d. height = 1 + max(leftHeight, rightHeight)
e. if height == len(res): res.append([])
f. res[height].append(node.val)
g. Return height
3. dfs(root)
4. Return res
Solution 1: [DFS] DFS Post Order Single Pass Height Bucketing - Tree/DFS Post Order Traversal
def findLeaves(self, root: Optional[TreeNode]) -> List[List[int]]:
# DFS Post Order Height Bucketing
# Track nodes height during DFS iteration
# and bucket node's value into result[height] bucket as we go
# Height buckets
# sc: O(n)
res = []
def dfs(node) -> int:
# Base Case:
# a null child has no height,
# pass back -1 so that the parent leaf is assigned height 0
if not node:
return -1
# Height of children
leftHeight = dfs(node.left)
rightHeight = dfs(node.right)
# Compute Height:
# 1 + the larger distance to a leaf between the two subtrees
height = 1 + max(leftHeight, rightHeight)
# Bucket Placement:
# ensure height has a bucket before appending
if height == len(result):
res.append([])
res[height].append(node.val)
# Return heigh to root node
return height
# Start height bucketing on root
dfs(root)
# overall: tc O(n)
# overall: sc O(n)
return res124. Binary Tree Maximum Path Sum ::1:: - Hard
Topics: Tree Subsection Splitting, 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.
Each node has either positive or negative value. We will use the negative values to split the tree into tree sections of only positive sections.
Pseudocode
Sol 1: Post Order Negative Path 0 Flattening Reset Separating Positive Subtrees
1. (maxSum = -inf)
2. dfs(node):
a. if not node: return 0
b. leftGain = max(dfs(node.left), 0)
c. rightGain = max(dfs(node.right), 0)
d. currTree = node.val + leftGain + rightGain
e. maxSum = max(maxSum, currTree)
f. Return node.val + max(leftGain, rightGain)
3. dfs(root)
4. Return maxSum
Solution 1: [Dynamic Programming] [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
# Positive Splits:
#
# [1, 5, 8], [2], [6, 3]
# Postorder Traversal (DFS): (left -> right -> root)
# - Need to process left and right subtrees
# before to determine where the positive tree paths are
# Dynamic Programming:
# - DP state: dp[node] = max downward path sum starting at node,
# extending into ONE child branch (a path can only pass through
# a node once, so it can't fork into both children and still
# extend upward to the parent)
# - Transition: dp[node] = node.val + max(0, dp[left], dp[right])
# - Base case: dp[None] = 0
# - Each dp[node] is computed exactly once (postorder = correct
# evaluation order, since dp[node] needs dp[left]/dp[right] first)
# and reused by its parent — this is memoization via recursion,
# no need for an explicit memo table since the tree structure
# itself guarantees each subproblem is visited once
# - maxSum is NOT dp[root] — it's a separate global answer that
# checks, at every node, the path that uses BOTH branches
# (node as the "peak"), since dp[node] alone only tracks what
# can be extended upward
# 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 |
|---|---|---|---|---|
2872. Maximum Number of K-Divisible Components ::1:: - Hard
Topics: Tree Subsection Splitting, Tree, Depth First Search, Greedy
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
Given a tree, find the maximum number of edges we can cut so that every resulting piece has a value sum divisible by k.
Each node contributes its value to a running subtree sum. We will use divisibility by k as the split point, so whenever a subtree's sum is a multiple of k, we can cut it away from its parent as its own valid component and continue.
Pseudocode
Sol 1: Greedy Postorder Simulate Tree Cutting By Returning 0
1. graph = adjacency list from edges (undirected)
2. (components = 0)
3. dfs(node, parent):
a. subtree_sum = values[node]
b. for each neighbor in graph[node]:
if neighbor == parent: continue
subtree_sum += dfs(neighbor, node)
c. if subtree_sum % k == 0:
components += 1
Return 0
d. Return subtree_sum
4. dfs(0, -1)
5. Return components
Solution 1: [Greedy] [DFS] Greedy Postorder Simulate Tree Cutting By Returning 0 - Tree/DFS Post Order Recursive Bottom Up
def maxKDivisibleComponents(self, n: int, edges: List[List[int]], values: List[int], k: int) -> int:
# Greedy Tree Cutting:
# The challenge is the problem asks for the MAXIMUM number of components,
# so brute force would be to try every possible set of edge cuts
# which would lead to O(2^n)
# Greedy Tree Cutting avoids trying every combination and allows O(n),
# as we can greedily cut edges bottom up using postorder DFS
# Key Point:
# When a subtree sum is divisible by k, we can safely cut it from its parent
# because adding a multiple of k to any sum never changes its remainder
# Summary:
# Pass 1 (Postorder):
# - accumulate subtree sums bottom up
# - cut edge immediately when subtree sum % k == 0
# Since the original graph is undirected,
# we need a undirected adjacency list
# to represent the connection going both ways for each edge
# sc: O(n)
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
# Total component count
# sc: O(1)
self.components = 0
# ------------------------------
# Pass 1: Postorder DFS (left- > right -> root)
# Greedily cut edges bottom up, counting valid components
# Need to calculate the components for children to determine components for root
# - 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)
def dfs(node, parent):
# Start subtree sum with current node's value
subtree_sum = values[node]
# Process left -> right ->
# accumulate subtree sums from all children
for leftOrRight in graph[node]:
# Skip original parent to avoid cycles
# (in case node is pointing to itself in adjacency list)
if leftOrRight == parent:
continue
subtree_sum += dfs(leftOrRight, 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 — proof above
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
# Pass 1: postorder greedy cut, count valid components
dfs(0, -1)
# overall: tc O(n)
# overall: sc O(n)
return self.components663. Equal Tree Partition ::1:: - Medium
Topics: Tree Subsection Splitting, Tree, Depth First Search, Binary Tree
Intro
You are given the root of a binary tree. Your task is to determine if it's possible to split the tree into two separate trees by removing exactly one edge, such that both resulting trees have equal sums of node values. The problem asks you to return true if such a partition exists, and false otherwise.
| Example Input | Output |
|---|---|
| root = [5,10,10,null,null,2,3] | true |
| root = [1,2,10,null,null,2,20] | false |
Constraints:
The number of nodes in the tree is in the range [1, 10^4].
-10^5 ≤ Node.val ≤ 10^5
Abstraction
Removing any single edge splits the tree into exactly two pieces: the subtree rooted at that edge's child, and everything else (total sum minus that subtree's sum). So the maximum product can be found by computing the sum of every possible subtree via one DFS pass, then for each subtree sum t, checking t * (totalSum - t) and keeping the best result.
Pseudocode
Sol 1: Post Order Subtree Sum Target Match
1. if not root.left and not root.right: return False
2. subtreeSums = []
3. dfs(node):
a. if not node: return 0
b. leftSum = dfs(node.left)
c. rightSum = dfs(node.right)
d. subtreeSum = node.val + leftSum + rightSum
e. subtreeSums.append(subtreeSum)
f. Return subtreeSum
4. totalSum = dfs(root)
5. subtreeSums.pop()
6. if totalSum % 2 != 0: return False
7. target = totalSum // 2
8. Return target in subtreeSums
Solution 1: [DFS] Post Order Subtree Sum Target Match - Tree/DFS Post Order Traversal
def checkEqualTree(self, root: Optional[TreeNode]) -> bool:
# DFS Post Order Subtree Sum Target Match
# Removing any single edge splits the tree into exactly two
# pieces: the subtree rooted at the child side of that edge,
# and everything else (totalSum minus that subtree's sum). For
# those two pieces to have equal sums, the subtree's sum must
# be exactly half of totalSum, which automatically guarantees
# the remaining piece is the other half.
# This means a valid partition exists if and only if some
# subtree sum (other than the root's own sum, since there's no
# edge above the root to remove) equals totalSum / 2.
# We can compute every subtree's sum in a single post-order DFS
# pass (children fully summed before the parent), collecting
# each one as a candidate. The root's own final sum is excluded
# afterward, since it represents "the whole tree," not a valid
# split.
# Edge Case:
# a single node has no edges to remove at all
if not root.left and not root.right:
return False
# Track every subtree sum encountered
# sc: O(n)
subtreeSums = []
def dfs(node):
# Base Case:
# a null node contributes 0 to its parent's sum
if not node:
return 0
# Process Children First (Post Order):
# a node's subtree sum depends on both children's sums
leftSum = dfs(node.left)
rightSum = dfs(node.right)
# Compute Subtree Sum:
subtreeSum = node.val + leftSum + rightSum
# Record Candidate Split:
# removing the edge above this node produces this exact split
subtreeSums.append(subtreeSum)
return subtreeSum
# Total sum of the entire tree, also the last value dfs() returns
# tc: O(n)
totalSum = dfs(root)
# Exclude Root's Own Sum:
# the whole tree's sum is not a valid split, there's no edge above it
subtreeSums.pop()
# Target Split Check:
# a valid partition exists if some subtree equals exactly half
# of totalSum, since the remaining piece automatically matches
# tc: O(n)
if totalSum % 2 != 0:
return False
target = totalSum // 2
# overall: tc O(n), one DFS pass to compute all subtree sums,
# one linear pass to check for the target
# overall: sc O(n), for subtreeSums plus O(h) recursion stack
# depth, h = height of tree
return target in subtreeSums1339. Maximum Product of Splitted Binary Tree ::1:: - Medium
Topics: Tree Subsection Splitting, Tree, Depth First Search, Binary Tree
Intro
Given the root of a binary tree, split the binary tree into two subtrees by removing one edge such that the product of the sums of the subtrees is maximized. Return the maximum product of the sums of the two subtrees. Since the answer may be too large, return it modulo 109 + 7. Note that you need to maximize the answer before taking the mod and not after taking it.
| Example Input | Output |
|---|---|
| root = [1,2,3,4,5,6] | 110 |
| root = [1,null,2,3,4,null,null,5,6] | 90 |
Constraints:
The number of nodes in the tree is in the range [2, 5 * 10^5]
1 ≤ Node.val ≤ 10^4
Abstraction
Removing any single edge splits the tree into exactly two pieces: the subtree rooted at that edge's child, and everything else (total sum minus that subtree's sum). So the maximum product can be found by computing the sum of every possible subtree via one DFS pass, then for each subtree sum t, checking t * (totalSum - t) and keeping the best result.
Pseudocode
Sol 1: Post Order Subtree Sum Enumeration
1. MOD = 10**9 + 7
2. subtreeSums = []
3. dfs(node):
a. if not node: return 0
b. leftSum = dfs(node.left)
c. rightSum = dfs(node.right)
d. subtreeSum = node.val + leftSum + rightSum
e. subtreeSums.append(subtreeSum)
f. Return subtreeSum
4. totalSum = dfs(root)
5. (maxProduct = 0)
6. for each t in subtreeSums:
maxProduct = max(maxProduct, t * (totalSum - t))
7. Return maxProduct % MOD
Solution 1: [DFS] Post Order Subtree Sum Enumeration - Tree/DFS Post Order Traversal
def maxProduct(self, root: Optional[TreeNode]) -> int:
# DFS Post Order Subtree Sum Enumeration
# Removing any single edge splits the tree into exactly two
# pieces: the subtree rooted at the child side of that edge,
# and everything else (the rest of the tree). If a subtree
# rooted at some node has sum t, then removing the edge above
# that node always produces a split of (t, totalSum - t).
# This means every possible split is fully determined by every
# possible subtree sum in the tree. Rather than simulating each
# edge removal individually, we can compute every subtree's sum
# in a single post-order DFS pass (children fully summed before
# the parent), collecting each one as a candidate split point.
# Once every subtree sum is known, the answer is just the
# maximum of t * (totalSum - t) across all collected sums.
MOD = 10**9 + 7
# Track every subtree sum encountered
# sc: O(n)
subtreeSums = []
def dfs(node):
# Base Case:
# a null node contributes 0 to its parent's sum
if not node:
return 0
# Process Children First (Post Order):
# a node's subtree sum depends on both children's sums
leftSum = dfs(node.left)
rightSum = dfs(node.right)
# Compute Subtree Sum:
subtreeSum = node.val + leftSum + rightSum
# Record Candidate Split:
# removing the edge above this node produces this exact split
subtreeSums.append(subtreeSum)
return subtreeSum
# Total sum of the entire tree, also the last value dfs() returns
# tc: O(n)
totalSum = dfs(root)
# Maximize Product Across All Candidate Splits:
# for each subtree sum t, the split is (t, totalSum - t)
# tc: O(n)
maxProduct = 0
for t in subtreeSums:
maxProduct = max(maxProduct, t * (totalSum - t))
# overall: tc O(n), one DFS pass to compute all subtree sums,
# one linear pass to find the best split
# overall: sc O(n), for subtreeSums plus O(h) recursion stack
# depth, h = height of tree
res = maxProduct % MOD
# overall: tc O(n)
# overall: sc O(n)
return res834. Sum of Distances in Tree ::1:: - Hard
Topics: Connected Components, Dynamic Programming, Tree, Depth First Search, Graph Theory, Tree Rerooting
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
Given a tree, which is just a connected component with n-1 edges with no cycles, find the sum of distances from every node to every other node.
Rather than recomputing distances from scratch for each node, we compute the sum once for one root, then reroot and shifting the root one edge at a time and adjusting the previous answer based on how many nodes are now closer or farther.
Since this connected component has no cycles, we do not need to worry about recomputing the sum of distance (a shorter path) for the node that ends up with the cycle
Pseudocode
Sol 1: ReRooting DP On PostOrder Node 0 and PreOrder All Other Nodes
1. tree = undirected adjacency list from edges
2. nodeToAllTotal = [0] * n
3. subtreeSize = [1] * n
4. dfs_postOrder(node, parent):
a. for each child in tree[node]:
if child == parent: continue
dfs_postOrder(child, node)
subtreeSize[node] += subtreeSize[child]
nodeToAllTotal[node] += nodeToAllTotal[child] + subtreeSize[child]
5. dfs_preOrder(node, parent):
a. for each child in tree[node]:
if child == parent: continue
nodeToAllTotal[child] = nodeToAllTotal[node] - subtreeSize[child] + (n - subtreeSize[child])
dfs_preOrder(child, node)
6. dfs_postOrder(0, -1)
7. dfs_preOrder(0, -1)
8. Return nodeToAllTotal
Solution 1: [DFS] ReRooting DP On PostOrder Node 0 and PreOrder All Other Nodes - Tree/DFS Post Order Recursive Two Sided Bottom Up
def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]:
# Re-rooting DP:
# A common idea in competitive programming,
# and somewhat easy to spot as such problems typically ask something like
# "find some value for each root"
# Re-rooting DP (Dynamic Programming On Trees):
# The challenge is the problem asks for answer at EVERY node,
# pretending as if that node is the root, so brute force would be:
# 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),
# as we can 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 we need to know the size of subtrees to calculate
# how many nodes are closer and how many are now farther
# Summary:
# Using the previously computed DP values, we can derive
# the child's answer in O(1) instead of running another DFS in O(n)
# General Re-rooting DP Pattern:
# Pass 1 (Postorder):
# - gather information from children -> parent
# Pass 2 (Preorder):
# - Propagate answers from parent -> children
# Since the original graph is undirected,
# we need a undirected adjacency list
# to represent the connection going both ways for each edge
# sc: O(n)
tree = defaultdict(list)
for u, v in edges:
tree[u].append(v)
tree[v].append(u)
# Sum of distances from node i to all other nodes:
# Ex: After our Post Order iteration for node 0, we get
# nodeToAllTotal[0] = sum of distances from root node 0 to all other nodes
# sc: O(n)
nodeToAllTotal = [0] * n
# Subtree sizes for the subtree at each node:
# Re-rooting requires us to know the subtree sizes so
# we can modify the solution for node 0 for all other nodes
# while iterating from parent -> child, we must know:
# - how many nodes get closer?
# - how many nodes get farther?
# Ex:
# 0
# / \
# 1 2
# / \
# 3 4
#
# count[0] = 5 (entire tree)
# count[1] = 1 (nodes 1)
# count[2] = 3 (nodes 2,3,4)
# count[i] = size of i's subtree
# sc: O(n)
subtreeSize = [1] * n
# ------------------------------
# Pass 1: Postorder DFS (left -> right -> root)
# - Solving for node 0 as the root,
# find the total sum of distances to all nodes
# - Calculate the subtree size for the subtree at each node during iteration
# to be used during Preorder dfs
# Process all children first:
# - pass up total distance needed
# - pass up subtree sizes
def dfs_postOrder(node, parent):
# First process children in adjacency matrix
for leftOrRight in tree[node]:
# Skip original parent to avoid cycles
# (in case node is pointing to itself in adjacency matrix)
if leftOrRight == parent:
continue
# ----------------------------------
# Generate subtree size for children
# Process child subtree first,
# and add children subtree size to parent
dfs_postOrder(leftOrRight, node)
subtreeSize[node] += subtreeSize[leftOrRight]
# ----------------------------------
# Use subtree size for children to calculate total distance for parent
# distances from parent node to all nodes in parent's own subtree subtree =
# nodeToAllTotal[child] — distances from child node to all nodes in child's own subtree
# + subtreeSize[child] — every node in child's subtree is 1 edge farther from parent node than from child node
nodeToAllTotal[node] += nodeToAllTotal[leftOrRight] + subtreeSize[leftOrRight]
# ------------------------------
# Pass 2: Preorder DFS (root -> left -> right)
# Using the answer for node 0, derive the answer for all other nodes
# We have solved for root already via DP (node 0 -> ith):
# - continue to solve downwards
# - propagate distances and re-root downward to children
def dfs_preOrder(node, parent):
# First process children in adjacency matrix
for leftOrRight in tree[node]:
# Skip original parent to avoid cycles
# (in case node is pointing to itself in adjacency matrix)
if leftOrRight == parent:
continue
# ----------------------------------
# Use parent's allNodeDistance to derive child's allNodeDistance
# distances from child node to all nodes in child's own subtree =
# nodeToAllTotal[node] — parent's distance to all nodes in tree
# - subtreeSize[leftOrRight] — every node in child's subtree is 1 edge closer to child than to parent
# + (n - subtreeSize[leftOrRight]) — every node outside child's subtree is 1 edge farther from child than from parent
nodeToAllTotal[leftOrRight] = nodeToAllTotal[node] - subtreeSize[leftOrRight] + (n - subtreeSize[leftOrRight])
# ----------------------------------
# Continue to propagate parent to children allNodeDistance
# 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 nodeToAllTotal427. Construct Quad Tree ::1:: - Medium
Topics: Tree Traversal, Array, Depth First Search, Divide and Conquer, Tree, Matrix, Grid
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
Given a grid, translate it into a quad tree. A Quad Trees is a data structure that efficiently divides 2D space into smaller regions.
Each cell in the grid is either 0 or 1. We will use value uniformity as the split point, whenever a region is entirely one value it becomes a leaf, whenever a region contains mixed values we quarter it into four sub grids and recurse until every section is uniform and becomes a leaf.
Pseudocode
Sol 1: Quad Tree Construction Split Grid Into 4 Subsections And Turn Leaf When Uniform Values
1. n = len(grid)
2. dfs(r0, c0, size):
a. if size == 1:
Return Node(val=grid[r0][c0], isLeaf=True)
b. half = size // 2
c. topLeft = dfs(r0, c0, half)
d. topRight = dfs(r0, c0 + half, half)
e. bottomLeft = dfs(r0 + half, c0, half)
f. bottomRight = dfs(r0 + half, c0 + half, half)
g. subtreeLeaves = all 4 children isLeaf
h. subtreeValues = all 4 children val equal
i. if subtreeLeaves and subtreeValues:
Return Node(val=topLeft.val, isLeaf=True)
j. Return Node(val=True, isLeaf=False, topLeft, topRight, bottomLeft, bottomRight)
3. Return dfs(0, 0, n)
Solution 1: [DFS] Quad Tree Construction Split Grid Into 4 Subsections And Turn Leaf When Uniform Values - Tree/DFS Pre order Traversal
def construct(self, grid: List[List[int]]) -> 'Node':
# DFS Post Order Quad Tree Construction:
# (top-left -> top-right -> bottom-left -> bottom-right -> root)
# - Divide the grid into 4 equal sub-grids
# - Process all 4 children first (Post Order), then merge at root
# - If all 4 children are leaves with the same value (uniformity):
# the root collapses into a single leaf
# - If the 4 children don't uniformly match:
# keep an internal node with all 4 children attached
# Grid size
# sc: O(1)
n = len(grid)
def dfs(r0, c0, size) -> Node:
# Base Case 1x1 Grid:
# We've reached a single 1x1 cell grid,
# which will always be a leaf since a single cell is by definition uniform.
# Go back up recursion call to 4x4 cell grid and continue up
if size == 1:
return Node(val=bool(grid[r0][c0]), isLeaf=True)
# 4x4 Cell Grid:
# We have at least 4 cells,
# now we need to validate if we have uniform cells or if we need to recurse
# Divide:
# split the current grid into 4 equal quadrants
half = size // 2
# Process Children First (Post Order):
# recursively build each quadrant before merging at this level
# tc: O(1) for each call,
# tc: O(n^2) recursive cost of the 4 children across recursion tree
topLeft = dfs(r0, c0, half)
topRight = dfs(r0, c0 + half, half)
bottomLeft = dfs(r0 + half, c0, half)
bottomRight = dfs(r0 + half, c0 + half, half)
# Merge Check:
# root can collapse into a single leaf if:
# - all 4 children are leaves
# - all 4 leaves hold the same value
subtreeLeaves = (
topLeft.isLeaf and
topRight.isLeaf and
bottomLeft.isLeaf and
bottomRight.isLeaf
)
subtreeValues = (
topLeft.val ==
topRight.val ==
bottomLeft.val ==
bottomRight.val
)
# Grid Uniformity:
# We can collapse into one leaf node
if subtreeLeaves and subtreeValues:
return Node(val=topLeft.val, isLeaf=True)
# Grid Mismatch:
# Create an internal node with 4 subtrees representing the 4 sub grids
return Node(
val=True, isLeaf=False,
topLeft=topLeft,
topRight=topRight,
bottomLeft=bottomLeft,
bottomRight=bottomRight
)
# Initialize at coordinates (0, 0)
# tc: O(n^2) every cell is visited exactly once at the base case for the value uniformity check O(n^2)
# sc: O(n^2 + log n) log n for the recursion stack itself + n^2 for the output quad tree
quadTreeRoot = dfs(0, 0, n)
# overall: tc O(n^2)
# overall: sc O(log n)
return quadTreeRoot| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|