Notes & software courses · Free to learn
Aph's Blog
On this page

ข้อ 46 · LC399 Evaluate Division (คำนวณการหาร) 🟡

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

weighted graph แต่ละตัวแปรเป็น node สมการเป็น edge ที่มีค่า หา x/y ด้วย DFS คูณน้ำหนักตลอดทาง

โจทย์ (LC399): กำหนด array ของคู่ตัวแปร equations และ array ของจำนวนจริง values โดย equations[i] = [Ai, Bi] และ values[i] แทนสมการ Ai / Bi = values[i] กำหนด queries มาด้วย โดย queries[j] = [Cj, Dj] แทนคำถามที่ j ว่า Cj / Dj มีค่าเท่าไร ให้ return คำตอบของทุก query ถ้า query ไหนหาคำตอบไม่ได้ (ตัวแปรไม่รู้จัก หรือไม่เชื่อมถึงกัน) ให้ตอบ -1.0 สำหรับ query นั้น

Example 1
Input:
equations = [["a","b"],["b","c"]], values = [2.0, 3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
Output:
[6.0, 0.5, -1.0, 1.0, -1.0]
Explanation:
a/c = a/b × b/c = 2 × 3 = 6.0 · b/a คือส่วนกลับ = 0.5 · a/e หาไม่ได้เพราะไม่มี e · a/a = 1.0 เพราะ a รู้จัก · x/x = -1.0 เพราะไม่รู้จัก x เลย
Example 2
Input:
equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"]]
Output:
[0.5, 2.0]
Explanation:
a/b = 0.5 ตามสมการโดยตรง ส่วน b/a คือส่วนกลับของ a/b เท่ากับ 1/0.5 = 2.0
Constraints (ข้อจำกัด)
  • 1 <= equations.length <= 20
  • 0.0 < values[i] <= 20.0
  • 1 <= queries.length <= 20

แนวทาง — ต้องใช้อะไร & คิดยังไง

ใช้ DFS บน weighted graph (กราฟถ่วงน้ำหนัก) คือ edge (เส้นเชื่อม) มีตัวเลขกำกับ ไอเดียคือแต่ละตัวแปรเป็น node (โหนด) สมการ a/b = 2.0 บอกว่าจาก a เดินไป b คูณ 2.0 และเพราะ b/a เท่ากับ 1/(a/b) เราจึงเพิ่ม edge ย้อนกลับจาก b ไป a คูณ 1/2.0 ด้วย ทำให้เดินได้สองทาง

การหาคำตอบ x/y คือ traverse (เดินไล่) จาก x ไป y แล้วคูณ weight (น้ำหนัก) ของ edge ที่ผ่านทั้งหมดสะสมกันไป DFS ที่นี่ต่างจากข้อก่อน ๆ ตรงที่มันต้อง return (คืนค่า) ผลคูณสะสมกลับขึ้นมา ไม่ใช่แค่ mark เมื่อเดินไปเจอ dst เราคืน 1.0 แล้วระหว่างถอย recursion (การเรียกตัวเอง) กลับ แต่ละชั้นคูณ weight ของ edge ตัวเองเข้าไป

  1. สร้าง graph graph[a][b] = val และ graph[b][a] = 1/val สำหรับทุกสมการ
  2. เขียน dfs(src, dst, visited) ถ้า src หรือ dst ไม่มีใน graph คืน -1.0 (ไม่รู้จัก)
  3. ถ้า src == dst คืน 1.0 (เจอปลายทางแล้ว หรือ x/x)
  4. mark src แล้ว iterate (วน) เพื่อนบ้าน (neighbor) (nbr, weight) ที่ยังไม่ visited
  5. เรียก dfs(nbr, dst) ถ้าผลไม่ใช่ -1.0 (มีทางถึง) คืน weight × result
  6. ถ้าลองทุกทางแล้วไปไม่ถึง คืน -1.0 ทำ dfs แยกทีละ query
จุดพลาดที่พบบ่อย

ลืมเช็คว่าตัวแปรรู้จักก่อนเช็ค src == dst ทำให้ x/x ตอบ 1.0 ทั้งที่ x ไม่มีในสมการ (ต้องตอบ -1.0) โค้ดจึงเช็ค src not in graph ก่อนแล้วค่อยเช็ค src == dst

▶ เฉลยละเอียด (ลองเองก่อนนะ)
เฉลย (Python) — โค้ดนี้รันได้จริงpython
from collections import defaultdict

def calc_equation(equations, values, queries):
    graph = defaultdict(dict)
    for (a, b), val in zip(equations, values):
        graph[a][b] = val          # a/b = val
        graph[b][a] = 1 / val      # b/a = 1/val

    def dfs(src, dst, visited):
        if src not in graph or dst not in graph:
            return -1.0            # มีตัวแปรที่ไม่รู้จัก
        if src == dst:
            return 1.0             # x/x = 1 (ต้องรู้จัก x ด้วย)
        visited.add(src)
        for nbr, weight in graph[src].items():
            if nbr not in visited:
                result = dfs(nbr, dst, visited)
                if result != -1.0:        # เจอทางถึง dst
                    return weight * result  # คูณน้ำหนักสะสม
        return -1.0                # ลองทุกทางแล้วไปไม่ถึง

    answers = []
    for a, b in queries:
        answers.append(dfs(a, b, set()))
    return answers

eq = [["a", "b"], ["b", "c"]]
vals = [2.0, 3.0]
q = [["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"]]
print(calc_equation(eq, vals, q))  # [6.0, 0.5, -1.0, 1.0, -1.0]
Output
[6.0, 0.5, -1.0, 1.0, -1.0]

ข้อนี้สอน weighted graph (กราฟถ่วงน้ำหนัก) คือ edge มีตัวเลขกำกับ ไอเดียคือแต่ละตัวแปรเป็น node สมการ a/b = 2.0 บอกว่าจาก a เดินไป b คูณ 2.0 และเพราะ b/a = 1/(a/b) เราจึงเพิ่ม edge ย้อนกลับจาก b ไป a คูณ 1/2.0 ด้วย ทำให้เดินได้สองทาง การหาคำตอบ x/y คือ traverse จาก x ไป y แล้วคูณ weight ของ edge ที่ผ่านทั้งหมดสะสมกันไป

DFS ที่นี่ต่างจากข้อก่อน ๆ ตรงที่มันต้อง return ผลคูณสะสมกลับขึ้นมา ไม่ใช่แค่ mark เมื่อเดินไปเจอ dst เราคืน 1.0 แล้วระหว่างถอย recursion กลับ แต่ละชั้นคูณ weight ของ edge ตัวเองเข้าไป ผลลัพธ์ที่โผล่กลับมาถึงจุดเริ่มจึงเป็นผลคูณตลอดเส้นทางพอดี ถ้าลองทุกเพื่อนบ้านแล้วไม่มีทางไหนถึง dst ก็คืน -1.0

edge case ที่ต้องระวังคือ ตัวแปรที่ไม่มีในสมการเลย (เช็ค src not in graph หรือ dst not in graph คืน -1.0) และกรณี a/a ที่ต้องคืน 1.0 เฉพาะเมื่อ a รู้จัก · Time O(Q × (V + E)) แต่ละ query ทำ DFS หนึ่งครั้ง Q คือจำนวน query · Space O(V + E) จาก graph และ visited

💡 สรุป pattern

ความสัมพันธ์เชิงอัตราส่วน / การแปลงหน่วยต่อเนื่อง มองเป็น weighted graph แล้ว DFS สะสมผลคูณตลอดเส้นทาง อย่าลืม edge ย้อนกลับที่เป็นส่วนกลับของ weight