DFS

Coding Style

  • 变量 / 函数名

    • 使用 小写字母 + 下划线(snake_case),如 max_value

  • 类名

    • 使用 首字母大写驼峰(PascalCase),如 MyClass

  • 常量

    • 全部大写,用下划线分隔,如 MAX_SIZE

  • 私有变量/方法

    • 前面加单下划线 _var

    • 双下划线 __var 用于避免子类覆盖。

Tip: rewrite the code using chatGPT and compare the difference

DFS

  • Depth First Search
  • One starts at the root (selecting some arbitrary node as the root in the case of a graph) and explores as far as possible along each branch before backtracking.

BFS

  • Breadth First Search
  • One starts at the root (selecting some arbitrary node as the root in the case of a graph) and explores the neighbor nodes first, before moving to the next level neighbors.

Searching Algorithm

Problem

sub-Problem

sub-Problem

sub-Problem

sub-sub-Problem

sub-sub-Problem

sub-sub-sub-Problem

base-

problem

Depth First Search

Search Order:

  • D, L, U, R

Search Order:

  • D, L, U, R

Problem

sub-Problem

sub-Problem

sub-Problem

sub-sub-Problem

sub-sub-Problem

Breadth First Search

sub-sub-Problem

sub-sub-Problem

Search Order:

layer by layer 

Search Order:

layer by layer 

A

/  \

B    C

/  \   /  \

D   E F   G

        /          \  

     H            I

  • Preorder Traversal
    • A B D E H C F G I
    • DFS Search Path
  • Level Order Traversal
    • A B C D E F G H I
    • BFS Search Path

Tree Traversal

DFS Search Order:

U, R, D, L

Filling the Grid

DFS Search Order:

U, R, D, L

Filling the Grid

BFS Search Order

 

Filling the Grid

BFS Search Order

 

Filling the Grid

Use Case

DFS and BFS are both algorithms for searching TREE and GRAPH, but we will ONLY focus on trees, which is more common in interviews.

  • DFS
    • Recursion and backtrace
    • Stack
  • BFS
    • Find minimum path/distance/...
    • Queue

DFS

  • Most DFS related problems in interview is for trees.
    • Balanced Binary Tree
    • Symmetric Tree
    • Minimum Depth of Binary Tree
    • etc.
  • More general, most recursion problems are also DFS.
    • Permutation, Combination, Combination Sum.
    • Letter Combination of Phone Number, Generate Parenthesis.
    • Surrounded Regions, Word Search,

Basic Terms of Tree

  • Root
  • Node (internal node)
  • Leaf (external node)
  • Parent
  • Child
  • Siblings
  • Ancestor
  • Descendant
  • Edge (N - 1)
  • Path
  • Height
  • Depth

A

B

C

F

E

D

PreOrder Traversal

InOrder Traversal

PostOrder Traversal

They are all DFS

Depth of Tree

Given a binary tree, calculate its depth.

1

2

3

Depth: 3

1

2

3

4

Depth: 4

3

2

3

3

4

1

2

3

4

2

Depth: 4

Depth of Tree

Given a binary tree, calculate its depth.

def depth(root):
    if root is None:
        return 0
    return max(depth(root.left), depth(root.right)) + 1

1

2

3

4

Depth: 4

3

2

3

3

4

Balanced Binary Tree

Given a binary tree, determine if it is height-balanced.

For this problem, a height-balanced binary tree is defined as a binary tree in which the depth of the two subtrees of every node never differ by more than 1.

1

2

2

3

3

4

4

False

1

2

3

4

True

3

2

3

3

4

Balanced Binary Tree

Given a binary tree, determine if it is height-balanced.

For this problem, a height-balanced binary tree is defined as a binary tree in which the depth of the two subtrees of every node never differ by more than 1.

  • root is balanced
    • Diff(depth(root.left), depth(root.right)) <= 1
    • root.left is balanced && root.right is balanced.

