แหล่งอ้างอิง: https://cses.fi/book/book.pdf
"กำหนดกราฟที่มีน้ำหนัก (G = (V, E)) และจุดเริ่มต้น (s) จงหาระยะทางที่สั้นที่สุด (d(s, v)) สำหรับทุกโหนด (v) ในกราฟ"
แหล่งอ้างอิง: https://usaco.guide/CPH.pdf
Dijkstra ทำงานโดยการ ขยายขอบเขต ของโหนดที่เรามั่นใจระยะทางสั้นที่สุดแล้ว:
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
Dijkstra ไม่รับรอง ความถูกต้องหากกราฟมีเส้นเชื่อมน้ำหนักติดลบ
เพราะหัวใจของอัลกอริทึมคือการเชื่อว่า "ถ้าเราเลือกโหนดที่ใกล้ที่สุดในปัจจุบัน ระยะทางนั้นจะไม่มีทางถูกทำให้สั้นลงได้อีกโดยการเดินผ่านโหนดอื่นที่ไกลกว่า" ซึ่งข้อสมมตินี้จะผิดทันทีถ้ามีการลดระยะทางด้วยน้ำหนักติดลบ
แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms
เก็บระยะทางที่สั้นที่สุดที่พบในปัจจุบันสำหรับแต่ละโหนด (dist[V])
ใช้เก็บคู่ของ (distance, node) เพื่อดึงโหนดที่มีระยะทางน้อยที่สุดออกมาอย่างรวดเร็ว
แหล่งอ้างอิง: https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/
if (dist[u] + weight(u, v) < dist[v]) {
dist[v] = dist[u] + weight(u, v);
}
คือการตรวจสอบว่า การเดินทางไปยังโหนด (v) โดยผ่านโหนด (u) นั้น ให้ระยะทางที่ สั้นกว่า เส้นทางที่เราเคยรู้จักมาก่อนหน้านี้หรือไม่
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
1. ตั้งค่า (dist[start] = 0) และ (dist[others] = \infty)
2. ใส่ (0, start) ลงใน Priority Queue
3. ขณะที่ Queue ไม่ว่าง: ดึงโหนด (u) ที่มีระยะทางน้อยที่สุดออกมา
4. สำคัญ: ถ้าโหนด (u) ถูกประมวลผล (Processed) แล้ว ให้ข้ามไป
5. สำหรับแต่ละโหนดเพื่อนบ้าน (v) ของ (u): ถ้าเส้นเชื่อม (u \to v) ช่วยลดระยะทาง (Relax) ให้ปรับปรุง (dist[v]) และใส่เข้า Queue ใหม่
แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms
priority_queue<pair<int,int>> q;
distance[x] = 0; q.push({0, x});
while (!q.empty()) {
int a = q.top().second; q.pop();
if (processed[a]) continue;
processed[a] = true;
for (auto u : adj[a]) {
int b = u.first, w = u.second;
if (distance[a]+w < distance[b]) {
distance[b] = distance[a]+w;
q.push({-distance[b], b}); // Use negative for min-heap
}
}
}
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
(O(V + E \log E))
• (V) คือจำนวนโหนด และ (E) คือจำนวนเส้นเชื่อม
• การวนรอบผ่านเส้นเชื่อมทุกเส้นใช้ (E)
• การดำเนินการใน Priority Queue (push/pop) ใช้ (\log E)
• ในกราฟทั่วไป มักเขียนในรูป (O(E \log V))
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
แหล่งอ้างอิง: https://usaco.guide/CPH.pdf
ในการหาว่าเส้นทางที่สั้นที่สุดผ่านโหนดใดบ้าง เราต้องบันทึกโหนดก่อนหน้า (Parent):
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
หากกราฟมีจำนวนเส้นเชื่อมใกล้เคียงกับ (V^2) (Dense Graphs):
แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms
ตราบใดที่น้ำหนักเส้นเชื่อม ไม่ติดลบ ((\ge 0)) Dijkstra จะหาคำตอบที่สั้นที่สุดได้เสมอ แต่ระวังหากกราฟมี Cycle ที่เป็นศูนย์ทั้งหมด อาจทำให้มีหลายเส้นทางที่สั้นที่สุดเท่ากัน
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
// บรรทัดที่ห้ามลืม!
if (d > distance[u]) continue;
เนื่องจาก Priority Queue ใน STL ไม่รองรับการอัปเดตค่า (Decrease Key) เราจึงต้องใส่โหนดเดิมที่มีระยะทางใหม่เข้าไป บรรทัดนี้จะช่วยข้ามโหนด "ขยะ" ที่มีระยะทางมากกว่าค่าปัจจุบัน ทำให้ความซับซ้อนคงอยู่ที่ (O(E \log E))
แหล่งอ้างอิง: https://usaco.guide/CPH.pdf
โหนดในที่นี้คือพิกัด (r, c) และเส้นเชื่อมคือการเคลื่อนที่ไปยังช่องข้างเคียง:
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
"โหนดใดอยู่ใกล้สถานีดับเพลิงที่สุด?"
แทนที่จะรัน Dijkstra ทีละสถานี เราสามารถใส่โหนดสถานีทั้งหมดลงใน Priority Queue พร้อมกันตั้งแต่เริ่มต้น:
ผลลัพธ์คือระยะทางสั้นที่สุดจากโหนดใดก็ได้ในกลุ่มต้นทางไปยังจุดอื่นๆ
แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms
ใส่ระยะทางเป็นค่าติดลบเพื่อให้ตัวที่น้อยที่สุด (แต่ค่าลบมากสุด) อยู่บนสุด
pq.push({-d, u});
ใช้ตัวดำเนินการเปรียบเทียบมาตรฐานของ C++ เพื่อสร้าง Min-Heap โดยตรง
priority_queue<T, vector<T>, greater<T>> pq;
แหล่งอ้างอิง: https://www.geeksforgeeks.org/custom-comparator-in-priority_queue-in-cpp-stl/
พิจารณาทางเดิน: A → B (น้ำหนัก 2), A → C (น้ำหนัก 5), B → C (น้ำหนัก -10)
1. Dijkstra จะเลือก B ก่อน (ระยะทาง 2) แล้วข้ามไปที่ C (ระยะทาง 5)
2. Dijkstra จะมองว่า C "เสร็จแล้ว" และจะไม่กลับมาแก้ไขระยะทางผ่าน B (ซึ่งจริงๆ คือ 2 - 10 = -8)
สรุป: ความละโมบ (Greedy) ของ Dijkstra จะใช้งานไม่ได้เมื่อ "ทางอ้อม" สามารถให้ผลลัพธ์ที่สั้นกว่าได้ไม่จำกัด
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
เทคนิคขั้นสูง: โหนดไม่จำเป็นต้องเป็นจุดในแผนที่
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
"เตรียมพร้อมสู่เซสชันบ่าย: เมื่อ Dijkstra รับมือไม่ได้ เราจะทำอย่างไร?"
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
เราสามารถดัดแปลง Dijkstra เพื่อหาว่ามีกี่เส้นทางที่ให้ระยะทางสั้นที่สุดเท่ากัน:
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
นอกจากการใช้ Priority Queue เรายังสามารถใช้ std::set ใน C++ ได้:
แหล่งอ้างอิง: https://usaco.guide/CPH.pdf
| Feature | Priority Queue (Lazy) | std::set (Red-Black Tree) |
|---|---|---|
| Memory | High (up to (E) entries) | Low (exactly (V) entries) |
| Complexity | (O(E \log E)) | (O(E \log V)) |
| Coding Speed | Fast/Simple | Slightly complex |
"ในสมรภูมิ Competitive Programming ส่วนใหญ่ Priority Queue คือผู้ชนะ"
เพื่อหาทางสั้นที่สุดระหว่าง (s) และ (t) เราสามารถรัน Dijkstra สองตัวพร้อมกัน:
แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms
Problem Description:
กำหนดเมือง (n) เมือง และเส้นทางบิน (m) เส้นทาง จงหาระยะทางที่สั้นที่สุดจากเมืองที่ 1 ไปยังทุกเมือง
แหล่งอ้างอิง: https://cses.fi/problemset/task/1671
"น้ำหนักรวมของเส้นทางอาจเกินช่วงของ int ((\approx 2 \times 10^9))"
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
แหล่งอ้างอิง: https://usaco.guide/CPH.pdf
ลืมใส่ค่าติดลบหรือลืมใช้ `greater<>` ทำให้กลายเป็น Max-Heap (ช้ามาก!)
ลืมเช็ค `if (d > dist[u]) continue;` ทำให้วนซ้ำโหนดเดิมหลายรอบ
ลืมตั้งระยะทางเริ่มต้นเป็น 0 (ทำให้ Dijkstra ไม่เริ่มทำงาน)
ใส่เส้นเชื่อมสลับทิศทางหรือไม่ครบตามโจทย์กำหนด
แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms
"ประสิทธิภาพที่เหนือชั้นในทางปฏิบัติ"
แหล่งอ้างอิง: https://cses.fi/book/book.pdf
"พักเที่ยง! กลับมาพบกับ Bellman-Ford และคู่อริอย่าง Negative Cycles"
แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms
ในช่วงบ่าย เราจะมาดูสถานการณ์ที่ Dijkstra รับมือไม่ได้...
13.00 - 15.00 น. Shortest Paths II
Bellman-Ford, Floyd-Warshall และการค้นหาเส้นทางในกราฟ "ติดลบ"
เตรียมโจทย์ CSES: Shortest Routes II ไว้รอกันได้เลยครับ