นากต้นฉบับสร้างเครือข่ายโหนดชุดใหม่คู่กับต้นฉบับโดยมีเส้นเชื่อมครบและไม่ปะปนกัน

LeetCode #133 Clone Graph: คัดลอกวงจรด้วยตาราง old-to-new

พี่โชว์Engineering

LeetCode #133 ให้ reference ไปยังโหนดหนึ่งใน undirected connected graph แล้วขอ deep copy ทั้งกราฟ ปัญหาไม่ได้อยู่ที่สร้างโหนดใหม่ แต่อยู่ที่วงจร: ถ้าเดิน A→B→A แล้วสร้างใหม่ทุกครั้ง โปรแกรมจะวนไม่จบหรือได้สำเนา A หลายตัว

หัวใจจึงเป็นตาราง ต้นฉบับ -> สำเนา เหมือนย้ายออฟฟิศแล้วแจกบัตรพนักงานใหม่ พอพบคนเดิมอีกครั้งต้องใช้บัตรใหม่ใบเดิม ไม่ออกบัตรซ้ำ จากนั้นค่อยเชื่อมความสัมพันธ์ระหว่างสำเนาเท่านั้น

วิธี 1: DFS แบบ recursive

เมื่อพบโหนดใหม่ สร้างสำเนาและบันทึกลง map ก่อน เดินเพื่อนบ้าน จากนั้น clone เพื่อนบ้านแต่ละตัว recursively ใช้เวลา O(V+E) และ map O(V) ส่วน call stack สูงสุด O(V) โค้ดกระชับและเป็น best default ภายใต้ constraints ของโจทย์ ที่มีโหนดไม่มาก เพราะสะท้อนนิยามกราฟโดยตรง

วิธี 2: BFS ด้วย queue

สร้างสำเนาโหนดเริ่ม บันทึก map แล้วใช้ queue เดินเป็นชั้น ถ้าเพื่อนบ้านยังไม่มีสำเนาจึงสร้างและ enqueue จากนั้นต่อ edge ทุกเส้น เวลา O(V+E), map O(V) และ queue สูงสุด O(V) เช่นกัน ข้อดีคือไม่เสี่ยง recursion limit เมื่อกราฟลึกมาก และ debug ตามลำดับชั้นง่ายกว่า

from __future__ import annotations

from collections import deque
import unittest


class Node:
    def __init__(self, val: int, neighbors: list["Node"] | None = None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []


def clone_dfs(node: Node | None) -> Node | None:
    copies: dict[Node, Node] = {}

    def clone(current: Node) -> Node:
        if current in copies:
            return copies[current]
        copy = Node(current.val)
        copies[current] = copy
        copy.neighbors = [clone(neighbor) for neighbor in current.neighbors]
        return copy

    return None if node is None else clone(node)


def clone_bfs(node: Node | None) -> Node | None:
    if node is None:
        return None
    copies = {node: Node(node.val)}
    pending = deque([node])
    while pending:
        current = pending.popleft()
        for neighbor in current.neighbors:
            if neighbor not in copies:
                copies[neighbor] = Node(neighbor.val)
                pending.append(neighbor)
            copies[current].neighbors.append(copies[neighbor])
    return copies[node]


def snapshot(start: Node | None) -> dict[int, list[int]]:
    if start is None:
        return {}
    result: dict[int, list[int]] = {}
    pending = deque([start])
    seen = {start}
    while pending:
        node = pending.popleft()
        result[node.val] = sorted(neighbor.val for neighbor in node.neighbors)
        for neighbor in node.neighbors:
            if neighbor not in seen:
                seen.add(neighbor)
                pending.append(neighbor)
    return result


class CloneGraphTests(unittest.TestCase):
    def graph_with_cycle_and_self_loop(self) -> Node:
        one, two, three = Node(1), Node(2), Node(3)
        one.neighbors = [two, three]
        two.neighbors = [one, three]
        three.neighbors = [one, two, three]
        return one

    def test_none(self):
        self.assertIsNone(clone_dfs(None))
        self.assertIsNone(clone_bfs(None))

    def test_both_methods_make_deep_copy(self):
        for solve in (clone_dfs, clone_bfs):
            original = self.graph_with_cycle_and_self_loop()
            cloned = solve(original)
            self.assertEqual(snapshot(cloned), snapshot(original))
            self.assertIsNot(cloned, original)
            self.assertTrue(all(a is not b for a, b in zip(cloned.neighbors, original.neighbors)))
            cloned.val = 99
            self.assertEqual(original.val, 1)


if __name__ == "__main__":
    unittest.main()

Decision table

เงื่อนไขRecursive DFSIterative BFS
Input เล็ก / readabilityสั้นและตรงนิยามมี queue เพิ่ม
กราฟลึกมากเสี่ยง recursion limitปลอดภัยกว่า
Memory จำกัดmap + stack O(V)map + queue O(V)
โครงสร้างคล้าย treeเหมาะเหมาะ
Streaming inputทำไม่ได้จาก Node API; ต้องมี graph ครบทำไม่ได้เช่นกัน
ลำดับเพื่อนบ้านสำคัญรักษาตาม listรักษาตาม list
ห้ามแก้ inputได้ได้

Edge cases และ trade-offs

Input None ต้องคืน None; โหนดเดียวอาจไม่มีเพื่อนบ้านหรือมี self-loop; ค่า val ไม่ควรใช้เป็น key แทน object identity เว้นแต่สัญญารับประกัน unique จริง เพราะสองโหนดอาจมีค่าเท่ากันในระบบทั่วไป บันทึกสำเนาลง map ก่อน recursion เสมอ ไม่เช่นนั้น self-loop จะวนทันที

Tests ไม่ได้เช็กแค่ adjacency เท่ากัน แต่เช็กว่า object สำเนาไม่ใช่ object เดิมและแก้สำเนาแล้วต้นฉบับไม่เปลี่ยน นี่คือความหมายของ deep copy ที่สำคัญกว่าการพิมพ์ค่าเหมือนกัน

Recap

  • ใช้ object ต้นฉบับเป็น key ของตาราง old-to-new
  • บันทึกสำเนาก่อนเดินเพื่อนบ้านเพื่อหยุดวงจร
  • ใช้ DFS เป็น default เมื่อกราฟไม่ลึกเกิน stack
  • เปลี่ยนเป็น BFS เมื่อกราฟลึกหรืออยากควบคุม traversal เอง