Balanced Binary Tree

Given a binary tree, determine if it is height-balanced.

For this problem, a height-balanced binary tree is defined as a binary tree in which the depth of the two subtrees of every node never differ by more than 1.

def is_balanced(root):
    if root is None:
        return True
    if abs(get_depth(root.left) - get_depth(root.right)) > 1:
        return False
    return is_balanced(root.left) and is_balanced(root.right)

def get_depth(root):
    if root is None:
        return 0
    return max(get_depth(root.left), get_depth(root.right)) + 1

Balanced Binary Tree

Given a binary tree, determine if it is height-balanced.

For this problem, a height-balanced binary tree is defined as a binary tree in which the depth of the two subtrees of every node never differ by more than 1.

def is_balanced(root):
    return depth(root) != -1

def depth(root):
    if root is None:
        return 0
    left_depth = depth(root.left)
    right_depth = depth(root.right)
    if left_depth == -1 or right_depth == -1 or abs(left_depth - right_depth) > 1:
        return -1
    return max(left_depth, right_depth) + 1

Symmetric Tree

Given a binary tree, check whether it is a mirror of itself (ie, symmetric around its center).

    1
   / \
  2   2
 / \ / \
3  4 4  3

True

    1
   / \
  2   2
   \   \
   3    3

False

    1
   / \
  2   2
 / \ / \
4  3 4  3

False

    1
   / \
  2   2
 /     \
3       3

True

Symmetric Tree

Given a binary tree, check whether it is a mirror of itself (ie, symmetric around its center).

  • Root is Symmetric
    • root.left is symmetric to root.right
  • A is symmetric to B
    • A.val == B.val
    • A.left is symmetric to B.right
    • A.right is symmetric to B.left
    1
   / \
  2   2
 / \ / \
3  4 4  3

True

    1
   / \
  2   2
   \   \
   3    3

False

Symmetric Tree

Given a binary tree, check whether it is a mirror of itself (ie, symmetric around its center).

def is_symmetric(root):
    if root is None:
        return True
    return is_symmetric_helper(root.left, root.right)

def is_symmetric_helper(left, right):
    if left is None and right is None:
        return True
    if left is None or right is None:
        return False
    if left.val != right.val:
        return False
    return is_symmetric_helper(left.left, right.right) and is_symmetric_helper(left.right, right.left)

Minimum Depth of Binary Tree

Given a binary tree, find its minimum depth.

The minimum depth is the number of nodes along the shortest path from the root node down to the nearest leaf node.

    1
   / \
  2   2
 / \ / \
3  4 4  3

3

    1
   / \
  2   2
 / \  
3  4   

2

    1
   / \
  2   2
 /     \
3       3

3

Minimum Depth of Binary Tree

Given a binary tree, find its minimum depth.

The minimum depth is the number of nodes along the shortest path from the root node down to the nearest leaf node.

  • Minimum depth of Root
    • if root is leaf, 1.
    • if root.left is null, return right + 1.
    • if root.right is null, return left + 1.
    • return the minimum of (left, right) + 1.

Minimum Depth of Binary Tree

Given a binary tree, find its minimum depth.

The minimum depth is the number of nodes along the shortest path from the root node down to the nearest leaf node.

def min_depth(root):
    if root is None:
        return 0
    left_depth = min_depth(root.left)
    right_depth = min_depth(root.right)
    if left_depth == 0:
        return right_depth + 1
    elif right_depth == 0:
        return left_depth + 1
    return min(left_depth, right_depth) + 1

Path Sum

Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.

      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1

22: [5, 4, 11, 2]

      3
     / \
    2   6
   /   / \
  9   22  9
 / \       \
7   2       1

31: [3, 6, 22]

Path Sum

Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.

      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1

22: [5, 4, 11, 2]

