You are given the root
of a binary tree with n
nodes. Each node is uniquely assigned a value from 1
to n
. You are also given an integer startValue
representing the value of the start node s
, and a different integer destValue
representing the value of the destination node t
.
Find the shortest path starting from node s
and ending at node t
. Generate step-by-step directions of such path as a string consisting of only the uppercase letters 'L'
, 'R'
, and 'U'
. Each letter indicates a specific direction:
'L'
means to go from a node to its left child node.'R'
means to go from a node to its right child node.'U'
means to go from a node to its parent node.Return the step-by-step directions of the shortest path from node s
to node t
.
Example 1:
Input: root = [5,1,2,3,null,6,4], startValue = 3, destValue = 6
Output: "UURL"
Explanation: The shortest path is: 3 → 1 → 5 → 2 → 6.
Example 2:
Input: root = [2,1], startValue = 2, destValue = 1
Output: "L"
Explanation: The shortest path is: 2 → 1.
Explain how you would find the shortest path and generate the corresponding directions string. Consider edge cases such as when the start or destination values are not in the tree, or when the tree is empty.
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The brute force approach is all about trying every single path. We will explore every possible route from the starting node to the destination node within the binary tree, one by one, until we find the correct directions.
Here's how the algorithm would work step-by-step:
def find_path_brute_force(root, start_value, destination_value):
shortest_path = None
def find_node(node, value):
if not node:
return None
if node.val == value:
return node
left_result = find_node(node.left, value)
if left_result:
return left_result
return find_node(node.right, value)
start_node = find_node(root, start_value)
destination_node = find_node(root, destination_value)
def explore_paths(current_node, current_path):
nonlocal shortest_path
if not current_node:
return
if current_node == destination_node:
# We found a path to the destination, check if it's the shortest
if shortest_path is None or len(current_path) < len(shortest_path):
shortest_path = current_path
return
# Explore going 'up' (to the parent)
if current_node.parent:
explore_paths(current_node.parent, current_path + ['U'])
# Explore going 'left'
if current_node.left:
explore_paths(current_node.left, current_path + ['L'])
# Explore going 'right'
if current_node.right:
explore_paths(current_node.right, current_path + ['R'])
# Add a parent attribute to each node.
def add_parent_pointers(node, parent):
if not node:
return
node.parent = parent
add_parent_pointers(node.left, node)
add_parent_pointers(node.right, node)
add_parent_pointers(root, None)
# Initiate the search from the starting node with an empty path.
explore_paths(start_node, [])
return ''.join(shortest_path) if shortest_path else ""
The core idea is to find the meeting point, or lowest common ancestor, of the two nodes in the tree. Then, we build paths from each node to that meeting point and combine them appropriately to get the directions.
Here's how the algorithm would work step-by-step:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def step_by_step_directions(
root: TreeNode, start_value: int, destination_value: int) -> str:
def find_node(node: TreeNode, target_value: int) -> TreeNode:
if not node:
return None
if node.val == target_value:
return node
left_node = find_node(node.left, target_value)
if left_node:
return left_node
return find_node(node.right, target_value)
start_node = find_node(root, start_value)
destination_node = find_node(root, destination_value)
def get_path_to_ancestor(node: TreeNode, target: TreeNode, path: str) -> str:
if node is None:
return None
if node == target:
return path
# Explore the left subtree
left_path = get_path_to_ancestor(node.left, target, path + 'L')
if left_path:
return left_path
# Explore the right subtree
right_path = get_path_to_ancestor(node.right, target, path + 'R')
if right_path:
return right_path
return None
# Finds the lowest common ancestor (LCA).
def find_lowest_common_ancestor(
node: TreeNode, start_node: TreeNode, destination_node: TreeNode) -> TreeNode:
if not node:
return None
if node == start_node or node == destination_node:
return node
left_lca = find_lowest_common_ancestor(node.left, start_node, destination_node)
right_lca = find_lowest_common_ancestor(node.right, start_node, destination_node)
if left_lca and right_lca:
return node
return left_lca if left_lca else right_lca
lowest_common_ancestor = find_lowest_common_ancestor(root, start_node, destination_node)
# Find path from start node to LCA
start_to_lca_path = get_path_to_ancestor(lowest_common_ancestor, start_node, '')
# Find path from LCA to destination node.
lca_to_destination_path = get_path_to_ancestor(lowest_common_ancestor, destination_node, '')
# Replace path from start to LCA with 'U's.
up_moves = 'U' * len(start_to_lca_path)
return up_moves + lca_to_destination_path
Case | How to Handle |
---|---|
Root is null | Return an empty string or null, as no path can exist from a null root. |
Start node and destination node are the same | Return an empty string indicating zero moves are needed. |
Either startValue or destValue does not exist in the tree | Return an appropriate error (null, exception) indicating no path exists. |
Large tree, potential for stack overflow with recursion | Consider an iterative approach or a limited recursion depth to avoid stack overflow errors. |
Start node is an ancestor of the destination node | The path should only contain 'D' (down) moves from the start to the destination. |
Destination node is an ancestor of the start node | The path should only contain 'U' (up) moves from the start to the destination. |
The tree is a skewed tree (all nodes on one side) | The solution should correctly handle traversal even in the case of a skewed tree. |
Integer overflow if node values are large | Node values are typically not directly used to compute path, this is not an issue. |