แหล่งอ้างอิง: https://cses.fi/book/book.pdf
นิยามทางกราฟ:
ต้นไม้ (Tree) คือ กราฟเชื่อมโยง (Connected Graph) ที่ **ไม่มีวงจร (Acyclic)**
คุณสมบัติสำคัญ:
รายละเอียดเพิ่มเติม: https://roadmap.sh/datastructures-and-algorithms
"โหนดแต่ละโหนด สามารถมองเป็นรากของต้นไม้ย่อย (Subtree) ได้เสมอ"
ในต้นไม้ที่มีราก (Rooted Tree) ต้นไม้ย่อยของโหนด \( v \) ประกอบด้วยโหนด \( v \) เองและทายาท (Descendants) ทั้งหมดของมัน แนวคิดนี้ช่วยให้เราเขียนอัลกอริทึมด้วย **Recursion** ได้อย่างมีประสิทธิภาพ
อ้างอิง: https://cses.fi/book/book.pdf (Chapter 14)
เป็นวิธีที่นิยมที่สุดในการเก็บ Tree ในการแข่งขัน:
vector<int> adj[N];
// เพิ่มเส้นเชื่อมระหว่าง u และ v
adj[u].push_back(v);
adj[v].push_back(u);
นิยาม:
เป็นต้นไม้ที่มีรากซึ่งแต่ละโหนดจะมีลูกได้ **ไม่เกิน 2 โหนด** (ซ้ายและขวา)
Binary Trees เป็นพื้นฐานของโครงสร้างขั้นสูงอย่าง Segment Tree และ Binary Search Tree (BST)
ในต้นไม้คู่ที่สมบูรณ์ (Complete Binary Tree) เราสามารถใช้ Index ของอาเรย์แทนตำแหน่งได้:
**เทคนิคนี้ใช้บ่อยมากในการเขียน Binary Heap และ Segment Tree**
เพิ่มเติม: https://www.geeksforgeeks.org/tree-data-structure/
เนื่องจากต้นไม้ไม่มีวงจร การท่องไปในโหนดต่างๆ จึงง่ายกว่ากราฟทั่วไป เพราะเราไม่จำเป็นต้องกังวลว่าจะกลับมาที่โหนดเดิมจากทิศทางอื่น
Depth-First Search (DFS)
พยายามลงไปให้ลึกที่สุดก่อนถอยกลับ
Breadth-First Search (BFS)
ท่องไปทีละระดับความลึก (Level-order)
// s = โหนดปัจจุบัน, e = โหนดพ่อ (เพื่อไม่ให้เดินย้อนกลับ)
void dfs(int s, int e) {
// 1. จัดการข้อมูลของโหนด s (Pre-order)
for (auto u : adj[s]) {
if (u == e) continue;
dfs(u, s);
}
// 2. จัดการข้อมูลหลังลูกๆ ทำงานเสร็จ (Post-order)
}
**หมายเหตุ**: ในต้นไม้ที่มีราก เราไม่จำเป็นต้องใช้ array `visited` ก็ได้ เพียงแค่จำโหนดพ่อ (Parent) ไว้
**ความท้าทาย**: ถ้าเรารู้ลำดับ Pre-order และ In-order เราจะสามารถสร้าง Tree เดิมกลับมาได้อย่างแม่นยำ!
ข้อมูลเพิ่มเติม: https://atcoder.jp/contests/dp (Educational DP Contest)
จำนวนเส้นเชื่อมจาก Root มายังโหนดปัจจุบัน โดยความลึกของ Root คือ \( 0 \)
เส้นทางที่ยาวที่สุดจากโหนดปัจจุบันลงไปจนถึง Leaf โดยความสูงของ Leaf คือ \( 0 \)
อ้างอิง: https://cses.fi/book/book.pdf
void calculate_depth(int s, int e, int d) {
depth[s] = d;
for (auto u : adj[s]) {
if (u == e) continue;
calculate_depth(u, s, d + 1);
}
}
พารามิเตอร์ \( d \) จะเพิ่มขึ้นเรื่อยๆ เมื่อเราเดินลงไปยังโหนดลูก ทำให้เราบันทึกระยะห่างจาก Root ได้ในเวลา \( O(n) \)
ในกราฟทั่วไป **Degree** คือจำนวนเส้นเชื่อมที่ติดกับโหนดนั้น แต่ใน Rooted Tree เรามักจะสนใจ:
"โหนดที่เป็น Leaf คือโหนดที่มีลูกเป็น \( 0 \) หรือมี Degree รวมเป็น \( 1 \) ในกรณีต้นไม้ไม่มีราก"
นิยาม: \( count(s) \) คือจำนวนโหนดทั้งหมดใน Subtree ที่มี \( s \) เป็นราก
สูตรการคำนวณแบบเรียกซ้ำ:
\( count(s) = 1 + \sum_{u \in children(s)} count(u) \)
เทคนิคนี้ใช้ **Post-order logic**: เราต้องให้ลูกๆ คำนวณขนาดของตัวเองให้เสร็จก่อน แล้วจึงนำมารวมกันเพื่อบอกขนาดของโหนดพ่อ
ศึกษาเพิ่มเติม: https://www.geeksforgeeks.org/tree-data-structure/
วิธีที่ง่ายที่สุดในการเก็บโครงสร้างต้นไม้ที่มีราก คือการเก็บเพียง **"โหนดพ่อ"** ของแต่ละโหนด
ใช้ **Queue** ในการจัดการลำดับการเยี่ยมชม โดยเริ่มจาก Root และขยายไปยังโหนดลูกทีละชั้น
ขั้นตอน:
1. ใส่ Root ใน Queue
2. ดึงโหนดออกมา และใส่ลูกๆ ทั้งหมดของมันลงใน Queue
3. ทำซ้ำจน Queue ว่าง
"ระยะทางที่ยาวที่สุดระหว่างโหนด 2 โหนดใดๆ"
• ในต้นไม้อาจมีโหนดหลายคู่ที่มีระยะทางเท่ากับ Diameter
• เป็นหนึ่งในปัญหาคลาสสิกที่สามารถแก้ได้ด้วย DFS 2 ครั้ง หรือ DP บนต้นไม้
• การหาเส้นทางนี้ช่วยให้เข้าใจโครงสร้างความกว้างของต้นไม้ได้ดีขึ้น
รายละเอียดอัลกอริทึม: https://roadmap.sh/datastructures-and-algorithms
ความจริงที่สำคัญ:
สำหรับโหนด \( u \) และ \( v \) ใดๆ ในต้นไม้ จะมี **เส้นทาง (Path) เพียงเส้นเดียวเท่านั้น** ที่เชื่อมระหว่างกัน
ระยะทาง \( dist(u, v) \) คือจำนวนเส้นเชื่อมบนเส้นทางที่สั้นที่สุดระหว่าง \( u \) และ \( v \)
*LCA (Lowest Common Ancestor) คือ บรรพบุรุษร่วมที่อยู่ลึกที่สุดของโหนดทั้งสอง
| คุณสมบัติ | Tree | General Graph |
|---|---|---|
| จำนวนเส้นเชื่อม | \( n-1 \) | \( 0 \) ถึง \( \frac{n(n-1)}{2} \) |
| วงจร (Cycles) | ไม่มีเด็ดขาด | มีหรือไม่มีก็ได้ |
| การเชื่อมโยง | ต้องเชื่อมถึงกันหมด | อาจมีโหนดที่แยกตัว (Disconnected) |
| ความลึก (Stack) | DFS ทำงานง่ายกว่ามาก | ต้องระวัง Infinite Loop |
อ้างอิง: https://cses.fi/book/book.pdf
ขั้นตอนที่เรียบง่ายแต่ทรงพลัง:
*หมายเหตุ: อัลกอริทึมนี้ใช้ได้เฉพาะกับต้นไม้ที่ไม่มีน้ำหนักเส้นเชื่อมเป็นลบ
ศึกษาการพิสูจน์: https://cses.fi/book/book.pdf
นิยาม LCA(u, v):
คือโหนดที่เป็นบรรพบุรุษ (Ancestor) ของทั้ง \( u \) และ \( v \) และมีความลึก (Depth) **มากที่สุด**
ใช้หาระยะทางระหว่างโหนด, ตรวจสอบความสัมพันธ์ในครอบครัวของโหนด, และแก้ปัญหาเส้นทางบนต้นไม้
Naive: \( O(n) \)
Binary Lifting: \( O(\log n) \)
dist(u, v) = depth[u] + depth[v] - 2 \cdot depth[LCA(u, v)]
เหตุผลคือ เมื่อเราเดินทางจาก \( u \) ไปยัง \( v \) เราต้องขึ้นไปหาจุดเชื่อมต่อที่เตี้ยที่สุด (LCA) แล้วค่อยลงไปหาอีกโหนดหนึ่ง การลบส่วนที่ซ้ำซ้อนของเส้นทางบรรพบุรุษออกทำให้ได้ระยะทางที่แท้จริง
เพิ่มเติมเรื่อง Binary Lifting: https://usaco.guide/gold/LCA
เป็น Binary Tree พิเศษที่มีการจัดเรียงข้อมูลอย่างเป็นระบบ:
"BST ช่วยให้เราค้นหาข้อมูล (Search) ได้ในเวลา \( O(\log n) \) หากต้นไม้มีความสมดุล"
"In-order traversal of BST results in sorted order"
เมื่อเราท่องไปใน BST ด้วยลำดับ (ซ้าย -> ราก -> ขวา) เราจะได้ข้อมูลที่เรียงลำดับจาก **น้อยไปมาก** เสมอ
ความสูงของต้นไม้คงอยู่ที่ \( O(\log n) \) ทำให้ทุกการดำเนินการ (Search, Insert, Delete) ทำงานได้รวดเร็ว
ต้นไม้กลายสภาพเป็น Linked List ทำให้ความสูงกลายเป็น \( O(n) \) และประสิทธิภาพตกลงอย่างมหาศาล
"เราใช้โครงสร้างอย่าง AVL Tree หรือ Red-Black Tree เพื่อรักษาความสมดุลนี้"
เก็บเพียงรายการของเส้นเชื่อมทั้งหมดที่มีในต้นไม้
นิยาม:
ป่า คือ กราฟที่ **ไม่มีวงจร (Acyclic)** แต่ไม่จำเป็นต้องเชื่อมต่อกัน (Disconnected)
หลายปัญหาบนต้นไม้สามารถแก้ได้ด้วย DP โดยใช้โครงสร้างแบบเรียกซ้ำ:
"เมื่อ \( u_i \) คือลูกทั้งหมดของ \( s \). เราจะรอให้คำตอบของลูกๆ พร้อมก่อน (Post-order) แล้วจึงสรุปผลที่โหนดแม่"
ตัวอย่าง: การหาขนาด Subtree, การหาค่าสูงสุดในเส้นทาง, การเลือกโหนดที่ไม่ติดกัน (Independent Set)
เข้าใจนิยาม \( n \) โหนด \( n-1 \) เส้นเชื่อม และความเป็นกราฟที่ไม่มีวงจร
Adjacency List คือหัวใจหลัก และ Parent Array คือเครื่องมือเดินขึ้นหาบรรพบุรุษ
DFS และ BFS คือรากฐานของทุกอัลกอริทึมบนต้นไม้
LCA, Distance และ Diameter ช่วยให้เราเข้าใจความกว้างและลึกของต้นไม้
Coming up next (13:00):
เราจะนำ DFS และ BFS มาใช้แก้ปัญหาที่ซับซ้อนขึ้น เช่น การหาจุดวิกฤต, การจัดการกับข้อมูลตามระดับชั้น และการนำ Traversal ไปประยุกต์กับโจทย์โอลิมปิก