Knapsack: 

  • Except picking from all of numbers, only path from root to leaf is acceptable.
  • hasSum(TreeNode root, int sum)
    • Base case: root is leaf.
    • hasSum(root, sum) = hasSum(root.left, sum-root.val) || hasSum(root.right, sum-root.val)

Path Sum

Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.

      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1

22: [5, 4, 11, 2]

def has_path_sum(root, target_sum):
    if root is None:
        return False
    if root.left is None and root.right is None:
        return target_sum == root.val
    return (has_path_sum(root.left, target_sum - root.val) or 
            has_path_sum(root.right, target_sum - root.val))

Binary Tree Maximum Path Sum

Given a binary tree, find the maximum path sum.

For this problem, a path is defined as any sequence of nodes from some starting node to any node in the tree along the parent-child connections. The path does not need to go through the root.

      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1

maxSum = 7 + 11 + 4 + 5 + 8 + 13 = 48

      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1

Binary Tree Maximum Path Sum

Given a binary tree, find the maximum path sum.

For this problem, a path is defined as any sequence of nodes from some starting node to any node in the tree along the parent-child connections. The path does not need to go through the root.

  5
 / \
-4 -8
  • Branch Sum (root):
    • Sum of nodes that end at root.
  • Maximum Path Sum (root):
    • root.val
    • root.val + maxbranchSum(root.left)
    • root.val + maxbranchSum(root.right)
    • root.val + maxbranchSum(root.left) + maxbranchSum(root.right)
  5
 / \
4  -8
  5
 / \
4   8
  5
 / \
-4  8

Binary Tree Maximum Path Sum

Given a binary tree, find the maximum path sum.

For this problem, a path is defined as any sequence of nodes from some starting node to any node in the tree along the parent-child connections. The path does not need to go through the root.

  • branchSum (root)
    • root.val
    • root.val + branchSum(root.left)
    • root.val + branchSum(root.right)
  • Why do we need branchSum? Only branchSum can be used in the recursion

Binary Tree Maximum Path Sum

Given a binary tree, find the maximum path sum.

For this problem, a path is defined as any sequence of nodes from some starting node to any node in the tree along the parent-child connections. The path does not need to go through the root.

class Solution:
    def __init__(self):
        self.max_sum = float('-inf')

    def max_path_sum(self, root):
        def max_branch_sum(node):
            if node is None:
                return 0
            left_sum = max_branch_sum(node.left)
            right_sum = max_branch_sum(node.right)
            
            # Maximum sum for the current branch including the node
            branch_max_sum = node.val + max(0, max(left_sum, right_sum))
            
            # Update the global maximum sum
            self.max_sum = max(self.max_sum, branch_max_sum, left_sum + node.val + right_sum)
            
            return branch_max_sum
        
        max_branch_sum(root)
        return self.max_sum

Binary Tree Maximum Path Sum

Given a binary tree, find the maximum path sum.

 

Use int[] to pass the maximum through the recursion

def max_path_sum(root):
    if root is None:
        return 0
    max_sum = [float('-inf')]
    max_branch_sum(root, max_sum)
    return max_sum[0]

def max_branch_sum(node, max_sum):
    if node is None:
        return 0
    left_sum = max_branch_sum(node.left, max_sum)
    right_sum = max_branch_sum(node.right, max_sum)
    branch_max_sum = node.val + max(0, max(left_sum, right_sum))
    max_sum[0] = max(max_sum[0], max(branch_max_sum, left_sum + node.val + right_sum))
    return branch_max_sum

DFS for Trees

  • Complete Search
    • If you need to traverse all the nodes in the tree.
    • No need for minimum/shortest/... depth, etc.
  • Normal Recursion
    • Tree is recursive definition,  so applying recursion on trees are very direct and easy understanding
    • Sub-problem
    • Base case
    • Recursion rule
    • ALWAYS think about two branches (left and right), pseudo code is helpful.

More general DFS

  • One Dimension
    • Letter Combination of Phone Number
    • Generate Parenthesis
    • Palindrome Partitioning
    • etc.
  • Two Dimensions
    • Maze
    • N Queens
    • Word Search
    • etc.

