programming.in.th · ข้อ 2013
ข้อที่สอนการกลับด้านคำถามไปถามที่มุมของสิ่งที่เราวาง แล้วสิ่งกีดขวางกลายเป็นสี่เหลี่ยมบวกค่าที่กวาดเส้นจัดการได้ พร้อมบันทึกการรีดเวลาจาก 8.1 วินาทีให้เหลือ 3.8 วินาที ว่าตัวการอยู่ตรงไหน
พื้นที่สำรวจเป็นตารางขนาด M × N ช่อง ฐานปิรามิดต้องเป็นจัตุรัสที่ด้านขนานกับเส้นตาราง
บนพื้นที่มีสิ่งกีดขวาง P ก้อน แต่ละก้อนเป็นสี่เหลี่ยมในตาราง (ทับซ้อนกันได้)
การทำลายก้อนที่ i เสียค่าใช้จ่าย C และต้องทำลายทั้งก้อน
จะทุบแค่ส่วนที่ขวางไม่ได้
เรามีงบ B งานคือหาด้านที่ยาวที่สุดของฐานปิรามิดที่วางได้
โดยค่าทำลายสิ่งกีดขวางที่ทับพื้นที่ฐานรวมแล้วไม่เกินงบ ถ้าวางไม่ได้เลยให้ตอบ 0
อินพุต / ขอบเขต / เอาต์พุต
M และ N บรรทัดที่ 2 คือ B
บรรทัดที่ 3 คือ P จากนั้น P บรรทัด แต่ละบรรทัดคือพิกัดมุมล่างซ้าย มุมบนขวา
และค่าทำลายของก้อนนั้น
M, N ≤ 1,000,000, C ≤ 7,000
เวลา 5 วินาที หน่วยความจำ 256 เมกะไบต์
B = 0 และ P ≤ 1,000
ชุดที่สอง 35 คะแนน B ≤ 2,000,000,000 และ P ≤ 30,000
ชุดที่สาม 30 คะแนน B = 0 และ P ≤ 400,000 | Input | Output |
|---|---|
| 6 9 42 5 4 1 6 3 12 3 6 5 6 9 1 3 3 8 24 3 8 6 9 21 5 1 6 2 20 | 4 |
| 13 5 0 8 8 4 10 4 1 4 3 4 4 1 10 2 12 2 2 8 2 8 4 3 2 4 6 4 5 10 3 10 4 8 12 3 12 4 13 2 2 4 2 21 | 3 |
ใบ้
ตารางใหญ่ได้ถึงล้านคูณล้าน แปลว่าเราจะไม่มีวันแตะทุกช่องได้ แต่สิ่งกีดขวางมีแค่ไม่กี่แสนก้อน ข้อมูลจริงที่มีอยู่จึงน้อยกว่าตารางมหาศาล
ลองถามว่า ถ้ารู้ว่าจัตุรัสด้าน s วางได้ แล้วจัตุรัสด้าน s − 1 วางได้ไหม
และถ้าเราคิดกลับด้าน มองว่ามุมล่างซ้ายของจัตุรัสวางได้ที่ไหนบ้าง
สิ่งกีดขวางหนึ่งก้อนจะห้ามมุมล่างซ้ายไว้ที่บริเวณรูปอะไร
ตารางเดียวกับตัวอย่างแรกของโจทย์ งบ 42
คลิกช่องเพื่อวางมุมล่างซ้ายของฐาน แล้วปรับด้านยาว ตัวเลขในช่องคือค่ารื้อของก้อนนั้น
ต้องจ่าย 0 จากงบ 0 · ใหญ่ที่สุดที่วางได้แล้ว 0
ก้อนหนึ่งก้อนคิดเงินครั้งเดียว ไม่ว่าฐานจะทับมันกี่ช่องก็ตาม
ลองเลื่อนฐานไปเรื่อย ๆ แล้วสังเกตว่าค่าใช้จ่ายเปลี่ยนตอนไหน มันไม่ได้เปลี่ยนทุกช่อง แต่เปลี่ยนตอนขอบของฐานข้ามขอบของก้อน
ที่มา และสามรอบที่ยังผิด
จุดตั้งต้นคือความไม่สมดุลของขนาด ตารางกว้างยาวได้ถึงล้านคูณล้าน ซึ่งแตะทุกช่องไม่ได้แน่นอน แต่สิ่งกีดขวางมีแค่หลักแสนก้อน แปลว่าข้อมูลจริงน้อยกว่าตารางมหาศาล อัลกอริทึมจึงต้องเดินตามก้อน ไม่ใช่เดินตามช่อง คำถามที่ปลดล็อกคือ กลับด้านไปถามที่มุมล่างซ้ายของจัตุรัสที่เราจะวาง แทนที่จะถามที่ตัวก้อน พอกลับด้านแล้ว ก้อนหนึ่งก้อนก็กลายเป็นสี่เหลี่ยมผืนหนึ่งที่บวกค่าเพิ่มลงไป ซึ่งกวาดเส้นได้
บักจริงที่เกิดขึ้นอยู่ในเซกเมนต์ทรี ผมขยายขนาดมันให้เป็นกำลังสองตามปกติ แล้วปล่อยใบส่วนเกินไว้ที่ศูนย์
ค่าที่ mn[1] จึงเป็นศูนย์ตลอดกาล โปรแกรมเลยตอบว่าวางได้ทุกขนาดที่ถาม
และมันตอบตัวอย่างในโจทย์ถูกทั้งสองชุด ตัวสุ่มเทียบจับได้ เคสแรกที่ปริ๊นต์ออกมาคือ
M = 9, N = 6, B = 10 ของจริงตอบ 3 แต่โค้ดตอบ 4 ผมสุ่มตารางไม่เกินเก้าคูณเก้า ก้อนไม่เกินหกก้อน รวม 1,500 ชุด
พอถูกแล้วยังเหลือเรื่องเวลา รอบแรกจับเวลาได้ 8.1 วินาที ขณะที่ลิมิตคือห้าวินาที พอไล่ดูว่าเวลาไปอยู่ตรงไหน ตัวการคือการเรียงข้อมูลใหม่ทุกรอบของการค้นหาแบบไบนารี ทั้งเรียงพิกัดที่บีบแล้วและเรียงเหตุการณ์ ยี่สิบรอบก็คือเรียงยี่สิบครั้ง แก้โดยเรียงดัชนีล่วงหน้าครั้งเดียวสี่ชุด (ตาม X1, X2, Y1, Y2) แล้วผสานสองสายที่เรียงมาแล้ว เหลือ 6.6 วินาที ยังไม่พอ จึงเปลี่ยนเซกเมนต์ทรีจากแบบเรียกซ้ำเป็นแบบไล่จากใบขึ้นราก จบที่ 3.8 วินาที
บทเรียนที่ยกไปข้ออื่นได้มีสองข้อ หนึ่งคือใบส่วนเกินของเซกเมนต์ทรีที่หาค่าน้อยสุด ต้องตั้งเป็นค่าอนันต์ ไม่ใช่ศูนย์ สองคือเวลาช้า ตัวการมักไม่ใช่โครงสร้างข้อมูลที่ดูหนักที่สุด แต่เป็นงานเตรียมข้อมูลที่ถูกทำซ้ำอยู่ในลูปนอก ให้ไล่ดูว่าเวลาไปอยู่ตรงไหนก่อนรื้อ
ถ้าจัตุรัสด้าน s วางได้ในงบ ให้มองจัตุรัสด้าน s − 1 ที่ซ้อนอยู่ข้างในมัน
สิ่งกีดขวางที่ชนตัวเล็กย่อมชนตัวใหญ่ด้วย ค่าใช้จ่ายจึงไม่มีทางมากกว่า แปลว่า
เงื่อนไข "วางได้" เป็นจริงเรียงติดกันแล้วเท็จตลอด ซึ่งพอดีกับการค้นหาแบบไบนารี
// s ใหญ่ขึ้น ค่าใช้จ่ายก็ไม่มีทางถูกลง เพราะจัตุรัสเล็กที่ซ้อนอยู่ข้างในย่อมชนก้อนไม่มากกว่า
// เงื่อนไข "มีที่วางได้" จึงเป็นจริงเรียงติดกันแล้วเท็จตลอด ค้นหาแบบไบนารีได้
ll lo = 0, hi = min(M, N);
while (lo < hi) {
ll md = (lo + hi + 1) / 2;
if (ok(md)) lo = md; else hi = md - 1;
} | ด้านยาว | ค่าใช้จ่ายต่ำสุด | มุมล่างซ้ายที่ถูกที่สุด | อยู่ในงบไหม |
|---|---|---|---|
| 1 | 0 | (1, 1) | ได้ |
| 2 | 0 | (1, 1) | ได้ |
| 3 | 9 | (4, 4) | ได้ |
| 4 | 33 | (1, 4) | ได้ |
| 5 | 45 | (1, 3) | ไม่ได้ |
| 6 | 54 | (1, 4) | ไม่ได้ |
ตรึง s ไว้แล้วถามว่ามุมล่างซ้ายวางตรงไหนได้บ้าง จัตุรัสที่มุมอยู่ที่ (x, y)
ชนก้อนที่ i ก็ต่อเมื่อช่วงของมันเหลื่อมกับช่วงของก้อนทั้งสองแกน ซึ่งเขียนออกมาได้ว่า
x ต้องอยู่ระหว่าง X1 − s + 1 ถึง X2 และ y ต้องอยู่ระหว่าง Y1 − s + 1 ถึง Y2
นั่นคือก้อนหนึ่งก้อนกลายเป็นสี่เหลี่ยมที่บวกค่าใช้จ่ายลงบนระนาบของมุมล่างซ้าย คำถามเดิมที่ว่า "มีที่วางถูก ๆ ไหม" จึงกลายเป็น ผลรวมของสี่เหลี่ยมที่ทับกันอยู่ มีจุดไหนที่ต่ำสุดไม่เกินงบ
ปัญหาแบบนี้คือปัญหากวาดเส้นมาตรฐาน กวาด x จากซ้ายไปขวา แต่ละก้อนมีเหตุการณ์สองอัน
คือบวกตอนเข้าและลบตอนออก ส่วนแกน y ใช้เซกเมนต์ทรีที่
บวกทั้งช่วง และถามค่าน้อยสุดของทั้งอาเรย์ พอถึงตำแหน่ง x ไหนก็ถามค่าน้อยสุดหนึ่งครั้ง
ถ้าไม่เกินงบก็แปลว่าขนาดนี้วางได้
พิกัดใหญ่ถึงล้าน จึงต้องบีบพิกัดแกน y ให้เหลือเฉพาะจุดที่ค่าเปลี่ยนจริง
ซึ่งมีไม่เกิน 2P จุด และเช่นเดียวกัน ตำแหน่ง x ที่ต้องถามก็มีแค่จุดที่มีเหตุการณ์เกิด
ระวัง
เซกเมนต์ทรีที่ถามค่าน้อยสุดของทั้งอาเรย์ มีกับดักที่ผมตกไปแล้วรอบหนึ่ง
ใบส่วนเกินที่ถูกเติมมาเพื่อให้ขนาดเป็นกำลังสอง ถ้าปล่อยค่าไว้ที่ศูนย์ คำตอบของ
mn[1] จะเป็นศูนย์ตลอดกาล และโปรแกรมจะตอบว่าวางได้ทุกขนาด
ต้องตั้งใบส่วนเกินเป็นอนันต์ตั้งแต่ตอนสร้าง
อาการของบักนี้คือ ตอบตัวอย่างในโจทย์ถูกทั้งสองชุด แต่พอสุ่มเทียบกับตัวไล่ทุกตำแหน่ง มันตอบเกินจริงทันที
ชุดทดสอบชุดแรกให้ B = 0 ซึ่งแปลว่าห้ามชนก้อนไหนเลย
คำถามเลยง่ายลงมาก จาก "หาจุดที่ผลรวมต่ำสุด" เหลือแค่ "มีจุดไหนที่ไม่มีใครทับ"
พอเป็นแบบนี้ ก็ไม่ต้องใช้โครงสร้างข้อมูลอะไรเลย สำหรับแต่ละตำแหน่ง x ที่น่าลอง
ก็เก็บช่วงของ y ที่ถูกห้ามของก้อนที่ยังคาบเกี่ยวอยู่ เรียงตาม y
แล้วไล่หาช่องว่างแรกที่ไม่มีใครทับ ถ้าเจอก็แปลว่าขนาดนี้วางได้
ต้นทุนคือ P² ต่อหนึ่งขนาด คูณจำนวนรอบของการค้นหาแบบไบนารี ที่ P พันก้อนก็ไหวสบาย
แต่พอชุดที่สามให้ P ถึงสี่แสน มันตายทันที และชุดที่สองที่มีงบจริงก็ใช้ท่านี้ไม่ได้ตั้งแต่แรก
เพราะการหา "จุดที่ผลรวมต่ำสุด" ไม่ใช่การหาช่องว่าง
บทเรียนที่ผมชอบจากข้อนี้คือ ชุดทดสอบย่อยสามชุดของมันไม่ได้ต่างกันแค่ขนาด แต่ต่างกันที่
รูปของคำถาม ชุด B = 0 ถามหาช่องว่าง ส่วนชุดที่มีงบถามหาค่าต่ำสุด
ซึ่งเป็นคำถามที่แพงกว่า และโค้ดที่ตอบคำถามแพงกว่าได้ ก็ตอบคำถามถูกกว่าได้ฟรีในตัว
| ทาง | ขอบเขตที่มันไหว | งานคร่าว ๆ |
|---|---|---|
| ไล่ช่วง y ที่ถูกห้าม ต่อหนึ่งขนาด | P หลักพัน และ B = 0 | 20,000,000 ครั้ง |
| ค้นหาไบนารีคู่กับเซกเมนต์ทรี | P ถึง 400,000 | 152,000,000 ครั้ง |
เวอร์ชันแรกที่ผมเขียนใช้เวลา 8.1 วินาที ซึ่งเกินลิมิต 5 วินาที ตัวการคือการเรียงข้อมูลใหม่ทุกรอบ ของการค้นหาแบบไบนารี ทั้งเรียงพิกัดที่บีบแล้วและเรียงเหตุการณ์ รวมยี่สิบรอบก็เป็นการเรียงยี่สิบครั้ง
ทางแก้คือเรียงดัชนีไว้ล่วงหน้าครั้งเดียวสี่ชุด (ตาม X1, X2, Y1,
Y2) เพราะค่าที่ต้องใช้ในแต่ละรอบเป็นฟังก์ชันไม่ลดของค่าเหล่านั้น
ลำดับจึงไม่เปลี่ยน แค่ผสานสองสายที่เรียงมาแล้วก็พอ เหลือ 6.6 วินาที
ที่เหลืออีกก้อนคือเซกเมนต์ทรีแบบเรียกซ้ำ ซึ่งถูกเรียกหลายสิบล้านครั้ง เปลี่ยนเป็นแบบไล่จากใบขึ้นราก แล้วจบที่ 3.8 วินาที ผ่านลิมิต
ผมสุ่มตารางเล็ก ๆ ไม่เกินเก้าคูณเก้า พร้อมก้อนไม่เกินหกก้อนและงบสุ่ม จำนวน 1,500 ชุด แล้วเทียบกับตัวไล่ทุกขนาดทุกตำแหน่ง ตรงกันหมด เวอร์ชันที่ลืมตั้งใบส่วนเกินเป็นอนันต์ตกตั้งแต่ชุดต้น ๆ
เครื่องมือของข้อนี้อยู่ในบทปูพื้นฐาน เซกเมนต์ทรีกับการค้นหาคำตอบแบบไบนารี
ท่าที่ติดมือกลับไปมีสองชั้น ชั้นแรกคือเกณฑ์ว่าจะหยิบการค้นหาคำตอบแบบไบนารีมาใช้ได้เมื่อไร ต้องมีคุณสมบัติว่าถ้าคำตอบขนาดหนึ่งทำได้ ขนาดที่เล็กกว่าต้องทำได้ด้วยเสมอ ข้อนี้มีให้ฟรีเพราะจัตุรัสเล็กกว่า ซุกอยู่ในจัตุรัสใหญ่ได้ พอมีข้อนั้นแล้วคำถามว่าใหญ่สุดเท่าไรก็แปลงเป็นคำถามว่า ขนาดนี้ทำได้ไหม ซึ่งตอบง่ายกว่ามาก ชั้นที่สองคือบทเรียนเรื่องเวลาที่ผมเสียไปข้างบน งานที่อยู่ในลูปของการค้นหาแบบไบนารีถูกทำซ้ำยี่สิบรอบเสมอ ก่อนจะไปรื้อโครงสร้างข้อมูลที่ดูหนักที่สุด ให้ไล่ดูก่อนว่ามีงานเตรียมข้อมูลชิ้นไหนที่ผลลัพธ์เหมือนกันทุกรอบ แล้วยกมันออกไปทำครั้งเดียวนอกลูป
เมื่อตารางใหญ่เกินจะแตะทุกช่อง ให้กลับด้านคำถามไปถามที่มุมของสิ่งที่เราวาง แล้วสิ่งกีดขวางแต่ละก้อน จะกลายเป็นสี่เหลี่ยมบวกค่าหนึ่งผืน ซึ่งกวาดเส้นกับเซกเมนต์ทรีจัดการได้ทั้งหมด
ในหน้านี้