แผนการเรียน
/

Shortest Paths II
Bellman-Ford & Floyd-Warshall

Day 08 Afternoon: การจัดการกราฟที่มีค่าน้ำหนักติดลบและการหาระยะทางระหว่างทุกคู่โหนด

หัวข้อที่จะเรียนรู้

1
Bellman-Ford: อัลกอริทึมที่รับมือกับเส้นเชื่อมติดลบได้
2
Negative Cycle Detection: การตรวจจับวงรอบที่ลดระยะทางไม่สิ้นสุด
3
Floyd-Warshall: การหาระยะทางที่สั้นที่สุดระหว่างทุกคู่โหนด (All-pairs)

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

The Limit of Dijkstra
ทำไม Dijkstra ถึงล้มเหลวในกราฟติดลบ?

Dijkstra ตั้งอยู่บนสมมติฐานแบบ Greedy: โหนดที่ใกล้ที่สุดที่ถูกเลือกจะไม่ถูกปรับปรุงอีก

  • หากมีเส้นเชื่อมติดลบ "ทางอ้อม" ที่ดูเหมือนจะไกลในตอนแรก อาจกลายเป็นทางที่สั้นที่สุดในภายหลังได้
  • Dijkstra ไม่สามารถตรวจจับ Negative Cycle ซึ่งทำให้ระยะทางลดลงเรื่อยๆ ได้
  • เราต้องการอัลกอริทึมที่ "พิจารณาเส้นเชื่อมซ้ำ" เพื่อความถูกต้อง

แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms

Bellman–Ford Algorithm
อัลกอริทึมของเบลล์แมน-ฟอร์ด

คุณสมบัติหลัก:

  • หาระยะทางสั้นที่สุดจากจุดเริ่มต้นเดียว (Single-source)
  • รองรับเส้นเชื่อมน้ำหนักติดลบ (Negative weights)
  • สามารถตรวจจับ Negative Cycle ในกราฟได้
  • ทำงานโดยการวนซ้ำพิจารณาเส้นเชื่อมทั้งหมดทีละรอบ

แหล่งอ้างอิง: https://www.geeksforgeeks.org/bellman-ford-algorithm-dp-23/

Incremental Relaxation
การผ่อนคลายเส้นเชื่อมแบบวนซ้ำ

ในขณะที่ Dijkstra เลือกโหนดที่ใกล้ที่สุดมาพิจารณาเพื่อนบ้าน แต่ Bellman-Ford จะพิจารณา "เส้นเชื่อมทุกเส้น" ในกราฟซ้ำๆ

"ถ้าเรามีโหนด (n) โหนด เส้นทางสั้นที่สุดที่ไม่มีวงรอบจะมีความยาวไม่เกิน (n-1) เส้นเชื่อม ดังนั้นเราจึงต้องวนรอบเพื่อผ่อนคลายเส้นเชื่อมทั้งหมดจำนวน (n-1) รอบ"

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

How it Works?
ลำดับการประมวลผล

1. distance[source] = 0, others = INF

2. for i = 1 to n-1 rounds:

for each edge (u, v) with weight w:

if distance[u] + w < distance[v]:

distance[v] = distance[u] + w

3. check for negative cycles (round n)

"ความซับซ้อนของเวลา: (O(V \cdot E)) ซึ่งช้ากว่า Dijkstra มาก"

แหล่งอ้างอิง: https://usaco.guide/CPH.pdf

Negative Cycles
วงรอบติดลบ: กับดักของกราฟ

"จะเกิดอะไรขึ้นถ้าเราวนรอบที่ (n)?"

หากหลังจากทำครบ (n-1) รอบแล้ว เรายังสามารถลดระยะทาง (Relax) ได้อีกในรอบที่ (n) แสดงว่ามีวงรอบที่ผลรวมน้ำหนักติดลบอยู่ ซึ่งหมายความว่า ไม่มีทางสั้นที่สุดที่แท้จริง เพราะคุณสามารถเดินวนเพื่อลดระยะทางลงได้เรื่อยๆ จนถึง (-\infty)

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

SPFA Optimization
การเพิ่มประสิทธิภาพด้วย Queue

"ทำไมต้องพิจารณาเส้นเชื่อมทุกเส้นในทุกรอบ?"

  • แนวคิด: เราจะประมวลผลเฉพาะโหนดที่ระยะทางเพิ่งถูก "ปรับปรุง" เท่านั้น
  • ใช้ Queue เพื่อเก็บโหนดที่ต้องนำไปพิจารณาต่อ
  • ในกรณีทั่วไปทำงานได้รวดเร็วพอๆ กับ Dijkstra
  • คำเตือน: ในกรณีเลวร้ายที่สุด (Worst-case) ความซับซ้อนยังคงเป็น (O(V \cdot E)) และมีโจทย์ที่ออกแบบมาเพื่อดักทาง SPFA โดยเฉพาะ

แหล่งอ้างอิง: https://www.geeksforgeeks.org/shortest-path-faster-algorithm/

All-Pairs Shortest Path
หาระยะทางระหว่าง "ทุกคู่โหนด"

โจทย์:

คำนวณหาระยะทางสั้นที่สุด d(u, v) สำหรับทุกๆ คู่ของโหนด u, v ในกราฟ

ทางเลือก:

• รัน Dijkstra จำนวน V ครั้ง: (O(V \cdot E \log V))

• ใช้ Floyd-Warshall: เขียนโค้ดง่ายที่สุดและรองรับน้ำหนักติดลบ

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Floyd-Warshall Intuition
แนวคิดของฟลอยด์-วอร์แชล

"ผ่านโหนด k หรือไม่?"

หัวใจหลักคือการใช้ Dynamic Programming โดยพิจารณาว่า เส้นทางจาก i ไป j จะสั้นลงไหมถ้าเราเลือกเดิน ผ่านโหนดตัวกลาง k

dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

แหล่งอ้างอิง: https://www.geeksforgeeks.org/floyd-warshall-algorithm-dp-16/

Implementation Simplicity
ความเรียบง่ายระดับ O(V^3)

// 3 Nested Loops: k ต้องเป็น loop นอกสุดเสมอ!

for (int k = 1; k <= n; k++) {

for (int i = 1; i <= n; i++) {

for (int j = 1; j <= n; j++) {

dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);

}

}

}

"เหมาะสำหรับกราฟขนาดเล็ก (n \le 500)"

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Implementation in C++
การเขียนโค้ดเบลล์แมน-ฟอร์ด

struct Edge { int a, b, w; };

vector<Edge> edges;

for (int i = 1; i <= n-1; i++) {

for (auto e : edges) {

dist[e.b] = min(dist[e.b], dist[e.a] + e.w);

}

}

// หมายเหตุ: ควรใช้ long long สำหรับระยะทางเพื่อป้องกัน Overflow

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Cycle Detection Logic
การตรวจจับวงรอบติดลบ

หลังจากทำครบ (n-1) รอบแล้ว ให้ทำการวนรอบที่ (n) อีกหนึ่งครั้ง:

  • หากยังมีเส้นเชื่อมใดที่สามารถ Relax ได้ (ระยะทางยังลดลงได้อีก)
  • แสดงว่ากราฟนั้นมี Negative Cycle ที่เข้าถึงได้จากจุดเริ่มต้น

แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms

Path Reconstruction
การหาร่องรอยเส้นทาง

การบันทึกเส้นทางใน Bellman-Ford ทำได้เช่นเดียวกับ Dijkstra:

  • สร้างอาร์เรย์ parent[V] เพื่อเก็บโหนดก่อนหน้า
  • เมื่อพบว่า (dist[u] + w < dist[v]) ให้บันทึก (parent[v] = u)
  • หากต้องการหาเส้นทางจริง ให้ไล่ย้อนกลับจากปลายทางไปยังต้นทาง
  • พิเศษ: หากพบวงรอบติดลบ คุณสามารถไล่ย้อนกลับเพื่อระบุโหนดทั้งหมดที่อยู่ในวงรอบนั้นได้

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

The Queue-based BF (SPFA)
เร่งความเร็วด้วย Shortest Path Faster Algorithm

แทนที่จะตรวจเส้นเชื่อมทุกเส้น ให้เราตรวจเฉพาะโหนดที่ระยะทาง "เพิ่งเปลี่ยน" เท่านั้น:

while (!q.empty()) {
  int u = q.front(); q.pop();
  for (auto [v, w] : adj[u]) {
    if (dist[u] + w < dist[v]) {
      dist[v] = dist[u] + w;
      if (!in_queue[v]) q.push(v);
    }
  }
}

แหล่งอ้างอิง: https://www.geeksforgeeks.org/shortest-path-faster-algorithm/

DP Intuition
Floyd-Warshall ในมุมมองของ DP

"ระยะทางสั้นที่สุดจาก i ไป j โดยใช้โหนดตัวกลางจากเซต (1, 2, ..., k)"

dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])

เราสามารถลดมิติของอาร์เรย์เหลือเพียง 2 มิติได้ในทางปฏิบัติเพื่อประหยัดหน่วยความจำ

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Transitive Closure
การหาความเชื่อมโยง (Reachability)

เราสามารถดัดแปลง Floyd-Warshall เพื่อตรวจสอบว่าโหนด (u) สามารถไปถึงโหนด (v) ได้หรือไม่:

  • 1. เปลี่ยนระยะทางเป็น Boolean (0/1)
  • 2. ใช้ตัวดำเนินการ Bitwise OR (|) และ AND (&) แทน min และ +
  • reach[i][j] = reach[i][j] | (reach[i][k] & reach[k][j])

แหล่งอ้างอิง: https://www.geeksforgeeks.org/transitive-closure-of-a-graph/

All-Pairs Path Reconstruction
การบันทึกเส้นทางระหว่างทุกคู่โหนด

ขั้นตอนการเก็บข้อมูล:

  1. สร้างอาร์เรย์ next[N][N] เริ่มต้นด้วย (next[i][j] = j) หากมีเส้นเชื่อมตรง
  2. ในลูปของ Floyd-Warshall: เมื่อพบทางที่ดีกว่าผ่านโหนด (k) ให้ปรับปรุง (next[i][j] = next[i][k])
  3. การสร้างเส้นทาง: เริ่มจากโหนด (u) แล้วตาม (next[u][v]) ไปเรื่อยๆ จนถึง (v)
"เหมาะสำหรับกราฟขนาดเล็กที่ต้องการตอบ Query เส้นทางได้ทันทีในเวลา (O(Path Length))"

แหล่งอ้างอิง: https://www.geeksforgeeks.org/finding-shortest-path-between-any-two-nodes-using-floyd-warshall-algorithm/

Shortest Path Algorithm Summary
เปรียบเทียบประสิทธิภาพบทเรียนวันนี้

AlgorithmTypeComplexityNegative Edges
DijkstraSSSP(O(E \log V))No
Bellman-FordSSSP(O(V \cdot E))Yes
SPFASSSP(O(k \cdot E)) avgYes
Floyd-WarshallAll-Pairs(O(V^3))Yes

"SSSP = Single-Source Shortest Path"

Competition Tips
เทคนิคสำหรับการสอบระดับโอลิมปิก

1. Memory Constraint

Floyd-Warshall ใช้ (N^2) bytes หาก (N=500) จะใช้พื้นที่ (500^2 \times 8) bytes (\approx 2MB) ซึ่งปลอดภัยมาก

2. Infinity Constant

อย่าใช้ 0x3f3f3f3f กับ long long หากค่าน้ำหนักมีโอกาสรวมกันเกิน (2 \times 10^9) ให้ใช้ 1e18 หรือ LLONG_MAX

3. Initial State

ลืมตั้งค่า (dist[i][i] = 0) ใน Floyd-Warshall มักเป็นสาเหตุของบั๊กยอดนิยม

4. Cycle Check

โจทย์มักถามหา "Negative Cycle" โดยตรง ให้ใช้ Bellman-Ford หรือ Floyd-Warshall (เช็ค (dist[i][i] < 0))

แหล่งอ้างอิง: https://roadmap.sh/datastructures-and-algorithms

Next: Practice Session
เตรียมตัวเข้าสู่ช่วงลงมือทำ

โจทย์ที่แนะนำ (CSES):

  • Shortest Routes I: Dijkstra พื้นฐาน
  • Shortest Routes II: Floyd-Warshall สำหรับระยะทางระหว่างทุกเมือง
  • High Score: Bellman-Ford สำหรับกราฟที่มีน้ำหนักติดลบ (Longest Path)
  • Cycle Finding: ค้นหาและแสดงผลโหนดในวงรอบติดลบ

แหล่งฝึกฝน: https://cses.fi/problemset/

Session II Summary
สรุปใจความสำคัญ

วันนี้เราผ่าน SSSP ที่ทรงพลังอย่าง Dijkstra
และก้าวข้ามขีดจำกัดด้วย Bellman-Ford รวมถึงความเรียบง่ายของ Floyd-Warshall
"เครื่องมือพร้อมแล้ว ถึงเวลาเปลี่ยนทฤษฎีเป็นผลลัพธ์ในการเขียนโปรแกรม"

"ไปลุยต่อในช่วง Practice (15:00 - 17:00 น.) กันครับ!"