Word Search

Given a 2D board and a word, find if the word exists in the grid.

The word can be constructed from letters of sequentially adjacent cell, where "adjacent" cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once.

For example, given a board,

[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

word = "ABCCED", -> returns true,
word = "SEE", -> returns true,
word = "ABCB", -> returns false.

Word Search

class Solution:
    def exist(self, board, word):
        if len(board) == 0 or len(board[0]) == 0:
            return False
        flag = [[False] * len(board[0]) for _ in range(len(board))]
        for i in range(len(board)):
            for j in range(len(board[0])):
                if self.dfs(board, i, j, word, 0, flag):
                    return True
        return False

    def dfs(self, board, x, y, word, index, flag):
        if index == len(word):
            return True
        if x < 0 or y < 0 or x >= len(board) or y >= len(board[0]) or flag[x][y] or board[x][y] != word[index]:
            return False
        
        # Directions: right, down, left, up
        dx = [1, 0, -1, 0]
        dy = [0, 1, 0, -1]
        
        flag[x][y] = True  # Mark the cell as visited
        
        for i in range(4):
            if self.dfs(board, x + dx[i], y + dy[i], word, index + 1, flag):
                return True
        
        flag[x][y] = False  # Unmark the cell (backtrack)
        return False

Palindrome Partitioning

Given a string s, partition s such that every substring of the partition is a palindrome.
Return all possible palindrome partitioning of s.
 
Example:
Input: "aab"
Output: [ ["aa", "b"], ["a", "a", "b"] ]

Given a string s, partition s such that every substring of the partition is a palindrome.
Return all possible palindrome partitioning of s.

 

  • Try substring from the beginning.
    • If it's palindrome, then palindrome partition the left part and merge the results.

Palindrome Partitioning

Palindrome Partitioning

class Solution:
    def partition(self, s):
        results = []
        self.partition_helper(results, s, 0, [])
        return results
    
    def partition_helper(self, results, s, start, path):
        if start == len(s):
            results.append(path[:])
            return
        for i in range(start + 1, len(s) + 1):
            sub = s[start:i]
            if self.is_palindrome(sub):
                path.append(sub)
                self.partition_helper(results, s, i, path)
                path.pop()

    def is_palindrome(self, s):
        return s == s[::-1]
  • Is there any improvement we can do?
    • Think about this, we have to check if string s is a palindrome every single time, do we have a way to simplify the check?
    • substring(a,b) -> substring(a+1, b-1)
    • keep the palindrome result when computing
    • Pre-generate all the palindrome result
    • A two-dimension array result

Palindrome Partitioning

Palindrome Partitioning

class Solution:
    def partition(self, s):
        n = len(s)
        # Precompute the palindrome table
        is_palindrome = [[False] * n for _ in range(n)]

        # Every single character is a palindrome
        for i in range(n):
            is_palindrome[i][i] = True

        # Check adjacent characters
        for i in range(n - 1):
            is_palindrome[i][i + 1] = (s[i] == s[i + 1])

        # Check substrings of length > 2
        for length in range(2, n):
            for i in range(n - length):
                j = i + length
                is_palindrome[i][j] = (s[i] == s[j]) and is_palindrome[i + 1][j - 1]

        results = []
        self.partition_helper(results, s, 0, [], is_palindrome)
        return results

    def partition_helper(self, results, s, start, path, is_palindrome):
        if start == len(s):
            results.append(path[:])  # Make a copy of the path
            return
        for i in range(start, len(s)):
            if is_palindrome[start][i]:
                path.append(s[start:i + 1])
                self.partition_helper(results, s, i + 1, path, is_palindrome)
                path.pop()


General DFS

  • Depth First Search
  • Search solutions from the whole possible search space
  • Search is a process of trying and backtracking
  • Pruning is important