DFS Applications
การหาขนาด Subtree, ความสูง และ Dynamic Programming บนต้นไม้
BFS & Distance
การหาระยะทางที่สั้นที่สุดและลำดับเลเยอร์ (Level-order)
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
**ไม่มีวงจร (No Cycles)**: เราไม่จำเป็นต้องกังวลว่าจะวนกลับมาที่เดิมผ่านเส้นทางอื่น ทำให้การจัดการสถานะ `visited` ง่ายขึ้น
**เส้นทางเดียว (Unique Path)**: มีเพียงเส้นทางเดียวที่เชื่อมระหว่าง 2 โหนดใดๆ เสมอ
**โครงสร้างเรียกซ้ำ (Recursive)**: ต้นไม้มีคุณสมบัติ Self-similarity ซึ่งเหมาะมากกับการใช้ DFS ร่วมกับสมการเวียนเกิด
อ่านเพิ่มเติม: https://roadmap.sh/datastructures-and-algorithms
โจทย์: ในโหนด \( s \) ใดๆ มีโหนดอยู่ภายใต้โหนดนี้ทั้งหมดกี่โหนด?
เราใช้ DFS ในการคำนวณโดยอาศัยแนวคิด **Post-order**:
\( count(s) = 1 + \sum_{u \in children(s)} count(u) \)
อ้างอิงอัลกอริทึม: https://cses.fi/book/book.pdf (Chapter 14)
โครงสร้างต้นไม้ (ตัวอย่าง):
ให้ 1 เป็น Root โดยมีเส้นเชื่อมดังนี้
1-2, 1-3, 1-4, 1-5, 2-6, 4-7, 4-8, 4-9
ผลลัพธ์ที่ต้องการ:
ขนาด subtree ของโหนดต่างๆ (count[s])
เริ่มต้นที่โหนดใบ (Leaf Nodes) อัลกอริทึมจะลงไปลึกที่สุดจนเจอโหนดที่ไม่มีลูก (เช่น 6, 3, 7, 8, 9) และกำหนดให้ count[leaf] = 1
**Recursive Step:** เมื่อกลับขึ้นมาจากโหนดลูก (Post-order) โหนดพ่อจะบวกขนาดของโหนดลูกเข้าไปในตัวเอง
เช่น โหนด 4 มีลูกคือ 7, 8, 9 ซึ่งแต่ละโหนดมีขนาด 1 ดังนี้
count[6] = 1 (ตัวเอง) + count[7] + count[8] + count[9]
count[6] = 1 + 1 + 1 + 1 = 4
**การหา Root:** ทำซ้ำจนถึงโหนด 1
count[10] = 1 + count[11] + count[12] + count[6] + count[13]
count[10] = 1 + 2 + 1 + 4 + 1 = 9
โจทย์: จงนับจำนวนโหนดที่มีค่าเท่ากับ \( x \) ภายใน Subtree ของ \( s \)
แนวทางแก้ปัญหา (Offline Algorithm)
อ้างอิงเทคนิค: Chapter 18.4 (Offline Algorithms)
ส่งค่า \( depth + 1 \) ลงไปพร้อมกับการเรียกฟังก์ชันลูก เพื่อระบุระดับของโหนด
\( height(s) = \max(height(u)) + 1 \)
เมื่อลูกๆ ส่งคำตอบที่สูงที่สุดกลับขึ้นมาหาพ่อ
นิยาม: ระยะทางที่ไกลที่สุดระหว่างโหนดสองโหนดใดๆ ในต้นไม้
ตัวอย่าง Input:
7 โหนด, เส้นเชื่อม:
1-2, 1-3, 1-4, 2-5, 2-6, 4-7
ตัวอย่าง Output:
เส้นผ่านศูนย์กลาง = 4
(เช่น ระยะทางจาก 5 ถึง 7)
แนวทางการแก้ปัญหาด้วย Two-DFS Algorithm:
อ้างอิง: Competitive Programmer's Handbook (Chapter 14.2)
Diameter Algorithm (C++)
Time Complexity: \( O(n) \)**ทำไมถึงใช้ได้?** โหนดที่ไกลที่สุดจากโหนดใดๆ มักจะเป็นจุดปลายจุดหนึ่งของเส้นผ่านศูนย์กลางเสมอ ดังนั้นการหาจุดที่ไกลที่สุดสองครั้งจึงการันตีการเจอเส้นทางที่ยาวที่สุดในต้นไม้
"Tree DP คือการทำ DFS ที่มีการจดจำผลลัพธ์"
เรามักจะคำนวณค่าสถานะ (State) ของโหนดพ่อ โดยอิงจากค่าสถานะของโหนดลูก (Children states) ซึ่งทำได้โดยการใช้ DFS ท่องโหนดไปจนถึงใบ (Leaf) แล้วนำผลลัพธ์สะสมขึ้นมาเรื่อยๆ
ตัวอย่างปัญหา:
รายละเอียดเพิ่มเติม: https://atcoder.jp/contests/dp
โจทย์: จงเลือกโหนดจำนวนมากที่สุดในต้นไม้ โดยห้ามเลือกโหนดที่ติดกัน (มีเส้นเชื่อมถึงกัน)
แนวคิดการแบ่งสถานะ (States):
เรากำหนดสถานะให้แต่ละโหนด \( v \) สองแบบ :
สูตรความสัมพันธ์ (Transitions) :
อ้างอิงอัลกอริทึม: Dynamic Programming on Trees (Chapter 14 )
Maximum Independent Set on Tree
Time Complexity: \( O(n) \)
💡 ผลลัพธ์สุดท้าย:
หลังจากเรียก `solve(root, 0)` คำตอบที่ได้คือ `max(dp[root], dp[root][4])`
Breadth-First Search เหมาะมากสำหรับการหา **ระยะทางที่สั้นที่สุด (Shortest Path)** จาก Root ไปยังโหนดอื่นๆ ในต้นไม้ที่ไม่มีน้ำหนักเส้นเชื่อม (Unweighted Tree)
แหล่งเรียนรู้: https://www.geeksforgeeks.org/tree-traversals-bfs-and-dfs/
โจทย์: จงหาระยะทางสั้นที่สุดจาก Root (โหนด 1) ไปยังทุกโหนดในต้นไม้
แนวทางแก้ปัญหาด้วย BFS:
อ้างอิง: Competitive Programmer's Handbook (Chapter 12.2)
// ใช้ Queue ในการหา Shortest Path บนต้นไม้
Time: \( O(V + E) \)การทำงานของ BFS จะกระจายออกเป็นวงกว้าง (Breadth) ทำให้เราสามารถระบุ "ความลึก" (Depth) หรือ "ระดับ" (Level) ของโหนดในต้นไม้ได้อย่างแม่นยำ
นิยาม: ระยะทางที่ยาวที่สุดระหว่างโหนด 2 โหนดใดๆ ในต้นไม้
หนึ่งในวิธีการหา Diameter ที่มีประสิทธิภาพและนิยมใช้ที่สุดคือการทำ **DFS/BFS 2 ครั้ง** ซึ่งจะให้ความซับซ้อนของเวลาเพียง \( O(n) \)
โจทย์: กำหนดต้นไม้ที่มี \( n \) โหนด จงหาความยาวของเส้นทางที่ยาวที่สุด
ขั้นตอนที่ 1: หาจุดปลายด้านที่หนึ่ง
เลือกโหนด \( a \) ใดก็ได้ (เช่น โหนด 1) แล้วใช้ DFS เพื่อหาโหนด \( b \) ที่อยู่ ไกลที่สุด จาก \( a \)
ขั้นตอนที่ 2: หาจุดปลายด้านที่สอง
เริ่ม DFS จากโหนด \( b \) ที่เจอ เพื่อหาโหนด \( c \) ที่อยู่ ไกลที่สุด จาก \( b \) ระยะทางนี้คือ Diameter
"โหนดที่อยู่ไกลที่สุดจากโหนดใดๆ มักจะเป็นจุดปลายจุดหนึ่งของ Diameter เสมอ"
Time Complexity: \( O(n) \)
Space Complexity: \( O(n) \) (Adjacency List)
อ้างอิง: Competitive Programmer's Handbook (Chapter 14.2)
Step 1:
เริ่มจากโหนดใดก็ได้ (โหนด \( x \)) แล้วใช้ DFS หาโหนดที่อยู่ไกลที่สุดจากมัน (สมมติว่าเป็นโหนด \( a \))
Step 2:
เริ่มจากโหนด \( a \) แล้วใช้ DFS หาโหนดที่ไกลที่สุดจาก \( a \) อีกครั้ง (สมมติว่าเป็นโหนด \( b \))
ระยะทางระหว่าง \( a \) และ \( b \) คือ Diameter!
ทำไมถึงใช้ได้?
เพราะจุดปลายของ Diameter จะเป็นจุดที่อยู่ไกลที่สุดจากโหนดใดๆ ในต้นไม้เสมอ การทำ DFS ครั้งแรกจึงรับประกันว่าเราจะเจอ "จุดปลาย" ด้านหนึ่งแน่นอน
อ้างอิง: https://cses.fi/book/book.pdf
ปัญหา: จงหาความยาวของเส้นทางที่ยาวที่สุดในต้นไม้ (Diameter)
ตัวอย่าง Input:
อธิบายการคำนวณ:
หมายเหตุ: ในต้นไม้ที่ไม่มีน้ำหนัก (Unweighted) ความยาวเส้นทางคือนับตามจำนวนเส้นเชื่อม
เพื่อให้โค้ดมีประสิทธิภาพ \( O(n) \) เราจะใช้โครงสร้าง DFS แบบไม่ใช้ `visited` array:
DFS Function: รับพารามิเตอร์ `(node, parent, distance)` เพื่อท่องไปในต้นไม้โดยไม่ย้อนกลับทางเดิม
Tracking: ใช้ตัวแปร Global เก็บ `maxDist` และ `farthestNode` โดยจะอัปเดตค่าทุกครั้งที่เจอระยะทางที่ไกลกว่าเดิม
Main Execution: เรียก DFS ครั้งแรกเพื่อหาจุดปลายหนึ่งด้าน จากนั้นใช้จุดนั้นเป็นจุดเริ่มในการเรียก DFS ครั้งที่สองเพื่อหาค่า Diameter จริง
⏱️ Time Complexity: \( O(n) \)
💾 Space Complexity: \( O(n) \)
อ้างอิงโค้ดพื้นฐาน: Competitive Programmer's Handbook
การใช้ DFS ช่วยให้เราประมวลผลความสัมพันธ์ระหว่างโหนดได้:
เพิ่มเติมเรื่อง LCA: https://usaco.guide/gold/LCA
โจทย์: กำหนดต้นไม้ที่มี \( n \) โหนด จงตอบคำถาม \( q \) ครั้ง ว่าบรรพบุรุษลำดับที่ \( k \) ของโหนด \( x \) คือใคร?
ตัวอย่าง Input:
ลำดับการไล่สายสัมพันธ์:
*หากใช้การขยับทีละขั้น (Naive) จะใช้เวลา \( O(n) \) ต่อ Query ซึ่งจะช้าเกินไปถ้าต้นไม้เป็นเส้นตรงและมี Query จำนวนมาก
แนวคิด: ขยับเป็นระยะทางกำลังของ 2 เพื่อลดเวลาเป็น \( O(\log n) \)
Preprocessing ใช้เวลา \( O(n \log n) \) และตอบ Query ใน \( O(\log n) \)
Precompute: \( O(n \log n) \)
Query: \( O(\log k) \)
แนวคิดการหา LCA แบบพื้นฐานด้วย Traversal:
ศึกษาเพิ่มเติม: https://cses.fi/book/book.pdf (Chapter 15.3)
นิยาม: โหนดที่ต่ำที่สุดที่เป็นบรรพบุรุษของทั้งสองโหนด .
ขั้นตอนการหาคำตอบ (Method 1):
up[x][i]) เพื่อช่วยในการกระโดดขึ้นไปหาบรรพบุรุษ .ตัวอย่างการใช้งาน:
โหนด 5 อยู่ระดับ 3, โหนด 8 อยู่ระดับ 4 .
LCA ของ 5 และ 8 คือ 2 ซึ่งอยู่ระดับ 2 .
การนำไปใช้ต่อ:
เราสามารถใช้ LCA เพื่อหา ระยะทาง (Distance) ระหว่างโหนด $a$ และ $b$ ได้โดยใช้สูตร:
$depth(a) + depth(b) - 2 \cdot depth(LCA(a,b))$ .
แทนที่จะเดินขึ้นทีละ 1 ขั้น เราจะเดินขึ้นทีละ ( 2^k ) ขั้น:
การเตรียมข้อมูล (Preprocessing) ใช้เวลา ( O(n \log n) ) ผ่าน DFS ครั้งเดียว แต่ช่วยให้เราหา LCA ได้ในเวลาเพียง ( O(\log n) ) ต่อหนึ่งคำถาม (Query)
รายละเอียดอัลกอริทึม: https://usaco.guide/gold/LCA
นิยาม: โหนด $c$ ที่เป็นบรรพบุรุษของทั้ง $a$ และ $b$ โดยที่ $c$ อยู่ในระดับที่ลึกที่สุด
ตัวอย่างโครงสร้าง:
Queries:
*โจทย์ต้องการให้ตอบคำถามเหล่านี้หลายๆ ครั้ง (Queries) ในเวลาที่รวดเร็วที่สุด
แบ่งการทำงานเป็น 2 ส่วนหลัก :
ปรับระดับโหนดให้เท่ากัน (Level Alignment)
หาก $depth(a) \neq depth(b)$ ให้ขยับโหนดที่ลึกกว่าขึ้นมาจนอยู่ในระดับเดียวกัน โดยใช้ตาราง up[v][k]
กระโดดขึ้นพร้อมกัน (Simultaneous Binary Jump)
ขยับทั้งสองโหนดขึ้นไปด้วยระยะทาง $2^k$ ที่มากที่สุด แต่ต้องทำให้โหนดทั้งสอง ยังไม่บรรจบกัน เมื่อขยับต่อไม่ได้แล้ว พ่อของพวกมันคือ LCA
💡 Tip: ใช้ up[v] เพื่อเก็บโหนดพ่อจากการทำ DFS
🎯 Application: ระยะทางระหว่าง $a, b = depth(a) + depth(b) - 2 \cdot depth(LCA)$
สูตรการคํานวณระยะทางระหว่างโหนด u และ v:
หลักการ: เราบวกระยะทางจากโหนดทั้งสองไปยัง Root แล้วลบส่วนของเส้นทางที่ซ้ำกันออก (เส้นทางจาก Root ไปยัง LCA) ซึ่งช่วยให้เราหาระยะทางได้ใน ( O(\log n) ) ทันทีหลังจากทำ Preprocessing
โจทย์: กำหนดต้นไม้ที่มี \( n \) โหนด จงหาระยะทางสั้นที่สุดระหว่างโหนด \( u \) และ \( v \)
ตัวอย่างโครงสร้าง:
การคำนวณ:
3 + 4 - 2(2) = 3
ระยะทางระหว่าง 5 และ 8 คือ 3
อ้างอิงหลักการ: ระยะทางระหว่างโหนด \( a \) และ \( b \) ลดรูปได้จากการหา Lowest Common Ancestor
กระบวนการประมวลผล (Algorithm Steps):
DFS Preprocessing: ท่องต้นไม้หนึ่งรอบเพื่อเก็บค่า depth[s] ของทุกโหนด และสร้างตาราง Binary Lifting สำหรับหา LCA
LCA Function: สร้างฟังก์ชัน get_lca(u, v) ที่ใช้เวลา \( O(\log n) \) ในการหาบรรพบุรุษร่วมที่ต่ำที่สุด
Distance Calculation: ใช้ฟังก์ชันหลักคำนวณตามสูตร:
depth[u] + depth[v] - 2 * depth[get_lca(u, v)]
⏱️ Complexity: O(log N) ต่อ Query
📌 ประยุกต์ใช้ได้ในโจทย์: Tree Path Sum Queries
อ้างอิงข้อมูล: Competitive Programmer's Handbook (Chapter 18.3)
หากแต่ละโหนดมีค่า ( value[s] ) เราต้องการหา ( sum[s] ) ซึ่งคือผลรวมของโหนดทั้งหมดใน Subtree ของ s
ใช้ในการแก้โจทย์ที่ต้องพิจารณาทรัพยากรภายใน Subtree หรือการแบ่งงานในโครงสร้างองค์กร
โจทย์: กำหนดโครงสร้างบริษัทเป็นต้นไม้ โดยแต่ละโหนดมีค่า "งบประมาณรายบุคคล" จงหา "งบประมาณรวม" ของทุก Subtree
ตัวอย่าง Input:
ผลลัพธ์ที่ต้องการ (Output):
การคำนวณต้องทำจาก "โหนดใบ" ขึ้นไปหา "โหนดพ่อ" (Bottom-up) เพื่อให้ข้อมูลพร้อมใช้งาน
กลยุทธ์การประมวลผล (Strategy):
Recursive DFS: เริ่มต้นการทำงานจาก Root และปล่อยให้ Recursion เดินลงไปจนถึงโหนดที่ลึกที่สุด (Leaf)
Base Initialization: ที่แต่ละโหนด $s$ ให้ตั้งต้นค่า sum[s] เท่ากับค่าของตัวเอง value[s]
Post-order Accumulation: เมื่อกลับขึ้นมาจากฟังก์ชันลูก (Recursive Call) ให้นำผลรวมของลูกแต่ละตัวมาบวกสะสมเข้ากับโหนดปัจจุบัน
⏱️ Complexity: O(n) เพราะเยี่ยมชมโหนดละ 1 ครั้ง
⚠️ ระวัง: หากจำนวนโหนดมาก ให้ระวัง Stack Overflow จาก Recursion
อ้างอิงหลักการ: Chapter 14 Dynamic Programming on Trees
นอกจากวิธี 2-DFS เรายังสามารถใช้ DP เพื่อหา Diameter ในโจทย์ที่ซับซ้อนกว่าได้:
f(v):
ระยะทางที่ยาวที่สุดจากโหนด v ลงไปหา Leaf ใน Subtree ของมัน
g(v):
Diameter ของเส้นทางที่ยาวที่สุดที่ ผ่าน โหนด v
g(v) = (ค่า f ที่ใหญ่ที่สุด 2 อันดับแรกของลูกๆ) + 2
อ้างอิงอัลกอริทึม: https://cses.fi/book/book.pdf (Chapter 14.3)
โจทย์: จงหาความยาวของเส้นทางที่ยาวที่สุดในต้นไม้ โดยห้ามใช้การ DFS 2 รอบ (ให้ใช้ DP)
แนวคิดการแบ่งปัญหา:
ในต้นไม้ที่ถูก Root ไว้แล้ว ทุกเส้นทางจะมี "จุดสูงสุด" (Highest Point) เสมอ . เราจะหาเส้นทางที่ยาวที่สุดที่ผ่านแต่ละโหนดในฐานะจุดสูงสุด แล้วหาค่ามากที่สุดจากทั้งหมดนั้น .
ตัวอย่างสถานะที่โหนด \( v \):
การคำนวณ \( f(v) \) และ \( g(v) \) ในรอบเดียว:
Compute \( f(v) \): ท่องลงไปหาใบ (Leaf) แล้วส่งระยะทางที่ไกลที่สุดกลับขึ้นมาหาพ่อ โดย \( f(v) = 1 + \max(f(child)) \) .
Maintain Top 2: ที่แต่ละโหนด ให้เก็บค่า \( f \) ของลูกที่มากที่สุด 2 อันดับแรก เพื่อนำมาหาเส้นทางที่ยาวที่สุดที่ "หักมุม" ผ่านโหนดนั้น .
Update Diameter: ให้ \( g(v) \) คือผลรวมของค่า \( f \) จากลูก 2 คนที่มากที่สุดบวกเพิ่มอีก 2 เส้นเชื่อม ค่า Diameter สุดท้ายคือค่าสูงสุดของ \( g(v) \) ทุกตัว .
⏱️ Time Complexity: O(n)
🎯 เหมาะสำหรับโหนดที่มีน้ำหนัก หรือโจทย์ที่ต้องการหา All Longest Paths .
"เปลี่ยนต้นไม้ให้กลายเป็นอาเรย์ (Linearizing a Tree)"
เราใช้ DFS เพื่อบันทึกเวลาที่ เข้า (Entry) และ ออก (Exit) จากแต่ละโหนด ซึ่งทำให้โหนดทุกลูกใน Subtree จะมีเวลาอยู่ในช่วงเดียวกันเสมอในรูปแบบอาเรย์
Problem
ให้ต้นไม้ที่มี N โหนด โดยโหนดแต่ละตัวมีค่า value[v]
มี Query 2 แบบ
ข้อจำกัด: N, Q ≤ 200000
ถ้าใช้ DFS ทุกครั้งจะช้าเกินไป (O(N) ต่อ query)
Step 1: Flatten Tree ด้วย Euler Tour
Subtree ของโหนด v จะกลายเป็นช่วงในอาเรย์
จากนั้นใช้ Segment Tree / Fenwick Tree เพื่อทำ Range Sum Query ได้เร็ว
Complexity: O((N + Q) log N)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
vector<int> g[MAXN];
int tin[MAXN], tout[MAXN], timer = 0;
long long val[MAXN];
long long fenwick[MAXN];
int N,Q;
void dfs(int v, int p){
tin[v] = ++timer;
for(int u : g[v]){
if(u == p) continue;
dfs(u,v);
}
tout[v] = timer;
}
void update(int i, long long delta){
for(; i <= N; i += i & -i)
fenwick[i] += delta;
}
long long query(int i){
long long s = 0;
for(; i > 0; i -= i & -i)
s += fenwick[i];
return s;
}
long long rangeQuery(int l, int r){
return query(r) - query(l-1);
}
int main(){
cin >> N >> Q;
for(int i=1;i<=N;i++)
cin >> val[i];
for(int i=0;i> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);
for(int i=1;i<=N;i++)
update(tin[i], val[i]);
while(Q--){
int type;
cin >> type;
if(type == 1){
int v; long long x;
cin >> v >> x;
update(tin[v], x - val[v]);
val[v] = x;
}
else{
int v;
cin >> v;
cout << rangeQuery(tin[v], tout[v]) << "\n";
}
}
}
ทำไมเทคนิคนี้ถึงทรงพลัง?
ศึกษาเพิ่มเติม: https://usaco.guide/gold/tree-euler
Problem
ให้ต้นไม้ที่มี N โหนด (root = 1) และค่าเริ่มต้น value[v]
มีคำสั่ง Q คำสั่ง
ข้อจำกัด
ถ้า DFS ทุกครั้งจะใช้เวลา O(NQ) ซึ่งช้าเกินไป
Step 1: Flatten Tree ด้วย Euler Tour
Subtree ของ v จะกลายเป็นช่วงต่อเนื่อง
Step 2: ใช้ Fenwick Tree
Complexity: O((N + Q) log N)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
vector<int> g[MAXN];
int tin[MAXN], tout[MAXN], timer = 0;
long long fenwick[MAXN];
int N,Q;
void dfs(int v, int p){
tin[v] = ++timer;
for(int u : g[v]){
if(u == p) continue;
dfs(u,v);
}
tout[v] = timer;
}
void update(int i, long long val){
for(; i <= N; i += i & -i)
fenwick[i] += val;
}
long long query(int i){
long long s = 0;
for(; i > 0; i -= i & -i)
s += fenwick[i];
return s;
}
int main(){
cin >> N >> Q;
for(int i=0;i> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);
while(Q--){
int type;
cin >> type;
if(type == 1){
int v,x;
cin >> v >> x;
update(tin[v], x);
update(tout[v]+1, -x);
}
else{
int v;
cin >> v;
cout << query(tin[v]) << "\n";
}
}
}
int timer = 0;
void dfs(int s, int e) {
tin[s] = ++timer; // เวลาเข้า
for (auto u : adj[s]) {
if (u == e) continue;
dfs(u, s);
}
tout[s] = timer; // เวลาออก
}
"ถ้า ( tin[u] \le tin[v] ) และ ( tout[u] \ge tout[v] ) แปลว่า u เป็นบรรพบุรุษของ v"
เทคนิคการแปลง Edge เป็น Node:
ในต้นไม้ เรามักจะเก็บค่าของเส้นเชื่อมไว้ที่ โหนดลูก (Child Node) เนื่องจากทุกลูกจะมีพ่อ (Parent) เพียงคนเดียวเสมอ ยกเว้น Root ซึ่งจะไม่มีค่าเส้นเชื่อมอยู่เหนือมัน
ต้นไม้ที่ลึกมาก (เช่น ( 10^5 ) โหนด) อาจทำให้ Recursion ค้างได้ ลองเพิ่มขนาด Stack หรือใช้ Iterative DFS
อย่าลืมส่งพารามิเตอร์พ่อ (Parent) เสมอเพื่อกันไม่ให้เดินย้อนกลับไปจุดเดิมจนติดลูปไม่รู้จบ
"Traversal is the bridge between Structure and Algorithm."
// ให้ up[v] = parent[v] จากการทำ DFS
for (int k = 1; k < LOG; k++) {
for (int v = 1; v <= n; v++) {
up[v][k] = up[up[v][k-1]][k-1];
}
}
แนวคิด: บรรพบุรุษลำดับที่ ( 2^k ) ของ ( v ) คือบรรพบุรุษลำดับที่ ( 2^{k-1} ) ของบรรพบุรุษลำดับที่ ( 2^{k-1} ) ของ ( v ) เราใช้เวลา ( O(n \log n) ) ในการเติมตารางนี้ให้ครบ
อ้างอิงเทคนิค: https://usaco.guide/gold/LCA
ถ้า ( depth[u] < depth[v] ) ให้สลับตัวแปร แล้วกระโดด ( u ) ขึ้นไปให้สูงเท่ากับ ( v ) โดยใช้เลขฐานสองของผลต่างระดับ
ถ้ายังไม่เจอกัน ให้ลองกระโดดขึ้นไป ( 2^k ) ขั้นพร้อมกันจาก ( k ) ใหญ่ไปหาเล็ก โดยมีเงื่อนไขว่าต้อง "ไม่กระโดดจนเลยจุดที่เจอกัน"
"ผลลัพธ์สุดท้ายคือ parent ของโหนดที่ u และ v กระโดดไปหยุด"
"กระโดดขึ้นไปตาม Bit ของ k ในเวลา ( O(\log k) )"
เทคนิคนี้ช่วยให้เราสามารถตอบคำถาม "ใครคือหัวหน้าลำดับที่ 100 ของพนักงานคนนี้" ได้อย่างรวดเร็วมาก แม้ต้นไม้จะมีความสูงถึงระดับแสนโหนดก็ตาม
โจทย์ฝึกหัด: https://cses.fi/problemset/task/1687
โจทย์: เลือกโหนดให้มีค่าน้ำหนักรวมมากที่สุด โดยห้ามเลือกโหนดที่ติดกัน
นิยามสถานะ:
dp[v] = Σ max(dp[u], dp[u][1])
dp[v][1] = weight[v] + Σ dp[u]
เมื่อเราแปลง Tree เป็น Array ผ่านช่วง ( [tin[v], tout[v]] ):
อัปเดตค่าที่ตำแหน่ง ( tin[v] ) ใน Fenwick Tree
หาผลรวมช่วง ( [tin[v], tout[v]] ) ในเวลา ( O(\log n) )
"นี่คือกุญแจสำคัญในการแก้โจทย์ Dynamic Tree Connectivity เบื้องต้น"
ศึกษาเพิ่มเติม: https://cses.fi/book/book.pdf (Chapter 18)
"โหนดที่ทำให้ระยะทางไปยัง Leaf ที่ไกลที่สุดสั้นที่สุด"
วิธีการหา:
1. หาเส้น Diameter
2. จุดศูนย์กลางจะอยู่กึ่งกลางของเส้นทาง Diameter นั้นเสมอ
(ต้นไม้อาจมีจุดศูนย์กลางได้ 1 หรือ 2 จุด)
ใช้ในการเลือกจุดกระจายสัญญาณหรือจุดพักที่มีประสิทธิภาพที่สุดบนโครงข่าย
โจทย์มักให้ข้อมูลเป็นเส้นเชื่อมลอยๆ (Unrooted Tree) การเลือก Root ที่เหมาะสมจะช่วยให้ใช้ Tree DP ได้ง่ายขึ้น:
URL: https://cses.fi/problemset/task/1130
หาจำนวนเส้นเชื่อมที่มากที่สุดที่ห้ามมีจุดปลายร่วมกัน (Matching)
เป็นโจทย์ที่ฝึกใช้ Tree DP (เลือก/ไม่เลือก) และการจัดการสถานะที่ขึ้นอยู่กับการตัดสินใจของโหนดพ่อและลูกพร้อมกัน
URL: https://cses.fi/problemset/task/1135
มีคำถาม ( q ) ข้อ แต่ละข้อให้หาความห่างระหว่างโหนด ( u ) และ ( v )
ฝึกฝนการเขียน LCA ด้วย Binary Lifting และการใช้ความลึก (Depth) ในการหาคำตอบอย่างรวดเร็ว (Preprocessing ( O(n \log n) ), Query ( O(\log n) ))
"คุณพร้อมสำหรับการลงมือปฏิบัติแก้โจทย์ต้นไม้ในช่วงสุดท้ายของวันนี้แล้ว!"
15:00 - 17:00
ภารกิจสุดท้ายของ Day 06: