programming.in.th · ข้อ 2014
กำแพงรอดเมื่อน้ำถึงสองฝั่งในชั่วโมงเดียวกัน บทนี้ไล่จากการปล่อยน้ำทีละชั่วโมง ไปตารางช่องละหนึ่งหน่วยที่ได้ 40 คะแนน บีบพิกัดให้ได้อีก 15 แล้วจบที่การเดินรอบผืนของผังจริงกับ BFS บนกราฟคู่
ปี 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 เส้น
น้ำเริ่มจากทะเลรอบนอก และทะลุกำแพงได้ชั่วโมงละหนึ่งชั้น
เริ่มจากวิธีที่ตรงที่สุด คือทำตามโจทย์ทุกตัวอักษรบนผังตัวอย่าง (ชุดที่ 2 ของเกม) ก่อนอื่นมาดูหน้าตาของผังนี้ให้ชัด
ให้ชั่วโมงของผืนคือจำนวนกำแพงที่น้อยที่สุดที่ต้องข้าม ถ้าเดินจากทะเลมาถึงผืนนั้น ทะเลเองได้ 0 เครื่องเดินข้างล่างปล่อยน้ำตามโจทย์ทีละชั่วโมง ลองดูว่าผืนไหนเปียกเมื่อไร
กดถัดไปเพื่อปล่อยน้ำทีละชั่วโมง
สีฟ้าคือผืนที่เปียกแล้ว เส้นแดงคือกำแพงที่จะพังในชั่วโมงถัดไป เส้นประคือกำแพงที่พังไปแล้ว เส้นเขียวคือกำแพงที่ยืนจนจบ
| ผืนสองฝั่ง | น้ำถึง (ชั่วโมง) | กำแพง | ผล |
|---|---|---|---|
| A กับ B | 0 กับ 1 | 1, 2, 3, 4, 5 | พัง |
| B ทั้งสองฝั่ง | 1 กับ 1 | 6 | รอด |
| B กับ C | 1 กับ 2 | 11, 12, 13 | พัง |
| B กับ D | 1 กับ 2 | 7, 8, 9, 10, 14 | พัง |
| C กับ D | 2 กับ 2 | 15, 16, 17 | รอด |
แถวที่รอดมีสองแบบ กำแพง 6 มีผืน B อยู่ทั้งสองฝั่ง ส่วนกำแพง 15, 16, 17 คั่นผืน C กับ D ซึ่งน้ำถึงในชั่วโมง 2 เหมือนกัน
กฎที่ได้
กำแพงยังยืนอยู่ ก็ต่อเมื่อน้ำถึงผืนสองฝั่งของมันในชั่วโมงเดียวกัน (กำแพงที่มีผืนเดียวกันทั้งสองฝั่งก็เข้ากฎนี้ เพราะผืนเดียวกันย่อมเปียกพร้อมกัน)
เหตุผลที่กฎนี้ใช้ได้กับทุกผัง ไล่ทีละข้อ
ชั่วโมงของผืนคือระยะทางสั้นที่สุด เมื่อการข้ามกำแพงหนึ่งเส้นนับเป็นหนึ่งก้าว เครื่องมือมาตรฐานสำหรับระยะแบบนี้คือ BFS (breadth-first search แปลว่าการค้นตามแนวกว้าง)
BFS ทำงานยังไง
อธิบายเต็มพร้อมตัวอย่างอยู่ในบทกราฟที่โจทย์ไม่ได้ให้มา
ส่วนที่เหลือของบทคือการหาชั่วโมงของทุกผืนให้เร็วพอ
ไล่จากช้าไปเร็วมีสี่ขั้น ตอนนี้กับตอนถัดไปคือสองขั้นกลาง
วิธีที่ 1
ราวจำนวนช่อง × จำนวนชั่วโมง
ทำตามโจทย์ตรง ๆ แบบตอนที่ 1 น้ำไหล หากำแพงที่มีน้ำข้างเดียว ทำซ้ำ
ไม่ได้ส่ง ใช้เป็นตัวตรวจของหน้านี้
วิธีที่ 2
ราวจำนวนช่อง
ได้ชั่วโมงของทุกช่องในรอบเดียว แล้วใช้กฎของตอนที่ 1
40 คะแนน (พิกัดไม่เกิน 500)
วิธีที่ 3
ไม่เกิน (N + 2)² ช่อง
แทนพิกัดด้วยลำดับที่ของมัน ตารางเล็กลงตามจำนวนจุด (ตอนที่ 3)
อีก 15 คะแนน (N ไม่เกิน 500) รวม 55
วิธีเต็ม
ราว 12 ก้าวต่อกำแพง
ไม่ใช้ช่องเลย หาผืนจากกำแพงตรง ๆ (ตอนที่ 4 และ 5)
100 คะแนน
ที่มาของแนวคิดนี้
ผมเริ่มจากวิธีที่ 2 ก่อนจะคิดถึงอย่างอื่น มันเขียนง่าย ตอบตัวอย่างถูก และกลายเป็นเฉลย 40 คะแนนของหน้านี้ พอมันเดินได้ ผมถึงเห็นว่าประโยคที่มันใช้ตัดสินใจคือ "ชั่วโมงสองฝั่งเท่ากัน" ซึ่งต้องการแค่ผืน ช่องไม่จำเป็นเลย
วิธีที่ 2 แบ่งระนาบเป็นช่องละหนึ่งหน่วย กำแพงกลายเป็นขอบระหว่างช่องสองช่องที่ติดกัน เดินข้ามขอบที่ไม่มีกำแพงมีค่าข้าม 0 ข้ามขอบที่มีกำแพงมีค่าข้าม 1
ตารางกว้างเท่ากับพิกัดสูงสุดบวก 3 ช่อง คือเลื่อนทุกจุดไปหนึ่งหน่วยเพื่อให้แถวและคอลัมน์ที่ 0 เป็นทะเล แล้วเผื่อด้านขวาบนอีกสองแถว ตัวอย่างมีพิกัดสูงสุด 8 เลยได้ตาราง 11 × 11 ช่อง
BFS แบบ 0-1 (0-1 BFS)
มีอธิบายพร้อมตัวอย่างในบทกราฟที่โจทย์ไม่ได้ให้มาเช่นกัน
ถ้าค่าข้ามเป็นเลขอะไรก็ได้ ต้องใช้ Dijkstra (ไดค์สตรา) ซึ่งเก็บคิวที่คอยจัดลำดับตามระยะ ค่าข้ามที่มีแค่ 0 กับ 1 ทำให้เราใช้ deque ที่ง่ายและเร็วกว่าได้
ทำไมคิดทีละช่องแล้วยังถูก
ที่พิกัดไม่เกิน 500 ตารางมีไม่เกิน 503 × 503 = 253,009 ช่อง เร็วและเล็กพอ โค้ดอยู่ท้ายตอนที่ 3 เพราะเติมขั้นบีบพิกัดไว้ในตัวเดียวกันแล้ว
ชุดย่อยที่สองให้จุดไม่เกิน 500 จุด แต่พิกัดยังไปถึง 1,000,000 ตารางตรง ๆ มีราว 1.0 × 10¹² ช่อง ใส่หน่วยความจำไม่ได้
ทางออกมาจากการสังเกตว่าผืนกับชั่วโมงขึ้นกับแค่ลำดับ คือจุดไหนอยู่ซ้าย ขวา บน หรือล่างของจุดไหน ระยะห่างจริงไม่มีผลเลย
บีบพิกัด (coordinate compression) คือแทนพิกัด x แต่ละค่าด้วยลำดับที่ของมันในบรรดาพิกัด x ที่ไม่ซ้ำกัน แล้วทำแบบเดียวกันกับ y ตัวอย่างในโจทย์ได้แบบนี้
| แกน | พิกัดเดิมที่ไม่ซ้ำ | ลำดับที่ |
|---|---|---|
| x | 1 2 4 6 7 8 | 1 2 3 4 5 6 |
| y | 1 2 3 5 6 8 | 1 2 3 4 5 6 |
ทำไมรูปผังไม่เปลี่ยน
การบีบรักษาลำดับของพิกัดทุกค่า กำแพงที่แตะกันยังแตะกัน กำแพงที่แยกกันก็ยังแยกกัน ระหว่างเส้นสองเส้นที่ติดกันยังเหลือช่องอย่างน้อยหนึ่งแถว ผืนทุกผืนอยู่ครบ แค่แคบลง
เมื่อจุดไม่เกิน 500 จุด แต่ละแกนมีพิกัดต่างกันไม่เกิน 500 ค่า ตารางไม่เกิน 502 × 502 ช่อง ช่องละ 6 ไบต์ รวมราว 1.5 เมกะไบต์
ที่ N = 100,000 การบีบพิกัดไปต่อไม่ไหว เพราะแต่ละแกนอาจมีพิกัดต่างกันถึง N ค่า ลองเทียบจำนวนงานของสามทางกับงบเวลา
หน่วยความจำแย่กว่าเวลาอีก ตารางบีบพิกัดที่ N = 100,000 มีราว 10¹⁰ ช่อง ช่องละ 6 ไบต์ก็ราว 60 กิกะไบต์ ขณะที่ลิมิตคือ 32 เมกะไบต์
งานต้องโตตามจำนวนกำแพงแทนจำนวนช่อง ตอนถัดไปจึงทิ้งช่องไปเลย แล้วหาผืนจากกำแพงตรง ๆ
ตอนที่ 1 ใช้ช่องแค่เพื่อรู้ว่ากำแพงแต่ละเส้นคั่นผืนไหนกับผืนไหน และผืนไหนติดทะเล ข้อมูลชุดนี้หาจากกำแพงตรง ๆ ได้
ผังที่เส้นไม่ตัดกันแบบนี้คือกราฟระนาบ (planar graph อ่านว่า แพลนนาร์ กราฟ คือกราฟที่วาดบนกระดาษได้โดยไม่มีเส้นไหนตัดกัน) ผืนที่เส้นล้อมไว้เรียกว่าหน้า (face)
ผังที่กำแพงต่อกันเป็นก้อนเดียวอย่างตัวอย่างนี้ หน้าตรงกับผืน A, B, C, D พอดี ผังที่มีหลายก้อนต่างออกไปนิดหนึ่ง ซึ่งเป็นเรื่องของตอนที่ 5
เครื่องมือที่ใช้หาหน้าคือครึ่งเส้น (half-edge) มองกำแพงหนึ่งเส้นเป็นลูกศรสองอันวิ่งสวนกัน อันละฝั่ง ลูกศรแต่ละอันเป็นของหน้าที่อยู่ทางซ้ายมือของมัน
ทิศในบทนี้ใช้แบบเดียวกับโจทย์ คือ x เพิ่มไปทางขวา (ตะวันออก) และ y เพิ่มขึ้นข้างบน (เหนือ) การเดินรอบหน้าหนึ่งหน้าทำแบบนี้
ทำซ้ำจนวนกลับถึงครึ่งเส้นแรก ทุกอันที่ผ่านเป็นของหน้าเดียวกัน แต่ละจุดมีทางออกได้ไม่เกินสี่ทิศ และครึ่งเส้นทั้ง 2W อันถูกเดินผ่านอันละครั้ง
| ก้าว | จุด | หันไปทาง | ที่จุดปลาย | xu·yv − xv·yu |
|---|---|---|---|---|
| 1 | 5 ไป 6 | ตะวันออก | ไปตรง | 6 − 12 = −6 |
| 2 | 6 ไป 7 | ตะวันออก | เลี้ยวซ้าย | 12 − 18 = −6 |
| 3 | 7 ไป 10 | เหนือ | เลี้ยวซ้าย | 30 − 18 = 12 |
| 4 | 10 ไป 9 | ตะวันตก | ไปตรง | 30 − 20 = 10 |
| 5 | 9 ไป 8 | ตะวันตก | เลี้ยวซ้าย | 20 − 10 = 10 |
| 6 | 8 ไป 5 | ใต้ | เลี้ยวซ้าย | 6 − 10 = −4 |
ห้องกับขอบนอก ต่างกันที่ทิศของการเดิน
คอลัมน์ขวาของตารางคือสูตรเชือกผูกรองเท้า (shoelace formula สูตรหาพื้นที่จากพิกัดจุดยอดที่ไล่ตามรอบ) ผลรวมของมันคือสองเท่าของพื้นที่ที่วงล้อม เป็นบวกเมื่อเดินทวนเข็ม และติดลบเมื่อเดินตามเข็ม เพราะห้องอยู่ซ้ายมือเสมอ ด้านในห้องเลยถูกเดินทวนเข็ม ส่วนขอบนอกมีตัวก้อนอยู่ขวามือ ถูกเดินตามเข็ม
ห้องสี่เหลี่ยม 3 × 3 ของชุดที่ 1 เดินด้านในได้ 18 = 2 × 9 ขอบนอกของห้องเดียวกันคือวงเดิมที่เดินกลับทิศ ได้ −18 กำแพงเส้นเดียวที่ไม่ล้อมอะไรถูกเดินไปแล้วกลับ สองพจน์หักล้างกันเหลือ 0
ผลรวมเป็นบวกแปลว่าห้อง ศูนย์หรือติดลบแปลว่าขอบนอกของก้อน (ขอบนอกของตัวอย่างได้ −98) พิกัดถึงล้าน แต่ละพจน์ถึงราว 10¹² เกินขอบของ int ซึ่งอยู่ราว 2.1 × 10⁹ โค้ดเลยเก็บผลรวมเป็น long long (จำนวนเต็ม 64 บิต)
พอรู้หน้าของครึ่งเส้นทุกอัน ก็สร้างกราฟคู่ (dual graph) ได้ คือให้หน้าเป็นปม และกำแพงแต่ละเส้นเป็นเส้นเชื่อมสองหน้าที่มันคั่น
BFS จากทะเลบนกราฟนี้ให้ชั่วโมงของทุกหน้า เส้นเชื่อมทุกเส้นมีค่าข้าม 1 คิวธรรมดาก็พอ ผังก้อนเดียวอย่างตัวอย่างได้ชั่วโมงตรงกับตอนที่ 1 ทุกผืน
ผังที่มีหลายก้อนยังเหลือเรื่องเดียวต้องเคลียร์ คือก้อนที่ลอยอยู่ในห้องของอีกก้อน
เดินรอบขอบนอกของก้อนใน ได้ผลรวมติดลบ โค้ดเลยมองว่ามันเป็นขอบนอก แต่พื้นที่ฝั่งนอกของรอบนั้นจริง ๆ คือห้อง B คำถามคือ ถ้ายุบมันเข้ากับทะเลไปเลย คำตอบจะเพี้ยนไหม
เรื่องจริงที่เกิดขึ้นตามลำดับ
หน้านี้รุ่นแรกหาห้องที่ห่อด้วยการยิงรังสีลงล่างจากขอบนอกของแต่ละก้อน และใช้เซกเมนต์ทรีหากำแพงแนวนอนเส้นแรกที่ชน รุ่นนั้นเขียนไว้ว่าถ้าข้ามขั้นนี้ คำตอบจะผิด
ตอนเขียนบทนี้ใหม่ ผมวัดหน่วยความจำของรุ่นนั้นได้ 39.4 เมกะไบต์ เกินลิมิต 32 เมกะไบต์ เลยลองตัดขั้นยิงรังสีทิ้งแล้วเทียบกับตัวปล่อยน้ำจริง ผลตรงกัน เหตุผลอยู่ในรายการข้างล่าง
การยุบขอบนอกทุกก้อนเข้ากับทะเลแค่เลื่อนชั่วโมงในแต่ละก้อนลงเท่ากันทั้งก้อน ผลรอดหรือพังของทุกเส้นเท่าเดิม ตารางข้างล่างคือผังในรูป
| กำแพง | ผังจริง | คิดก้อนในแยก | ผล |
|---|---|---|---|
| 5 | 1 กับ 2 | 0 กับ 1 | พัง ทั้งสองแบบ |
| 6 | 1 กับ 2 | 0 กับ 1 | พัง ทั้งสองแบบ |
| 7 | 2 กับ 1 | 1 กับ 0 | พัง ทั้งสองแบบ |
| 8 | 2 กับ 1 | 1 กับ 0 | พัง ทั้งสองแบบ |
| 9 | 2 กับ 1 | 1 กับ 0 | พัง ทั้งสองแบบ |
| 10 | 1 กับ 2 | 0 กับ 1 | พัง ทั้งสองแบบ |
| 11 | 2 กับ 2 | 1 กับ 1 | รอด ทั้งสองแบบ |
เหตุผลข้างบนคือสิ่งที่หักล้างคำเตือนของรุ่นแรก ส่วนการเทียบ 2,000 ผังในตอนที่ 6 เป็นหลักฐานเสริม โค้ดเต็มเลยเหลือแค่นี้ หน้าไหนผลรวมไม่เกินศูนย์ ยุบรวมเป็นปมเดียวคือทะเล
| โค้ด | เมกะไบต์ |
|---|---|
| รุ่นเดิม (ยิงรังสี เซกเมนต์ทรีตามพิกัดดิบ) | 39.4 |
| รุ่นเดิม แต่เซกเมนต์ทรีตามลำดับที่ของ x | 25.0 |
| โค้ดเต็มของหน้านี้ | 14.6 |
รวมตอนที่ 4 กับ 5 เข้าด้วยกัน ได้โค้ดข้างล่าง
| ส่วนในโค้ด | อธิบายอยู่ที่ |
|---|---|
| แตกกำแพง e เป็นครึ่งเส้น 2e กับ 2e+1 | ตอนที่ 4 (ครึ่งเส้น) |
| เดินรอบแต่ละหน้า ลองเลี้ยวซ้ายก่อน | ตอนที่ 4 (ลำดับการเลี้ยว) |
| area2 สูตรเชือกผูกรองเท้า แยกห้องกับขอบนอก | ตอนที่ 4 (ห้องกับขอบนอก) |
| ยุบขอบนอกทุกก้อนเป็นทะเล ปม 0 | ตอนที่ 5 |
| กราฟคู่ แล้ว BFS จากทะเล | ตอนที่ 4 กับ 5 (ชั่วโมงอาจเลื่อนทั้งก้อน แต่ผลรอดหรือพังเท่าเดิม) |
| เทียบ dist สองฝั่งของกำแพง | ตอนที่ 1 (กฎที่ได้) |
ตรวจอะไรไปแล้วบ้าง
ทั้งหมดนี้คือการตรวจ ไม่ใช่การพิสูจน์ การสุ่มครอบคลุมแค่ผังเล็ก เหตุผลในตอนที่ 1 และ 5 คือส่วนที่ครอบคลุมผังทุกขนาด
| ผัง | จุด | กำแพง | วินาที | เมกะไบต์ |
|---|---|---|---|---|
| ตาราง 316 × 316 จุด | 99,856 | 199,080 | 0.04 ถึง 0.05 | 14.6 |
| ห้องซ้อนกัน 25,000 ชั้น | 100,000 | 100,000 | 0.03 ถึง 0.04 | 9.6 |
บทนี้ไล่จากปล่อยน้ำจริง ไปตาราง บีบพิกัด แล้วเดินรอบผืน ทุกขั้นตอบคำถามเดียวกันว่าผืนไหนเปียกชั่วโมงเท่าไร ต่างกันแค่ว่าแบ่งพื้นที่ละเอียดแค่ไหน
คำถามที่ยกไปใช้ข้ออื่นได้คือ คำตอบขึ้นกับพิกัดจริง หรือขึ้นกับแค่ลำดับและการแตะกัน ถ้าเป็นอย่างหลัง จะบีบพิกัดหรือเปลี่ยนไปคิดบนกราฟก็ได้ โดยไม่ต้องยุ่งกับทศนิยมเลย
ในหน้านี้