ปูพื้นฐาน
เหยือกน้ำ ล็อกรหัส กระดานเลื่อนเบี้ย ใช้โค้ดชุดเดียวกันหมด ของที่ต้องเตรียมไปคือสามคำถามที่ต้องตอบให้ได้ก่อนพิมพ์โค้ดทุกครั้ง และสัญญาณที่บอกว่าเมื่อไรกราฟใหญ่เกินกว่าจะเดินตรง ๆ พร้อมโจทย์ฝึก 3 ข้อ
โจทย์กลุ่มนี้หน้าตาไม่เหมือนโจทย์กราฟเลยสักนิด มีเหยือกน้ำสองใบ ตวงให้ได้สี่ลิตรพอดีต้องทำอย่างน้อยกี่ท่า มีล็อกรหัสสี่หลัก หมุนจาก 0000 ไปเป็นรหัสหนึ่งโดยห้ามผ่านรหัสต้องห้าม ทำได้ไหมและกี่ครั้ง มีกระดานเลื่อนเบี้ย จัดให้เข้าที่ต้องเลื่อนกี่ตา
ทั้งหมดนี้เป็นโจทย์เดียวกัน และมันเป็นโจทย์กราฟที่เราต้องสร้างกราฟขึ้นมาเอง ความยากไม่ได้อยู่ที่อัลกอริทึม เพราะอัลกอริทึมคือการค้นตามความกว้างธรรมดาที่เขียนสิบบรรทัดจบ ความยากอยู่ที่การมองให้ออกว่าอะไรคือจุด และอะไรคือเส้น
สถานะ (state) คือภาพนิ่งของโลกที่มีข้อมูลพอจะเดินต่อได้โดยไม่ต้องรู้อดีต ในโจทย์เหยือกน้ำ สถานะคือปริมาณน้ำในเหยือกทั้งสองใบ ไม่ต้องรู้เลยว่ากว่าจะมาถึงตรงนี้เททิ้งไปกี่รอบ เพราะสิ่งที่ทำได้ต่อจากนี้ขึ้นกับปริมาณน้ำตอนนี้เท่านั้น
เส้นเชื่อมคือหนึ่งท่าที่กติกาอนุญาต ในโจทย์เหยือกน้ำมีหกท่าคือเติมใบใดใบหนึ่งให้เต็ม เททิ้งใบใดใบหนึ่ง และเทใบหนึ่งใส่อีกใบจนกว่าใบรับจะเต็มหรือใบเทจะหมด
เมื่อทุกท่าราคาเท่ากันคือหนึ่งท่า การหาทางที่สั้นที่สุดจึงเป็นงานของการค้นตามความกว้าง
(breadth-first search หรือ BFS) ซึ่งกวาดสถานะออกไปเป็นชั้น ๆ ชั้นที่ d คือสถานะทุกอันที่ไปถึงได้
ด้วย d ท่าพอดี ไม่มากไม่น้อยกว่านั้น
ชั้นแรกที่มีสถานะที่เราต้องการคือชั้นที่ 6 คำตอบของโจทย์นี้จึงเป็น 6 ท่า และเพราะเราเดินเป็นชั้น เราจึงรู้ทันทีว่าไม่มีทางที่สั้นกว่านี้ ไม่ต้องพิสูจน์อะไรเพิ่ม
| ท่าที่ | จาก | ทำอะไร | ได้เป็น |
|---|---|---|---|
| 1 | (0, 0) | เติมใบใหญ่ให้เต็ม | (0, 5) |
| 2 | (0, 5) | เทใบใหญ่ใส่ใบเล็ก | (3, 2) |
| 3 | (3, 2) | เททิ้งใบเล็ก | (0, 2) |
| 4 | (0, 2) | เทใบใหญ่ใส่ใบเล็ก | (2, 0) |
| 5 | (2, 0) | เติมใบใหญ่ให้เต็ม | (2, 5) |
| 6 | (2, 5) | เทใบใหญ่ใส่ใบเล็ก | (3, 4) |
ทำไมต้องเป็นการค้นตามความกว้าง
การค้นตามความลึก (DFS) ก็ไปถึงทุกสถานะเหมือนกัน แต่ระยะที่มันเจอครั้งแรกไม่ใช่ระยะที่สั้นที่สุด เพราะมันดิ่งไปสุดทางหนึ่งก่อน สถานะที่อยู่ห่างจากจุดเริ่มแค่สองท่าอาจถูกพบตอนที่เดินไปแล้วสิบท่า แล้วถูกทำเครื่องหมายว่าเคยไปแล้ว จนไม่มีใครมาแก้ให้อีก การค้นตามความกว้างปลอดภัยเพราะมันกวาดครบทั้งชั้น ก่อนขึ้นชั้นถัดไป สถานะจึงถูกพบครั้งแรกด้วยระยะที่สั้นที่สุดเสมอ ซึ่งเป็นเหตุผลที่เราตัดทิ้งได้เลย เมื่อเจอสถานะที่เคยไปแล้ว
เมื่อนิยามสถานะกับเส้นเชื่อมได้แล้ว โค้ดที่เหลือเหมือนกันหมดทุกข้อ เปลี่ยนแค่สามจุด
// โครงของการค้นตามความกว้าง ใช้ซ้ำได้ทุกข้อ เปลี่ยนแค่สามจุดที่มีคอมเมนต์กำกับ
vector<int> dist(STATES, -1); // (1) จำนวนสถานะทั้งหมด
queue<int> q;
dist[START] = 0; // (2) สถานะเริ่มต้น
q.push(START);
while (!q.empty()) {
int v = q.front(); q.pop();
if (isGoal(v)) { answer = dist[v]; break; }
for (int u : neighbours(v)) { // (3) หนึ่งท่าที่ทำได้จากสถานะ v
if (dist[u] != -1) continue; // เคยไปถึงแล้วด้วยระยะที่สั้นกว่าหรือเท่ากัน
dist[u] = dist[v] + 1;
q.push(u);
}
}
จุดที่เหลือให้คิดจริง ๆ มีข้อเดียว คือจะเข้ารหัสสถานะเป็นเลขตัวเดียวยังไง
เพราะเราอยากใช้อาเรย์ธรรมดาเป็นตัวจำว่าไปถึงหรือยัง ไม่ใช่ตารางแฮชที่ช้ากว่าหลายเท่า
ในโจทย์เหยือกน้ำ สถานะ (a, b) อัดเป็น a × (Y + 1) + b ได้ตรง ๆ
เหมือนการอ่านเลขสองหลักในฐาน Y + 1
| โจทย์ | สถานะคืออะไร | มีกี่สถานะ |
|---|---|---|
| กริด 1000 × 1000 | ตำแหน่ง (แถว, คอลัมน์) | 1,000,000 |
| เหยือกน้ำ 3 กับ 5 ลิตร | ปริมาณน้ำในเหยือกทั้งสองใบ | 24 |
| ล็อกรหัสสี่หลัก | ตัวเลขสี่หลักที่วงล้อชี้อยู่ | 10,000 |
| ปริศนา 15 | ตำแหน่งของเบี้ยทั้ง 16 ช่อง | 10,461,394,944,000 |
คิดก่อนอ่านต่อ
ก่อนเขียนโค้ดทุกครั้ง ให้ตอบสามคำถามนี้ให้ได้เป็นคำพูดก่อน หนึ่ง สถานะของโจทย์นี้คืออะไร และมีทั้งหมดกี่อัน สอง จากสถานะหนึ่ง ท่าที่ทำได้มีอะไรบ้าง สาม จะแปลงสถานะเป็นเลขจำนวนเต็มยังไงให้ไม่ชนกัน ถ้าตอบข้อแรกแล้วได้ตัวเลขที่ใหญ่เกินกว่าจะสร้างอาเรย์ได้ แปลว่าโจทย์ไม่ได้ต้องการ BFS ธรรมดา และต้องกลับไปอ่านโจทย์ใหม่ว่ามันขออะไรกันแน่
เหยือกสามกับห้าลิตร ขอสี่ลิตรพอดี
ต้องการ 4 ลิตรพอดี ในเหยือกใบใดใบหนึ่ง
เติม เททิ้ง หรือเทใส่กัน ทีละท่า จนกว่าจะมีใบไหนได้ปริมาณที่ขอ
ใช้ไปแล้ว 0 ท่า
ทุกท่าราคาเท่ากันหมด ซึ่งเป็นเงื่อนไขที่ทำให้การค้นตามความกว้างใช้ได้
ลองนับดูว่าสถานะที่ต่างกันจริง ๆ มีกี่แบบ แล้วเทียบกับจำนวนลำดับท่าที่เป็นไปได้ ซึ่งโตแบบทวีคูณ
สามข้อนี้ตอบคนละคำถาม ข้อแรกถามว่า "สถานะที่เห็นชัดอยู่แล้วเขียนยังไง" ข้อสองถามว่า "ถ้าสถานะไม่ใช่ตำแหน่งบนแผนที่" ข้อสามถามว่า "ถ้าบางสถานะเข้าไม่ได้เลย" ลองคิดเองก่อนแล้วค่อยแตะดูเฉลยครับ
โจทย์กำหนด
ให้กริด R คูณ C ที่ # คือกำแพง . คือช่องว่าง
S คือจุดเริ่ม และ T คือจุดหมาย เดินได้สี่ทิศ ถามว่าต้องเดินอย่างน้อยกี่ก้าว
ถ้าไปไม่ถึงให้ตอบ -1 โดย R, C ≤ 1 000
| Input | Output |
|---|---|
| 5 5 S..#. .#..# .#.#. ...#T .#... | 9 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนแถวกับจำนวนคอลัมน์ จากนั้นคือกริดทีละแถว เอาต์พุตคือจำนวนก้าว
ไม่ใช่จำนวนช่องที่เดินผ่าน ซึ่งต่างกันหนึ่ง เพราะยืนอยู่ที่ S ตั้งแต่ต้นโดยยังไม่ได้เดิน
เดินได้สี่ทิศแปลว่าห้ามเดินทแยง ชุดนี้เดินจาก S ไป T ได้สั้นสุด
9 ก้าว โดยผ่าน 10 ช่องรวมช่องเริ่มกับช่องจบ
คำใบ้
ข้อนี้สถานะคือตำแหน่งตรง ๆ ไม่ต้องแปลงอะไร แต่มีจุดหนึ่งที่ทำให้ไม่ต้องเขียนเงื่อนไขพิเศษสำหรับกรณี "ไปไม่ถึง" เลย ลองดูว่าค่าตั้งต้นของตารางระยะควรเป็นเท่าไร
ที่มาของท่านี้
ท่านี้ไม่ได้เริ่มจากการเลือกอัลกอริทึม เพราะอัลกอริทึมมีตัวเดียวและสั้นสิบบรรทัด มันเริ่มจากการตอบคำถามสองข้อให้ได้ก่อน และถ้าตอบผิดข้อใดข้อหนึ่ง โค้ดที่เขียนถูกทุกบรรทัดก็ยังตอบผิด
ข้อแรก ทุกท่าราคาเท่ากันจริงไหม การค้นตามความกว้างเชื่อว่าสถานะที่เจอก่อนคือสถานะที่ถึงได้เร็วที่สุด ความเชื่อนี้ตั้งอยู่บนราคาที่เท่ากันล้วน ๆ ถ้าโจทย์บอกว่ามีท่าหนึ่งราคาสองและอีกท่าราคาหนึ่ง มันจะยังรันจบ ยังคืนตัวเลขหน้าตาน่าเชื่อ และตัวเลขนั้นผิด งานแบบนั้นเป็นของไดค์สตรา ไม่ใช่ของท่านี้
ข้อสอง สถานะที่เลือกไว้ พอจะเดินต่อโดยไม่ต้องรู้อดีตหรือยัง ข้อนี้สำคัญเพราะเราทำเครื่องหมายว่า "เคยมาแล้ว" แล้วไม่กลับมาอีก ถ้าสถานะเก็บข้อมูลไม่พอ สองเส้นทางที่ต่างกันจริงจะถูกนับเป็นอันเดียวกัน แล้วเส้นทางที่ยังไปต่อได้จะถูกตัดทิ้งเพราะเข้าใจผิดว่าซ้ำ
บทเรียนที่ยกไปข้ออื่นได้คือ ในโจทย์กลุ่มนี้ เวลาที่ควรลงไปกับการเขียนนิยามของสถานะออกมาเป็นตัวหนังสือ มีค่ามากกว่าเวลาที่ลงไปกับโค้ด เพราะโค้ดชุดเดิมใช้ซ้ำได้ทุกข้อ แต่นิยามสถานะเปลี่ยนไปทุกข้อ
ตั้งค่าเริ่มต้นของตารางระยะไว้ที่ -1 ซึ่งทำหน้าที่สองอย่างพร้อมกัน คือแปลว่า "ยังไม่เคยไปถึง"
ระหว่างการค้น และแปลว่า "ไปไม่ถึงเลย" ตอนพิมพ์คำตอบ ไม่ต้องมีเงื่อนไข if เพิ่มสักบรรทัด
#include <bits/stdc++.h>
using namespace std;
int main() {
int R, C;
scanf("%d %d", &R, &C);
vector<string> g(R);
for (auto& s : g) { char b[1024]; scanf("%s", b); s = b; }
int sr = 0, sc = 0, tr = 0, tc = 0;
for (int i = 0; i < R; i++)
for (int j = 0; j < C; j++) {
if (g[i][j] == 'S') { sr = i; sc = j; }
if (g[i][j] == 'T') { tr = i; tc = j; }
}
vector<vector<int>> dist(R, vector<int>(C, -1));
queue<pair<int, int>> q;
dist[sr][sc] = 0;
q.push({sr, sc});
const int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
while (!q.empty()) {
auto [x, y] = q.front(); q.pop();
for (int k = 0; k < 4; k++) {
int nx = x + dx[k], ny = y + dy[k];
if (nx < 0 || ny < 0 || nx >= R || ny >= C) continue;
if (g[nx][ny] == '#') continue;
if (dist[nx][ny] != -1) continue;
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
}
printf("%d\n", dist[tr][tc]); // ไปไม่ถึงจะยังเป็น -1 อยู่พอดี
return 0;
} สุ่มเทียบ 400 เทส กับตัวตรวจที่ไม่ใช้คิวเลย คือไล่ผ่อนคลายค่าไปเรื่อย ๆ จนไม่มีอะไรเปลี่ยน ซึ่งช้ากว่ามากแต่ถูกต้องโดยไม่ขึ้นกับลำดับในคิว ตรงกันทั้งหมด รวมกรณีที่กำแพงล้อมจุดเริ่มจนออกไปไหนไม่ได้
โจทย์กำหนด
ให้เหยือกความจุ X กับ Y ลิตร เริ่มต้นว่างทั้งคู่ ทำได้หกท่าคือเติมใบใดใบหนึ่งให้เต็ม
เททิ้งใบใดใบหนึ่ง และเทใบหนึ่งใส่อีกใบจนใบรับเต็มหรือใบเทหมด ถามว่าอย่างน้อยกี่ท่าถึงจะมีเหยือกใบใดใบหนึ่ง
เก็บน้ำได้ Z ลิตรพอดี ทำไม่ได้ให้ตอบ -1
| Input | Output |
|---|---|
| 5 3 4 | 6 |
อ่านตัวอย่างนี้ยังไง
อินพุตมีสามเลขคือความจุของเหยือกสองใบ แล้วปริมาณที่ต้องการ เอาต์พุตคือจำนวนท่า ที่ทำน้อยที่สุด ไม่ใช่ปริมาณน้ำ และการเทน้ำระหว่างเหยือกหนึ่งครั้งนับเป็นหนึ่งท่า ไม่ว่าจะเทไปกี่ลิตรก็ตาม
ข้อนี้ไม่มีอะไรให้วัดด้วยมือ สิ่งที่ต้องมองให้เห็นคือ "สภาพตอนนี้" ของทั้งระบบ ซึ่งคือคู่ตัวเลข (น้ำในใบใหญ่, น้ำในใบเล็ก) พอเขียนแบบนี้ สภาพที่เป็นไปได้ก็มีจำกัดแค่ 24 แบบ และหนึ่งท่าคือการกระโดดจากคู่หนึ่งไปอีกคู่หนึ่ง ชุดนี้ทำได้ใน 6 ท่า
คำใบ้
โจทย์ไม่ได้พูดถึงกราฟเลยสักคำ แต่ถ้าเขียน "ปริมาณน้ำตอนนี้" ลงกระดาษเป็นคู่ตัวเลข แล้วลากลูกศรไปยัง ทุกคู่ที่ทำต่อได้ คุณจะได้กราฟมาแล้วหนึ่งอัน เหลือแค่ต้องแปลงคู่ตัวเลขเป็นดัชนีของอาเรย์
สถานะคือ (a, b) โดย 0 ≤ a ≤ X และ 0 ≤ b ≤ Y มีทั้งหมด
(X + 1)(Y + 1) อัน อัดเป็นเลขตัวเดียวด้วย a × (Y + 1) + b แล้วแตกกลับด้วยการหาร
กับหารเอาเศษ ท่าที่ยากที่สุดคือการเท ซึ่งย้ายน้ำได้เท่ากับค่าที่น้อยกว่าระหว่างน้ำที่มีในใบเท
กับที่ว่างที่เหลือในใบรับ
#include <bits/stdc++.h>
using namespace std;
int main() {
int X, Y, Z;
scanf("%d %d %d", &X, &Y, &Z);
// สถานะคือ (a, b) ปริมาณน้ำในใบเล็กและใบใหญ่ อัดเป็นเลขตัวเดียว a*(Y+1)+b
int S = (X + 1) * (Y + 1);
vector<int> dist(S, -1);
auto id = [&](int a, int b) { return a * (Y + 1) + b; };
queue<int> q;
dist[id(0, 0)] = 0;
q.push(id(0, 0));
int ans = -1;
while (!q.empty()) {
int cur = q.front(); q.pop();
int a = cur / (Y + 1), b = cur % (Y + 1);
if (a == Z || b == Z) { ans = dist[cur]; break; }
int t = min(a, Y - b); // เทใบเล็กใส่ใบใหญ่ ได้เท่าที่ใบใหญ่ยังว่าง
int t2 = min(b, X - a); // เทใบใหญ่ใส่ใบเล็ก
int nx[6][2] = {{X, b}, {a, Y}, {0, b}, {a, 0}, {a - t, b + t}, {a + t2, b - t2}};
for (auto& p : nx) {
int j = id(p[0], p[1]);
if (dist[j] == -1) { dist[j] = dist[cur] + 1; q.push(j); }
}
}
printf("%d\n", ans);
return 0;
}
สุ่มเทียบ 400 เทส กับตัวตรวจแบบผ่อนคลายค่าเช่นเดิม รวมกรณี Z = 0 ซึ่งตอบ 0 ตั้งแต่ยังไม่ทำอะไร
และกรณี Z ที่ใหญ่กว่าเหยือกทั้งสองใบซึ่งตอบ -1 ตรงกันทั้งหมด
โจทย์กำหนด
ล็อกมีวงล้อสี่วง แต่ละวงเป็นเลข 0 ถึง 9 หมุนวนได้ทั้งสองทาง (จาก 9 หมุนขึ้นได้ 0) หนึ่งท่าคือหมุนวงเดียว
ขึ้นหรือลงหนึ่งขั้น เริ่มที่ 0000 มีรายการรหัสต้องห้าม m ตัวที่ห้ามให้ล็อกอยู่ในสภาพนั้น
แม้แต่ระหว่างทาง ถามว่าหมุนอย่างน้อยกี่ครั้งถึงจะได้รหัสเป้าหมาย ทำไม่ได้ให้ตอบ -1
| Input | Output |
|---|---|
| 5 0201 0101 0102 1212 2002 0202 | 6 |
อ่านตัวอย่างนี้ยังไง
บรรทัดแรกคือจำนวนรหัสต้องห้าม แล้วรายการรหัสนั้น แล้วรหัสเป้าหมาย เอาต์พุตคือ จำนวนครั้งที่หมุน โดยหมุนวงเดียวขึ้นหรือลงหนึ่งขั้นนับเป็นหนึ่งครั้ง และวงล้อหมุนวนได้ จาก 9 ขึ้นไปเป็น 0 ได้ในครั้งเดียว
ถ้าไม่มีรหัสต้องห้ามเลย ข้อนี้ไม่ต้องค้นอะไร แค่คิดทีละวงว่าหมุนขึ้นหรือลงอันไหนใกล้กว่า รหัส 0202 จะใช้แค่ 4 ครั้ง แต่รายการต้องห้ามบังคับให้ต้องอ้อม คำตอบจริงจึงเป็น 6 ครั้ง
คำว่า "ห้ามอยู่ในสภาพนั้นแม้แต่ระหว่างทาง" คือหัวใจ รหัสต้องห้ามไม่ได้ห้ามแค่เป็นคำตอบ มันห้ามเหยียบผ่านด้วย
คำใบ้
รหัสสี่หลักเป็นเลข 0 ถึง 9999 อยู่แล้ว จึงเป็นดัชนีของอาเรย์ได้เลยโดยไม่ต้องแปลงอะไร ส่วนรหัสต้องห้ามไม่ใช่กรณีพิเศษ มันแค่เป็นสถานะที่ไม่มีเส้นเชื่อมเข้าไป แล้วมีกรณีหนึ่งที่ต้องตอบก่อนเริ่มค้นด้วยซ้ำ ลองหาให้เจอ
กรณีที่ต้องดักก่อนคือ 0000 เองอยู่ในรายการต้องห้าม ซึ่งแปลว่าเราออกตัวไม่ได้เลย ต้องตอบ
-1 ทันที ถ้าไม่ดัก โค้ดจะเริ่มค้นจากสถานะที่ผิดกติกาแล้วอาจตอบเป็นตัวเลขออกมา
ส่วนการหมุนหนึ่งขั้น เขียนได้สะอาดด้วยการมองเลขสี่หลักเป็นผลรวมของหลักคูณค่าประจำหลัก
ดึงหลักที่ k ออกมาด้วยการหารแล้วหารเอาเศษ เปลี่ยนค่าแล้วบวกกลับด้วย
ส่วนต่างคูณค่าประจำหลัก ไม่ต้องแปลงเป็นสตริงเลย ทั้งข้อมี 10 000 สถานะ สถานะละ 8 ท่า
จึงจบเร็วมาก
#include <bits/stdc++.h>
using namespace std;
int main() {
char tgt[16];
int m;
scanf("%s %d", tgt, &m);
vector<char> bad(10000, 0);
for (int i = 0; i < m; i++) { char s[16]; scanf("%s", s); bad[atoi(s)] = 1; }
int target = atoi(tgt);
if (bad[0]) { printf("-1\n"); return 0; } // ออกจากจุดเริ่มไม่ได้เลย
vector<int> dist(10000, -1);
queue<int> q;
dist[0] = 0;
q.push(0);
while (!q.empty()) {
int v = q.front(); q.pop();
if (v == target) break;
int p[4] = {1000, 100, 10, 1};
for (int k = 0; k < 4; k++) {
int digit = (v / p[k]) % 10;
for (int d : {1, 9}) { // หมุนขึ้นหนึ่ง กับหมุนลงหนึ่ง (คือขึ้นเก้าแบบวน)
int nd = (digit + d) % 10;
int u = v + (nd - digit) * p[k];
if (bad[u] || dist[u] != -1) continue;
dist[u] = dist[v] + 1;
q.push(u);
}
}
}
printf("%d\n", dist[target]);
return 0;
}
สุ่มเทียบ 200 เทส กับตัวตรวจแบบผ่อนคลายค่าซึ่งกวาดทั้ง 10 000 สถานะซ้ำจนนิ่ง โดยจงใจให้รหัสต้องห้าม
บางเทสไปตกที่ 0000 พอดี ตรงกันทั้งหมด
ระวัง
การหมุนลงหนึ่งขั้นเขียนเป็น (digit - 1) % 10 ไม่ได้ เพราะภาษา C++ ให้เศษที่ติดลบ
เมื่อตัวตั้งติดลบ ตอน digit เป็น 0 จะได้ -1 แล้วดัชนีหลุดขอบอาเรย์
ท่าที่ปลอดภัยคือบวก 9 แทนการลบ 1 ซึ่งให้ผลเดียวกันในโลกที่วนครบสิบ
ทั้งบทนี้ยืนอยู่บนสมมติฐานเงียบ ๆ ข้อหนึ่ง คือทุกท่ามีราคาเท่ากันคือหนึ่ง สมมติฐานนั้นคือเหตุผลเดียวที่ BFS ใช้ได้ เพราะเมื่อทุกก้าวราคาเท่ากัน ลำดับที่เราไปเจอสถานะจึงเรียงตามระยะทางพอดี คิวจึงไม่มีวันเสียลำดับ
พอโจทย์เปลี่ยนให้บางท่าราคาต่างกัน สมมติฐานนั้นหายไป และ BFS จะให้คำตอบผิดแบบเงียบ ๆ ไม่ใช่พังหรือวนไม่จบ มันจะตอบเลขที่น้อยกว่าความจริง เพราะมันไปถึงสถานะหนึ่งด้วยเส้นทางที่ ก้าวน้อยแต่แพงกว่า แล้วปิดตายสถานะนั้นไปเลย ทั้งที่มีเส้นทางที่ก้าวมากกว่าแต่ถูกกว่ารออยู่ นี่คือบั๊กประเภทที่ตัวอย่างในโจทย์มักจับไม่ได้
กรณีที่เจอบ่อยที่สุดคือราคามีแค่สองแบบ คือ 0 กับ 1 เช่นโจทย์ที่เดินในทิศเดิมฟรี
แต่เลี้ยวเสียหนึ่งครั้ง หรือผ่านช่องว่างฟรีแต่ทุบกำแพงเสียหนึ่ง กรณีนี้ไม่ต้องขยับไปใช้ Dijkstra
ที่มีคิวลำดับความสำคัญและ log ติดมาด้วย มีท่าที่แก้ด้วยการเปลี่ยนคิวเป็น
แถวสองหัว เท่านั้น เรียกว่า 0-1 BFS
กฎมีบรรทัดเดียว เมื่อผ่อนคลายเส้นที่ราคา 0 ให้ใส่ปมปลายทางไว้ที่หัวแถว เมื่อผ่อนคลายเส้นที่ราคา 1 ให้ใส่ไว้ที่ท้ายแถว เหตุผลตรงไปตรงมา เส้นราคา 0 ไม่เพิ่มระยะเลย ปมปลายทางจึงต้องอยู่ในชั้นเดียวกับปมที่เรากำลังยืน มันจึงต้องได้คิวก่อนทุกปมที่อยู่ชั้นถัดไป และเส้นราคา 1 พาไปชั้นถัดไปพอดี จึงต่อท้ายเหมือน BFS เดิม ผลคือแถวยังเรียงตามระยะทางเสมอ เหมือนที่คิวเคยเรียงให้ตอนทุกก้าวราคาเท่ากัน
// 0-1 BFS: ระยะทางสั้นสุดเมื่อเส้นเชื่อมมีน้ำหนักได้แค่ 0 หรือ 1
// ใช้แถวสองหัว เส้นน้ำหนัก 0 ใส่หัวแถว เส้นน้ำหนัก 1 ใส่ท้ายแถว
// อินพุต: n m start แล้ว m บรรทัด u v w (กราฟไม่มีทิศ, w เป็น 0 หรือ 1)
// เอาต์พุต: ระยะจาก start ถึงทุกปม ปมที่ไปไม่ถึงพิมพ์ -1
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m, start;
if (scanf("%d %d %d", &n, &m, &start) != 3) return 0;
vector<vector<pair<int, int>>> g(n);
for (int i = 0; i < m; i++) {
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
g[u].push_back({v, w});
g[v].push_back({u, w});
}
const int INF = INT_MAX;
vector<int> dist(n, INF);
deque<int> dq;
dist[start] = 0;
dq.push_back(start);
while (!dq.empty()) {
int u = dq.front();
dq.pop_front();
for (auto [v, w] : g[u]) {
// ถ้าเส้นนี้ไม่เพิ่มระยะเลย ปมปลายทางต้องได้คิวเท่ากับปมนี้ จึงต้องอยู่หัวแถว
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (w == 0) dq.push_front(v);
else dq.push_back(v);
}
}
}
for (int i = 0; i < n; i++) printf("%d%c", dist[i] == INF ? -1 : dist[i], i + 1 == n ? '\n' : ' ');
return 0;
} เกณฑ์เลือกจึงเรียงได้เป็นบันได ราคาเท่ากันหมดให้ใช้ BFS ราคาเป็น 0 กับ 1 ให้ใช้ 0-1 BFS ราคาเป็นจำนวนเต็มเล็ก ๆ หลายค่าให้ใช้คิวหลายชั้น และราคาเป็นอะไรก็ได้ให้ใช้ Dijkstra สิ่งที่ต้องจำคืออย่าใช้ BFS กับกราฟที่ราคาไม่เท่ากัน เพราะมันไม่เตือนอะไรเลย
ถ้าโจทย์ถามว่า "อย่างน้อยกี่ท่า" และทุกท่าราคาเท่ากัน ให้หยุดคิดเรื่องอัลกอริทึมทันที แล้วไปตอบสองคำถามแทน ว่าสถานะคืออะไรกับท่าหนึ่งท่าพาไปไหนได้บ้าง ที่เหลือคือโครงสิบบรรทัดที่เหมือนกันทุกข้อ และถ้านับสถานะแล้วได้ตัวเลขที่ใหญ่เกินสร้างอาเรย์ นั่นไม่ใช่สัญญาณว่าต้องหาอัลกอริทึมที่แรงกว่า แต่เป็นสัญญาณว่าอ่านโจทย์ยังไม่ครบ
ในหน้านี้