ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด
แก้โจทย์ผลรวมเส้นทางจากรากถึงใบ ผลรวมของทุกเส้นทาง และบรรพบุรุษร่วมที่ใกล้ที่สุดสำหรับต้นไม้ทวิภาคทั่วไปด้วยการไล่เรียกซ้ำ
ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
ผลรวมเส้นทางจากรากถึงโหนดใบ
ปัญหาผลรวมเส้นทาง ถามว่ามีเส้นทางจากรากถึงโหนดใบใดบ้างที่มีผลรวมเท่ากับเป้าหมายหรือไม่ ส่งต่อ เป้าหมายที่เหลืออยู่ ลงไปในการเรียกซ้ำ โดยลบค่าของแต่ละโหนดออก เมื่อถึงโหนดใบ ให้ตรวจสอบว่าค่าที่เหลืออยู่เท่ากับค่าของโหนดใบหรือไม่ วิธีนี้ไม่ต้องเก็บรายการเส้นทางอย่างชัดเจน จึงใช้พื้นที่อย่างมีประสิทธิภาพและเข้าใจง่าย กรณีพิเศษคือ ต้นไม้ที่ว่างเปล่าไม่มีเส้นทาง ดังนั้นให้ส่งคืน False ทันที
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def has_path_sum(root, target):
if not root:
return False
if not root.left and not root.right: # leaf
return root.val == target
remain = target - root.val
return (has_path_sum(root.left, remain) or
has_path_sum(root.right, remain))
root = TreeNode(5)
root.left = TreeNode(4)
root.right = TreeNode(8)
root.left.left = TreeNode(11)
root.left.left.left = TreeNode(7)
root.left.left.right = TreeNode(2)
print(has_path_sum(root, 22)) # True: 5->4->11->2เส้นทางทั้งหมดจากรากถึงโหนดใบ
หากต้องการแจกแจงเส้นทางทั้งหมด ให้เก็บรายการเส้นทางที่กำลังสร้างไว้ เมื่อมีการเรียกซ้ำแต่ละครั้ง ให้เพิ่มค่าของโหนดปัจจุบันด้วย append เรียกซ้ำไปยังโหนดลูก จากนั้นใช้ pop เมื่อย้อนกลับจากการเรียก (ย้อนรอย) เมื่อถึงโหนดใบ ให้บันทึกสำเนาของเส้นทางปัจจุบันด้วย list(path) รูปแบบนี้ — เลือก เรียกซ้ำ และยกเลิกการเลือก — เป็นรากฐานของการย้อนรอยบนต้นไม้
def all_path_sums(root, target):
results = []
def dfs(node, path, remaining):
if not node:
return
path.append(node.val)
if not node.left and not node.right and remaining == node.val:
results.append(list(path)) # snapshot
else:
dfs(node.left, path, remaining - node.val)
dfs(node.right, path, remaining - node.val)
path.pop() # backtrack
dfs(root, [], target)
return results
root = TreeNode(5)
root.left = TreeNode(4)
root.right = TreeNode(8)
root.left.left = TreeNode(11)
root.left.left.right = TreeNode(2)
root.right.right = TreeNode(5)
print(all_path_sums(root, 22)) # [[5,4,11,2]]ผลรวมเส้นทาง III: เส้นทางใดก็ได้ โหนดใดก็ได้
ผลรวมเส้นทาง III (LeetCode #437) นับจำนวนเส้นทางที่มีผลรวมเท่ากับเป้าหมาย โดยเส้นทางสามารถเริ่มต้นและสิ้นสุดที่ใดก็ได้ (ไม่จำเป็นต้องเริ่มที่รากและสิ้นสุดที่โหนดใบ) วิธีแรงตรงใช้เวลา O(n²) โดยทำ DFS จากทุกโหนด ส่วนวิธีที่มีประสิทธิภาพสูงสุดซึ่งใช้เวลา O(n) ใช้ตารางแฮชของผลรวมคำนำหน้า: ติดตามผลรวมที่กำลังสะสม และนับว่าค่า current_sum - target เคยปรากฏมาก่อนกี่ครั้ง ซึ่งสอดคล้องกับแนวทางหาผลรวมของอาร์เรย์ย่อย
def path_sum_iii(root, target):
prefix_counts = {0: 1}
def dfs(node, running_sum):
if not node:
return 0
running_sum += node.val
count = prefix_counts.get(running_sum - target, 0)
prefix_counts[running_sum] = prefix_counts.get(running_sum, 0) + 1
count += dfs(node.left, running_sum)
count += dfs(node.right, running_sum)
prefix_counts[running_sum] -= 1 # backtrack
return count
return dfs(root, 0)
root = TreeNode(10)
root.left = TreeNode(5)
root.right = TreeNode(-3)
root.left.left = TreeNode(3)
root.left.right = TreeNode(2)
root.right.right = TreeNode(11)
root.left.left.left = TreeNode(3)
root.left.left.right = TreeNode(-2)
root.left.right.right = TreeNode(1)
print(path_sum_iii(root, 8)) # 3บรรพบุรุษร่วมที่ใกล้ที่สุดคืออะไร
บรรพบุรุษร่วมที่ใกล้ที่สุด (LCA) ของโหนด p และ q ในต้นไม้ทวิภาค คือโหนดที่อยู่ลึกที่สุดซึ่งมีทั้ง p และ q เป็นโหนดสืบทอด (โหนดหนึ่งสามารถเป็นโหนดสืบทอดของตัวเองได้) LCA ปรากฏในปัญหาอย่าง 'ระยะห่างระหว่างสองโหนด' 'เส้นทางระหว่างสองโหนด' และการค้นหาช่วงใน BST การทำความเข้าใจ LCA เป็นสิ่งจำเป็นสำหรับปัญหาเกี่ยวกับต้นไม้ระดับกลาง
# 3
# / \
# 5 1
# / \ / \
# 6 2 0 8
# / \
# 7 4
# LCA(5, 1) = 3 (root)
# LCA(5, 4) = 5 (p itself is ancestor of q)
# LCA(6, 4) = 5
# LCA(7, 4) = 2
# Key insight: the LCA is the node where p and q
# first 'split' into different subtrees.
print('LCA: deepest node that is ancestor of both p and q')อัลกอริทึม LCA แบบเรียกซ้ำ
วิธีแก้ปัญหา LCA แบบเรียกซ้ำที่กระชับจะส่งคืนโหนดแรกที่เป็น p หรือ q หรือเป็นโหนดที่มีทั้งสองโหนดอยู่ในต้นไม้ย่อยของตน หากโหนดปัจจุบันเป็น p หรือ q ให้ส่งคืนโหนดนั้น มิฉะนั้นให้เรียกซ้ำกับด้านซ้ายและด้านขวา หากทั้งสองด้านส่งคืนค่าที่ไม่ใช่ค่าว่าง โหนดปัจจุบันคือ LCA หากมีเพียงด้านเดียวที่ส่งคืนค่าที่ไม่ใช่ค่าว่าง ให้ส่งต่อค่านั้นขึ้นไป วิธีนี้ใช้เวลา O(n) และใช้พื้นที่ O(h)
def lowest_common_ancestor(root, p, q):
# Base case: empty or found one of the targets
if not root or root == p or root == q:
return root
# Search both subtrees
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
# If both sides found something, this node is the LCA
if left and right:
return root
# Otherwise, return whichever side found something
return left if left else right
root = TreeNode(3)
root.left = TreeNode(5)
root.right = TreeNode(1)
root.left.left = TreeNode(6)
root.left.right = TreeNode(2)
p, q = root.left, root.right # 5 and 1
lca = lowest_common_ancestor(root, p, q)
print(lca.val) # 3LCA เมื่อโหนดสามารถเป็นบรรพบุรุษของตัวเอง
กรณีพิเศษที่สำคัญคือ หาก p เป็นบรรพบุรุษของ q (หรือกลับกัน) LCA จะเป็น p เอง อัลกอริทึมแบบเรียกซ้ำจัดการกรณีนี้ได้โดยอัตโนมัติ — เมื่อไปถึง p ก็ส่งคืน p ทันที โดยไม่ตรวจสอบต้นไม้ย่อยของ p โหนดแม่จะเห็นว่าด้านหนึ่งส่งคืน p และอีกด้านส่งคืนค่าว่าง จึงส่งต่อ p ขึ้นไปในฐานะ LCA ควรตรวจสอบกรณีนี้ด้วยการทดสอบของคุณเสมอเมื่อเขียนโค้ด LCA
# Test case: p is ancestor of q
# Tree: 3 -> left=5 -> left=6
# LCA(5, 6) should be 5
root = TreeNode(3)
root.left = TreeNode(5)
root.left.left = TreeNode(6)
p = root.left # node 5
q = root.left.left # node 6
lca = lowest_common_ancestor(root, p, q)
print(lca.val) # 5 (p itself is the LCA)LCA ด้วยตัวชี้ไปยังโหนดแม่
หากแต่ละโหนดมีตัวชี้ไปยังโหนดแม่ ปัญหา LCA จะลดรูปเป็นปัญหา 'จุดตัดของรายการเชื่อมโยงสองรายการ' ให้รวบรวมบรรพบุรุษของ p ไว้ในเซต จากนั้นเลื่อนขึ้นจาก q จนกว่าจะพบโหนดที่อยู่ในเซตนั้น วิธีนี้ใช้เวลา O(h) และใช้พื้นที่ O(h) มักพบในการสัมภาษณ์ด้านการออกแบบระบบ ซึ่งคุณเป็นผู้ควบคุมโครงสร้างโหนดและสามารถเก็บการอ้างอิงไปยังโหนดแม่ได้
class NodeWithParent:
def __init__(self, val, parent=None):
self.val = val
self.parent = parent
self.left = None
self.right = None
def lca_with_parent(p, q):
ancestors = set()
# Collect all ancestors of p
node = p
while node:
ancestors.add(node)
node = node.parent
# Walk up from q until we hit a known ancestor
node = q
while node:
if node in ancestors:
return node
node = node.parent
return None
print('With parent pointers: O(h) time and space')LCA ในต้นไม้ค้นหาแบบทวิภาค
ใน BST การหา LCA ทำได้ง่ายกว่า เพราะคุณสมบัติด้านลำดับจะบอกว่าต้นไม้ย่อยใดมีแต่ละโหนดอยู่ หากทั้ง p และ q มีค่าน้อยกว่าโหนดปัจจุบัน LCA จะอยู่ในต้นไม้ย่อยด้านซ้าย หากทั้งคู่มีค่ามากกว่า LCA จะอยู่ในต้นไม้ย่อยด้านขวา มิฉะนั้น โหนดปัจจุบันจะแบ่งโหนดทั้งสองออกจากกัน จึงเป็น LCA วิธีนี้ลดเวลาในการแก้ปัญหาเหลือ O(log n) สำหรับ BST ที่สมดุล
def lca_bst(root, p, q):
if not root:
return None
if p.val < root.val and q.val < root.val:
return lca_bst(root.left, p, q) # both in left
if p.val > root.val and q.val > root.val:
return lca_bst(root.right, p, q) # both in right
return root # split point = LCA
# Iterative BST LCA (no recursion overhead):
def lca_bst_iter(root, p, q):
while root:
if p.val < root.val and q.val < root.val:
root = root.left
elif p.val > root.val and q.val > root.val:
root = root.right
else:
return root
return None
print('BST LCA: O(log n) for balanced trees')ระยะห่างระหว่างสองโหนด
ระยะห่างระหว่างสองโหนดในต้นไม้เท่ากับจำนวนเส้นเชื่อมบนเส้นทางที่เชื่อมโหนดทั้งสองเข้าด้วยกัน ค่านี้คำนวณได้โดยตรงจาก LCA: distance(p, q) = depth(p) + depth(q) - 2 * depth(LCA(p,q)) ให้หา LCA ก่อน จากนั้นนับระดับความลึกของแต่ละโหนด ด้วยฟังก์ชันผู้ช่วยที่เหมาะสม วิธีนี้ใช้เวลา O(n) และใช้พื้นที่ O(h)
def find_depth(root, target, depth=0):
if not root:
return -1
if root == target:
return depth
left = find_depth(root.left, target, depth + 1)
if left != -1:
return left
return find_depth(root.right, target, depth + 1)
def node_distance(root, p, q):
lca = lowest_common_ancestor(root, p, q)
# depth from LCA to p and q
dp = find_depth(lca, p)
dq = find_depth(lca, q)
return dp + dq
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print(node_distance(root, root.left.left, root.left.right)) # 2เส้นทางผลรวมสูงสุดจากรากถึงโหนดใบ
เส้นทางผลรวมสูงสุดจากรากถึงโหนดใบจะติดตามผลรวมที่กำลังสะสมตั้งแต่รากถึงโหนดปัจจุบัน เมื่อถึงโหนดใบ ให้เปรียบเทียบกับค่าสูงสุดส่วนกลาง นี่คือ DFS แบบพรีออร์เดอร์ที่ส่งผลรวมของเส้นทางปัจจุบันเป็นพารามิเตอร์ แตกต่างจากปัญหาผลรวมเส้นทางสูงสุดทั่วไป ตรงที่วิธีนี้จำกัดอยู่เฉพาะเส้นทางจากรากถึงโหนดใบ จึงเรียบง่ายกว่า — ไม่จำเป็นต้องพิจารณาเส้นทางระหว่างโหนดใด ๆ
def max_root_to_leaf_sum(root):
if not root:
return float('-inf')
best = [float('-inf')]
def dfs(node, running):
running += node.val
if not node.left and not node.right: # leaf
best[0] = max(best[0], running)
return
if node.left:
dfs(node.left, running)
if node.right:
dfs(node.right, running)
dfs(root, 0)
return best[0]
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print(max_root_to_leaf_sum(root)) # 1+2+5 = 8ผลรวมตัวเลขจากรากถึงโหนดใบ
ผลรวมตัวเลขจากรากถึงโหนดใบ (LeetCode #129) มองเส้นทางจากรากถึงโหนดใบแต่ละเส้นเป็นเลขฐานสิบ (เช่น เส้นทาง 1→2→3 แทนเลข 123) แล้วให้หาผลรวมของตัวเลขเหล่านั้น สร้างตัวเลขโดยส่ง current_number * 10 + node.val ลงไปในการเรียกซ้ำ เมื่อถึงโหนดใบ ให้บวกตัวเลขที่สร้างเสร็จแล้วเข้ากับผลรวมทั้งหมด นี่เป็นตัวอย่างที่ชัดเจนของ DFS แบบพรีออร์เดอร์ที่ส่งสถานะสะสมลงไป
def sum_numbers(root):
def dfs(node, num):
if not node:
return 0
num = num * 10 + node.val
if not node.left and not node.right: # leaf
return num
return dfs(node.left, num) + dfs(node.right, num)
return dfs(root, 0)
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
print(sum_numbers(root)) # 12 + 13 = 25
root2 = TreeNode(4)
root2.left = TreeNode(9)
root2.right = TreeNode(0)
root2.left.left = TreeNode(5)
root2.left.right = TreeNode(1)
print(sum_numbers(root2)) # 495 + 491 + 40 = 1026ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้: รูปแบบต่าง ๆ ของผลรวมเส้นทาง (จากรากถึงโหนดใบ ทุกเส้นทาง และผลรวมเส้นทาง III ด้วยผลรวมคำนำหน้า) บรรพบุรุษร่วมที่ใกล้ที่สุดโดยใช้การแยกแบบเรียกซ้ำที่กระชับ และ LCA ใน BST ที่ใช้เวลา O(log n) ด้วยคุณสมบัติด้านลำดับ บทถัดไปเราจะเริ่มเรียนรู้ต้นไม้ค้นหาแบบทวิภาคด้วยการดำเนินการแทรกและค้นหา
คำถามที่พบบ่อย
บทเรียน “ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด”
แก้โจทย์ผลรวมเส้นทางจากรากถึงใบ ผลรวมของทุกเส้นทาง และบรรพบุรุษร่วมที่ใกล้ที่สุดสำหรับต้นไม้ทวิภาคทั่วไปด้วยการไล่เรียกซ้ำ คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- คลาส TreeNode และ BFS แบบเรียงตามระดับ
- DFS แบบลำดับกลาง ก่อน และหลัง
- เส้นผ่านศูนย์กลาง ความสูง และต้นไม้สมดุล
- ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด