0Pricing
DSA Interview Prep · บทเรียน

ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด

แก้โจทย์ผลรวมเส้นทางจากรากถึงใบ ผลรวมของทุกเส้นทาง และบรรพบุรุษร่วมที่ใกล้ที่สุดสำหรับต้นไม้ทวิภาคทั่วไปด้วยการไล่เรียกซ้ำ

ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด เป็นบทเรียน 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)  # 3

LCA เมื่อโหนดสามารถเป็นบรรพบุรุษของตัวเอง

กรณีพิเศษที่สำคัญคือ หาก 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. คลาส TreeNode และ BFS แบบเรียงตามระดับ
  2. DFS แบบลำดับกลาง ก่อน และหลัง
  3. เส้นผ่านศูนย์กลาง ความสูง และต้นไม้สมดุล
  4. ผลรวมเส้นทางและบรรพบุรุษร่วมที่ใกล้ที่สุด
← กลับไปที่ DSA Interview Prep