programming.in.th · ข้อ 2014
ข้อที่ประโยคเฉลยสั้นที่สุดในคลังนี้ คือกำแพงรอดเมื่อสองข้างท่วมพร้อมกัน ที่เหลือคือ BFS ธรรมดา บทนี้ให้ทั้งเวอร์ชันตารางที่ได้ 40 คะแนนและใช้เป็นตัวตรวจสอบ กับเวอร์ชันเต็มที่ต้องสร้างพื้นที่ของผังเอง
ปี 1964 เกิดน้ำท่วมใหญ่ที่เมืองซาเกร็บ โจทย์นี้ให้แบบจำลองของเมืองก่อนน้ำท่วม
เป็นจุด N จุดบนระนาบ กับกำแพง W เส้น แต่ละเส้นเชื่อมจุดสองจุดและ
ไม่ผ่านจุดอื่นเลย กำแพงทุกเส้นขนานกับแกน และไม่มีเส้นไหนตัดหรือทับกัน
(แตะกันได้แค่ที่ปลาย)
ตอนเริ่มต้นทุกที่แห้ง แล้วน้ำท่วมบริเวณนอกสุดทันที หลังจากนั้นทุก ๆ หนึ่งชั่วโมง กำแพงทุกเส้นที่ข้างหนึ่งเป็นน้ำ อีกข้างเป็นอากาศ จะพังลง แล้วน้ำก็ไหลเข้าไปในพื้นที่ที่เพิ่งเปิด ทำซ้ำจนน้ำท่วมทั่ว งานของเราคือบอกว่ากำแพงเส้นไหนบ้างที่ยังยืนอยู่
อินพุต / ขอบเขต / เอาต์พุต
N ตามด้วย N บรรทัดของพิกัดจุด
จากนั้นคือ W และอีก W บรรทัดของหมายเลขจุดสองจุดที่กำแพงเส้นนั้นเชื่อม
2 ≤ N ≤ 100,000, 1 ≤ W ≤ 2N
พิกัดอยู่ระหว่าง 0 ถึง 1,000,000 เวลา 2 วินาที หน่วยความจำ 32 เมกะไบต์
| Input | Output |
|---|---|
| 15 1 1 8 1 4 2 7 2 2 3 4 3 6 3 2 5 4 5 6 5 4 6 7 6 1 8 4 8 8 8 17 1 2 2 15 15 14 14 13 13 1 14 11 11 12 12 4 4 3 3 6 6 5 5 8 8 9 9 11 9 10 10 7 7 6 | 4 6 15 16 17 |
ใบ้
กำแพงพังเมื่อข้างหนึ่งเปียก อีกข้างแห้ง ลองกลับด้านประโยคนี้ดู กำแพงจะไม่พัง เมื่อไร
ถ้าเราหาได้ว่าแต่ละบริเวณน้ำถึงเมื่อชั่วโมงที่เท่าไร คำถามทั้งข้อจะเหลืออะไร
ผังนี้มีห้องปิดหนึ่งห้อง กับกำแพงเดี่ยว ๆ ที่ไม่ได้ล้อมอะไรเลย
กดเลือกกำแพงที่คิดว่าจะรอด แล้วกดตรวจ กำแพงที่ไม่ได้เลือกคือกำแพงที่คิดว่าจะพัง
เลือกไว้ 0 เส้น จากทั้งหมด 0 เส้น
น้ำเริ่มจากทะเลรอบนอก และทะลุกำแพงได้ชั่วโมงละหนึ่งชั้น
ถ้าทายพลาดบ่อย ลองเปลี่ยนคำถามในหัวจาก "กำแพงนี้แข็งแรงไหม" เป็น "น้ำสองข้างของกำแพงนี้มาถึงพร้อมกันหรือเปล่า"
ที่มาของแนวคิดนี้ และจุดที่ผมสะดุด
ลำดับจริงคือผมไม่ได้เริ่มจากกราฟระนาบเลย ผมเริ่มจากเวอร์ชันง่ายที่สุดคือมองผังเป็นตารางช่องละหนึ่งหน่วย แล้วปล่อยน้ำท่วมทีละชั้น เวอร์ชันนั้นเขียนง่าย ตอบตัวอย่างในโจทย์ถูก และกลายเป็นเฉลยชุดทดสอบย่อย 40 คะแนน พอมันเดินได้ ผมถึงกลับไปอ่านประโยคที่มันใช้ตัดสินใจ คือ "กำแพงรอดเมื่อสองข้างท่วมพร้อมกัน" แล้วเห็นว่าประโยคเดียวกันนี้ไม่ได้ต้องการช่องเลย มันต้องการแค่คำว่าสองข้าง ซึ่งหน้าของกราฟระนาบก็มีให้เหมือนกัน
จุดที่ผมสะดุดอยู่นานคือเรื่องราคา กำแพงหนึ่งเส้นพังทั้งเส้นพร้อมกัน ไม่ได้พังทีละช่อง ตอนนั้นผมนึกว่าการคิดราคาทีละช่องจะทำให้ตอบผิด สิ่งที่ทำให้กลับมามั่นใจคือ ช่องที่อยู่ในพื้นที่ผืนเดียวกันเชื่อมถึงกันด้วยการเดินที่มีราคาศูนย์ ค่าที่คำนวณได้ของทุกช่องในผืนเดียวกันจึง เท่ากันเสมอโดยอัตโนมัติ การคิดทีละช่องกับการคิดทีละผืนจึงให้เลขเดียวกัน
บทเรียนที่ยกไปข้ออื่นได้คือ เขียนเวอร์ชันหยาบที่ถูกให้เดินได้ก่อน แล้วอ่านว่ามันใช้ประโยคไหนตัดสินใจ ประโยคนั้นมักไม่ผูกกับโครงสร้างหยาบ ๆ ที่เราใช้ และนั่นคือทางไปเวอร์ชันเต็ม
กำแพงเส้นหนึ่งกั้นพื้นที่สองผืน ให้ dist ของผืนหนึ่งคือชั่วโมงที่น้ำท่วมถึงผืนนั้น
ถ้าสองผืนมีค่าต่างกัน ก็ต้องเคยมีช่วงเวลาที่ผืนหนึ่งเป็นน้ำ อีกผืนยังเป็นอากาศ กำแพงจึงพัง
แต่ถ้าสองผืนท่วมพร้อมกันเป๊ะ กำแพงเส้นนั้นไม่เคยรับแรงต่างข้างเลยสักชั่วโมง
กำแพงยังยืนอยู่ ก็ต่อเมื่อพื้นที่สองข้างของมันมีชั่วโมงที่น้ำท่วมถึงเท่ากันพอดี
แล้ว dist หาได้ยังไง มองว่าการเดินจากพื้นที่หนึ่งไปพื้นที่ที่ติดกันต้องข้ามกำแพงหนึ่งเส้น
ซึ่งราคาหนึ่งชั่วโมง เริ่มจากพื้นที่นอกสุดที่ dist เป็นศูนย์ แล้วค้นตามความกว้าง
ออกไป นี่คือ BFS ธรรมดา ไม่ต้องใช้อะไรหรูกว่านั้นเลย
// dist[f] = ชั่วโมงที่น้ำท่วมถึงพื้นที่ f หาได้ด้วย BFS ธรรมดาจากพื้นที่นอกสุด
// กำแพงเส้นหนึ่งกั้นพื้นที่สองผืน ถ้าน้ำถึงสองผืนนั้นคนละชั่วโมง แปลว่าเคยมีจังหวะที่
// ข้างหนึ่งเป็นน้ำอีกข้างเป็นอากาศ กำแพงจึงพัง ถ้าถึงพร้อมกัน กำแพงไม่เคยรับแรงต่างข้าง
for (int e = 0; e < w; e++) {
int a = find(heFace[2 * e]), b = find(heFace[2 * e + 1]);
if (dist[a] == dist[b]) ans.push_back(e + 1);
} กดถัดไปเพื่อตรวจกำแพงทีละเส้น
| เส้นที่ | เชื่อมจุด | น้ำถึงฝั่งหนึ่ง | น้ำถึงอีกฝั่ง | ผล |
|---|---|---|---|---|
| 1 | 1 ถึง 2 | 0 | 1 | พัง |
| 2 | 2 ถึง 15 | 1 | 0 | พัง |
| 3 | 15 ถึง 14 | 1 | 0 | พัง |
| 4 | 14 ถึง 13 | 1 | 0 | พัง |
| 5 | 13 ถึง 1 | 0 | 1 | พัง |
| 6 | 14 ถึง 11 | 1 | 1 | รอด |
| 7 | 11 ถึง 12 | 2 | 1 | พัง |
| 8 | 12 ถึง 4 | 2 | 1 | พัง |
| 9 | 4 ถึง 3 | 1 | 2 | พัง |
| 10 | 3 ถึง 6 | 1 | 2 | พัง |
| 11 | 6 ถึง 5 | 1 | 2 | พัง |
| 12 | 5 ถึง 8 | 1 | 2 | พัง |
| 13 | 8 ถึง 9 | 2 | 1 | พัง |
| 14 | 9 ถึง 11 | 1 | 2 | พัง |
| 15 | 9 ถึง 10 | 2 | 2 | รอด |
| 16 | 10 ถึง 7 | 2 | 2 | รอด |
| 17 | 7 ถึง 6 | 2 | 2 | รอด |
กำแพงที่รอดมี 4 เส้น คือเส้นที่ 6, 15, 16, 17 ตรงกับคำตอบในโจทย์ ตัวเลขทุกตัวมาจากการเดินน้ำจริงบนตารางในหน้านี้ ไม่ได้พิมพ์มือ น้ำใช้เวลาท่วมทั่วทั้งเมืองนี้ 2 ชั่วโมง
สังเกตว่าเส้นที่ 6 กับ 15 กับ 16 กับ 17 ล้วนเป็นเส้นที่ทั้งสองข้างเป็นพื้นที่ผืนเดียวกัน หรือเป็นผืนที่น้ำเดินไปถึงด้วยจำนวนกำแพงเท่ากันพอดีทั้งสองทาง
ชุดทดสอบย่อยให้พิกัดไม่เกิน 500 ซึ่งเป็นคำเชิญให้ทำอะไรที่ตรงไปตรงมามาก แบ่งระนาบเป็นช่องละหนึ่งหน่วย กำแพงกลายเป็นเส้นขอบระหว่างช่อง แล้วเดินน้ำบนช่อง โดยการเดินข้ามขอบที่ไม่มีกำแพงราคาศูนย์ และข้ามขอบที่มีกำแพงราคาหนึ่ง
การเดินที่มีราคาแค่ศูนย์กับหนึ่งใช้ BFS แบบสองแถว ได้ คือใช้คิวสองหัว เจอราคาศูนย์ก็ยัดหน้าคิว เจอราคาหนึ่งก็ต่อท้าย ได้ผลเหมือน Dijkstra แต่เร็วเท่า BFS
โน้ต
มีจุดหนึ่งที่ดูเหมือนจะผิดแต่ไม่ผิด กำแพงพังทั้งเส้นพร้อมกัน ไม่ใช่พังทีละช่อง แล้วทำไมการคิดราคาทีละช่องถึงยังถูก
เพราะช่องที่อยู่ในพื้นที่ผืนเดียวกันเชื่อมถึงกันด้วยการเดินราคาศูนย์ทั้งหมด มันจึงได้ค่าเท่ากันเสมออยู่แล้ว และเพราะโจทย์รับประกันว่ากำแพงไม่ตัดกันและไม่มีจุดอื่นอยู่กลางเส้น กำแพงหนึ่งเส้นจึงกั้นพื้นที่ สองผืนเดิมตลอดความยาวของมัน ทุกช่องคู่ตลอดเส้นจึงตัดสินเหมือนกันหมด
ท่านี้ได้ 40 คะแนนและตายทันทีที่พิกัดขึ้นเป็นล้าน แต่มันมีค่ามากกว่านั้น เพราะมันเป็นตัวตรวจสอบ ให้เวอร์ชันเต็ม ผมสุ่มผังกำแพงบนตารางเล็ก ๆ 3,000 ชุด (โดยใช้เส้นยาวหนึ่งหน่วยบนแลตทิซ ซึ่งไม่มีทางตัดกันเอง และสร้างผังซ้อนในผังได้ตามธรรมชาติ) แล้วเทียบสองโปรแกรม ตรงกันหมด
ปัญหาที่โผล่มาตอนออกแบบเวอร์ชันนี้
ตอนแรกผมคิดว่าเวอร์ชันเต็มคือแค่เปลี่ยนคำว่าช่องเป็นคำว่าหน้า แล้วจบ จนกระทั่งเจอว่าผังไม่จำเป็นต้องเป็นก้อนเดียว มันแตกเป็นหลายก้อนได้ และก้อนหนึ่ง อาจอยู่ข้างในพื้นที่ปิดของอีกก้อนได้ด้วย ถ้าเหมารวมว่าขอบนอกของทุกก้อนคือทะเล คำตอบของผังซ้อนผังจะผิดทันที เพราะน้ำจากทะเลไปไม่ถึงก้อนข้างในจริง ๆ
ทางแก้คือยิงรังสีลงล่างจากจุดต่ำสุดของขอบนอกแต่ละก้อน แล้วเอาก้อนนั้นไปรวมกับพื้นที่ที่อยู่เหนือกำแพงแนวนอนเส้นที่รังสีชนก่อน ถ้าไม่ชนอะไรเลยก็แปลว่าก้อนนั้นติดทะเลจริง
ตอนตรวจ ผมสุ่มผังบนแลตทิซ 3,000 ชุด โดยใช้เส้นยาวหนึ่งหน่วยซึ่งไม่มีทางตัดกันเอง และสร้างผังซ้อนผังขึ้นมาได้เองตามธรรมชาติ แล้วเทียบเวอร์ชันหน้ากับเวอร์ชันตาราง ตรงกันหมด ตรงนี้สำคัญตรงที่ตัวเทียบเป็นคนละโมเดลกันจริง ๆ ตัวหนึ่งท่วมบนตาราง อีกตัวท่วมบนหน้าของกราฟ
บทเรียนที่ยกไปข้ออื่นได้คือ เวลาสุ่มเคสมาเทียบ ให้ออกแบบตัวสุ่มให้ผลิตโครงสร้างที่เรากลัวที่สุดออกมาได้เอง ไม่งั้นสุ่มเป็นหมื่นชุดก็ไม่เคยเจอผังซ้อนผังสักชุด
พิกัดเป็นล้านจึงแบ่งช่องไม่ได้ ต้องหาพื้นที่ของผังจริง ๆ ซึ่งทำได้เพราะกำแพงไม่ตัดกัน ผังนี้จึงเป็นกราฟระนาบ วิธีมาตรฐานคือแตกกำแพงแต่ละเส้นเป็นครึ่งเส้นสองอัน แล้วเดินไปตามครึ่งเส้นโดยให้พื้นที่อยู่ทางซ้ายมือเสมอ พอเดินวนกลับที่เดิมก็ได้ขอบของพื้นที่หนึ่งผืน
เพราะกำแพงขนานแกน แต่ละจุดมีเพื่อนบ้านได้แค่สี่ทิศ การหาว่า "ครึ่งเส้นถัดไปคืออันไหน" จึงเป็นแค่การกวาดทิศทวนเข็มจากทิศที่ย้อนกลับ
ระวัง
ผังอาจแตกเป็นหลายก้อนที่ไม่ต่อกัน และก้อนหนึ่งอาจอยู่ข้างในพื้นที่ปิดของอีกก้อน ถ้าเหมารวมว่าขอบนอกของทุกก้อนคือทะเล คำตอบจะผิดสำหรับผังซ้อนผัง
วิธีแก้ที่ผมใช้คือ ยิงรังสีลงล่างจากจุดต่ำสุดของขอบนอกแต่ละก้อน แล้วดูว่าไปโดนกำแพงแนวนอนเส้นไหนก่อน พื้นที่ที่อยู่เหนือกำแพงเส้นนั้นคือพื้นที่ที่ห่อก้อนนี้ไว้ แล้วรวมสองอันเข้าด้วยกัน ถ้ายิงแล้วไม่โดนอะไรเลย ก็แปลว่าก้อนนี้อยู่ติดทะเลจริง
เวลาที่วัดได้บนเครื่องผม ผังตารางแลตทิซที่มีจุดราวห้าหมื่นจุดและกำแพงเกือบแสนเส้น ใช้เวลา 170 มิลลิวินาที จากลิมิตสองวินาที
ถ้ายังไม่คุ้นกับการมองปัญหาให้กลายเป็นกราฟที่โจทย์ไม่ได้ให้มา ลองอ่าน กราฟที่โจทย์ไม่ได้ให้มา ซึ่งมีหัวข้อ BFS แบบสองแถวที่ข้อนี้ใช้ด้วย
ข้อนี้ยืนอยู่บนสองท่า ท่าแรกคือเรามองกำแพงกับพื้นที่เป็นรูปทรงบนระนาบ แล้วถามคำถามเรื่องรูปทรงเหล่านั้น ซึ่งเรียกรวม ๆ ว่าเรขาคณิตเชิงคำนวณ (computational geometry) ท่าที่สองคือการไล่ระบายพื้นที่ที่ติดกัน ให้เป็นก้อนเดียว ซึ่งทำด้วยการไล่ลึก (DFS) หรือการไล่กว้าง (BFS) ก็ได้ผลเหมือนกัน เพราะเราสนใจแค่ว่าช่องไหนติดกัน ไม่ได้สนใจระยะทาง
ทบทวนพื้นฐาน · เรขาคณิตเชิงคำนวณ
ที่โจทย์เรขาคณิตน่ากลัวกว่าที่ควร เพราะคนมักรีบกระโดดไปคิดเป็นพิกัดทศนิยม แล้วไปเจอปัญหาความคลาดเคลื่อน ท่าที่ปลอดภัยกว่าคือถามก่อนว่าคำตอบขึ้นกับพิกัดจริง หรือขึ้นกับความสัมพันธ์เท่านั้น ข้อนี้เป็นแบบหลัง เราไม่ต้องรู้ว่าน้ำสูงเท่าไรเป็นเลขทศนิยม รู้แค่ว่ากำแพงเส้นไหนเปียกก่อนเส้นไหนก็พอ พอถามแบบนั้นได้ ปัญหาเรขาคณิตก็ยุบเป็นปัญหากราฟที่คิดด้วยจำนวนเต็มได้หมด
อีกท่าที่ใช้ซ้ำได้คือเปลี่ยนพื้นที่ต่อเนื่องให้เป็นช่องกริด ก่อนจะกลับไปทำเวอร์ชันเต็ม ซึ่งเป็นสิ่งที่หัวข้อ "อีกมุมหนึ่ง" ข้างบนทำ กริดตอบผิดในรายละเอียด แต่มันถูกในเรื่องโครงสร้าง จึงใช้เป็นตัวตรวจคำตอบของเวอร์ชันเต็มได้
ส่วนการไล่ลึกกับการไล่กว้าง มีบทปูพื้นเรื่อง BFS บนกราฟสถานะ และคำอธิบายเรื่องการไล่ลึกอยู่ในคลังนี้แล้ว
| ทาง | ขอบเขตที่มันไหว | งานคร่าว ๆ |
|---|---|---|
| มองเป็นตารางช่องละหนึ่งหน่วย | พิกัดไม่เกิน 500 | 250,000 ช่อง |
| เดินบนพื้นที่จริง | พิกัดถึงหนึ่งล้าน | 100,000 เส้น |
เวลาที่บอกว่าอะไรพังทีละชั้น มักแปลเป็นระยะทางบนกราฟได้ ที่นี่พื้นที่คือปม กำแพงคือเส้นราคาหนึ่ง และคำถามว่ากำแพงไหนรอด ก็เหลือแค่การเทียบเลขสองตัวที่ BFS คำนวณให้แล้ว
ในหน้านี้