programming.in.th · ข้อ 2028
อีกครึ่งคือต้องได้ชุดที่หมายเลขเล็กที่สุดตามพจนานุกรม บทนี้เปลี่ยนคำถามระดับทั้งชุด ให้เป็นคำถามระดับช่องว่างเดียวที่งานนั้นตกอยู่ แล้วตอบมันด้วยตารางกระโดดสองยกกำลัง
ศูนย์ประชุมมีห้องเดียว บริษัท N รายยื่นขอเช่า แต่ละรายบอกวันเริ่มกับวันจบของงานตัวเอง
และจะเช่าก็ต่อเมื่อได้ห้องคนเดียวตลอดช่วงนั้น ฝ่ายการตลาดอยากปล่อยห้องให้ได้จำนวนบริษัทมากที่สุด
ปัญหาคือชุดที่ได้จำนวนมากที่สุดมักมีหลายชุด ฝ่ายการตลาดจึงตั้งกติกาเพิ่มว่า ในบรรดาชุดที่ใหญ่ที่สุดทั้งหมด ให้เลือกชุดที่เขียนหมายเลขบริษัทเรียงจากน้อยไปมากแล้ว เล็กที่สุดตามลำดับพจนานุกรม
อินพุต / ขอบเขต / เอาต์พุต
N จากนั้นอีก N บรรทัด
บรรทัดที่ i คือวันเริ่มกับวันจบของบริษัทที่ i N ≤ 200,000 วันเริ่มไม่น้อยกว่า 1 และวันจบไม่เกิน
1,000,000,000 เวลา 1.5 วินาที หน่วยความจำ 64 เมกะไบต์
N ไม่เกิน 3,000| Input | Output |
|---|---|
| 4 4 9 9 11 13 19 10 17 | 2 1 3 |
ชุดที่ใหญ่ที่สุดของตัวอย่างนี้มีสามชุด คือ (1, 3), (2, 3) และ (1, 4) เรียงตามพจนานุกรมแล้ว (1, 3) มาก่อน (1, 4) และ (1, 4) มาก่อน (2, 3) คำตอบจึงเป็น (1, 3)
ใบ้
ครึ่งแรกของโจทย์คือจัดตารางให้ได้จำนวนมากที่สุด ซึ่งเป็นโจทย์คลาสสิก คำตอบคือเรียงตามวันจบแล้วเลือกตัวที่จบเร็วที่สุดที่ยังไม่ชนกับที่เลือกไว้
ครึ่งหลังหนักกว่ามาก เพราะเราต้องได้ชุดที่หมายเลขเล็กที่สุด ท่าที่อยากทำคือ ไล่บริษัทที่ 1, 2, 3 ไปเรื่อย ๆ แล้วรับถ้ารับได้ คำถามคือรับได้แปลว่าอะไร และตรวจมันด้วยราคาเท่าไร
ชุดนี้วางได้มากที่สุดสองงาน แต่มีหลายวิธี คำถามคือชุดไหนที่หมายเลขเรียงแล้วเล็กที่สุด
กดที่แท่งเพื่อรับบริษัทนั้น แท่งที่ทับกันรับพร้อมกันไม่ได้
รับไว้ 0 บริษัท
เป้าหมายมีสองชั้น ชั้นแรกคือจำนวนต้องมากที่สุด ชั้นที่สองคือถ้าจำนวนเท่ากัน หมายเลขต้องเล็กที่สุด
ถ้าลองชุดที่สองจะเห็นตัวล่อชัด ๆ คือบริษัทที่เริ่มวันแรกสุดแต่ยาวจนกินที่ของอีกสองราย รับมันแล้วจำนวนรวมลดลงทันที ทั้งที่หมายเลขมันเล็กที่สุด
ที่มาของแนวคิดนี้
ผมเริ่มจากท่าที่อยากทำที่สุด คือไล่บริษัทตามหมายเลข แล้วถามทีละตัวว่า "ถ้ารับตัวนี้ จำนวนรวมยังเท่าเดิมไหม" ซึ่งถูกต้องแน่นอน เพราะการรับตัวที่หมายเลขน้อยที่สุด เท่าที่รับได้โดยไม่เสียจำนวนรวม คือนิยามของคำว่าเล็กที่สุดตามพจนานุกรมพอดี
ปัญหาอยู่ที่ราคา ถ้าตอบคำถามนั้นด้วยการคำนวณจำนวนรวมของทั้งชุดใหม่ทุกครั้ง
มันคือ N คูณ N ซึ่งที่สองแสนคือสี่หมื่นล้าน ผมจึงต้องหาวิธี
ตอบคำถามนี้แบบเฉพาะที่
สิ่งที่ปลดล็อกคือสังเกตว่า งานที่รับไปแล้วทำให้ช่วงเวลาทั้งหมดถูกซอยเป็นช่องว่างที่เป็นอิสระต่อกัน งานหนึ่งงานตกอยู่ในช่องว่างเดียวเสมอ และการรับมันมีผลแค่กับช่องว่างนั้น คำถามจึงเล็กลงเหลือ "ช่องว่างนี้เคยวางได้กี่งาน และถ้าตัดตรงนี้จะวางได้กี่งาน"
บทเรียนที่ยกไปข้ออื่นได้คือ เวลาต้องตอบคำถามว่า "รับตัวนี้แล้วคำตอบรวมเปลี่ยนไหม" ซ้ำ ๆ ให้หาก่อนว่าของที่รับไปแล้วแบ่งปัญหาออกเป็นก้อนอิสระได้หรือเปล่า ถ้าได้ คำถามระดับโลกจะกลายเป็นคำถามระดับก้อน ซึ่งมักถูกกว่ามาก
ครึ่งแรกคือโจทย์จัดตารางแบบคลาสสิก เรียงตามวันจบ แล้วไล่เลือกตัวที่จบเร็วที่สุด ที่เริ่มหลังงานที่เลือกไว้ล่าสุด ท่านี้ได้จำนวนมากที่สุดเสมอ เพราะการเลือกตัวที่จบเร็วที่สุด เหลือเวลาข้างหลังให้มากที่สุด จึงไม่มีทางเสียเปรียบทางเลือกอื่น
รับตัวที่หมายเลขเล็กที่สุดเท่าที่รับได้โดยจำนวนรวมไม่ลด ทำแบบนี้ไปเรื่อย ๆ ก็ได้ชุดที่เล็กที่สุดตามพจนานุกรม
ต้องมีฟังก์ชัน cnt(l, r) ที่บอกว่าช่วงวัน [l, r] วางงานที่ไม่ทับกันได้มากที่สุดกี่งาน
โดยนับเฉพาะงานที่อยู่ในช่วงนั้นทั้งก้อน
ท่าโลภบอกไว้แล้วว่าจากตำแหน่ง p เราควรหยิบงานที่เริ่มไม่ก่อน p และจบเร็วที่สุด
เรียกวันจบของมันว่า f(p) แล้วตำแหน่งถัดไปคือ f(p) + 1
การนับจึงเป็นการกระโดดตามฟังก์ชันเดิมซ้ำ ๆ จนกว่าจะเลยขอบขวา
การกระโดดทีละก้าวยังช้า จึงใช้ตารางกระโดดสองยกกำลัง คือเก็บไว้ว่าจากตำแหน่งนี้
กระโดด 1 ครั้งไปไหน 2 ครั้งไปไหน 4 ครั้งไปไหน ไปจนถึง N ครั้ง
แล้วนับจำนวนก้าวด้วยการไล่จากก้าวใหญ่ลงมา เหมือนเขียนเลขเป็นฐานสอง
// รับบริษัทนี้ได้ก็ต่อเมื่อจำนวนรวมของช่องว่างที่มันตกอยู่ ไม่ลดลง
// ซ้ายของมัน บวกตัวมันเอง บวกขวาของมัน ต้องเท่ากับที่ช่องว่างนี้เคยวางได้
if(cnt(gl, s[i] - 1) + 1 + cnt(e[i] + 1, gr) != cnt(gl, gr)) continue; โน้ต · ทำไมต้องบีบพิกัดวัน
วันมีได้ถึงพันล้าน แต่ตำแหน่งที่มีความหมายจริง ๆ มีแค่วันเริ่ม วันจบ และวันจบบวกหนึ่ง
ของทุกบริษัท ซึ่งไม่เกินสามเท่าของ N เราจึงเก็บเฉพาะพิกัดเหล่านั้น
แล้วอ้างถึงมันด้วยดัชนี ตารางกระโดดจึงมีขนาดพอดีกับข้อมูลจริง ไม่ใช่พอดีกับช่วงเวลา
งานที่รับไปแล้วซอยเวลาออกเป็นช่องว่าง ซึ่งเก็บไว้ในเซตที่เรียงตามจุดเริ่ม
พอถึงคิวบริษัทที่ i เราหาว่ามันตกอยู่ในช่องว่างไหน ถ้ามันคร่อมงานที่รับไปแล้ว
ก็ปฏิเสธทันที ถ้าอยู่ในช่องว่างเดียวพอดี ก็เทียบสามตัวเลข
cnt(gl, gr) คือช่องว่างนี้เคยวางได้กี่งานcnt(gl, s - 1) คือถ้ารับตัวนี้ ฝั่งซ้ายของมันยังวางได้กี่งานcnt(e + 1, gr) คือฝั่งขวายังวางได้กี่งานถ้าซ้ายบวกหนึ่งบวกขวาเท่ากับของเดิม แปลว่ารับตัวนี้แล้วไม่เสียอะไรเลย จึงรับ แล้วแทนที่ช่องว่างเดิมด้วยสองช่องว่างใหม่ ถ้าน้อยกว่า แปลว่ารับแล้วจำนวนรวมลด จึงต้องปฏิเสธ
การรับงานหนึ่งงาน มีผลแค่กับช่องว่างเดียวที่มันตกอยู่ คำถามระดับทั้งชุดจึงยุบเหลือคำถามระดับช่องว่าง
ไล่ทีละบริษัทบนชุด 8 ราย ซึ่งวางได้มากที่สุด 4 งาน
| บริษัท | ขอวันที่ | ช่องว่างเดิมวางได้ | ถ้ารับจะได้ | ผล |
|---|---|---|---|---|
| 1 | 2 ถึง 5 | 4 | 3 | ไม่รับ |
| 2 | 1 ถึง 3 | 4 | 4 | รับ |
| 3 | 4 ถึง 6 | 3 | 3 | รับ |
| 4 | 6 ถึง 9 | · | · | ไม่รับ |
| 5 | 7 ถึง 8 | 2 | 2 | รับ |
| 6 | 10 ถึง 12 | 1 | 1 | รับ |
| 7 | 9 ถึง 11 | · | · | ไม่รับ |
| 8 | 3 ถึง 4 | · | · | ไม่รับ |
ชุดนี้วางได้มากที่สุด 4 งาน และชุดที่หมายเลขเล็กที่สุดคือ 2, 3, 5, 6 ตัวเลขทุกช่องคำนวณตอนสร้างหน้า และถูกเทียบกับการลองทุกเซตย่อยแล้วว่าตรงกัน
เวลาที่วัดได้บนเครื่องผม ที่ N เท่ากับ 200,000 และวันกระจายเต็มพันล้าน
ใช้เวลา 0.46 วินาที ส่วนชุดที่จงใจให้ทับกันหนามาก คือทุกงานยาวไม่เกินสี่วันในช่วงพันวัน
ใช้เวลา 0.14 วินาที จากลิมิต 1.5 วินาที
ท่าแรกคือตารางกระโดดสองยกกำลัง (binary lifting) ซึ่งใช้ได้ทุกครั้งที่มีฟังก์ชัน "จากตรงนี้ไปตรงไหนต่อ" แล้วต้องถามว่าเดินซ้ำกี่ครั้งถึงจะพ้นขอบ ท่าที่สองคือ ตรวจว่ารับของชิ้นนี้แล้วเสียของหรือเปล่า ด้วยการเทียบจำนวนก่อนกับหลัง ซึ่งเป็นวิธีมาตรฐานของโจทย์ที่ขอคำตอบเล็กที่สุดตามพจนานุกรม
ทบทวนพื้นฐาน · ทำไมท่าโลภแบบจบเร็วก่อนถึงถูก
สมมติมีคำตอบที่ดีที่สุดชุดหนึ่งที่ไม่ได้เลือกงานที่จบเร็วที่สุด ให้สลับงานแรกของชุดนั้น ด้วยงานที่จบเร็วที่สุด งานที่เหลือทั้งหมดยังวางได้เหมือนเดิม เพราะงานใหม่จบไม่ช้ากว่างานเดิม จำนวนจึงไม่ลด ทำแบบนี้ซ้ำไปเรื่อย ๆ ก็แปลงคำตอบที่ดีที่สุดให้กลายเป็นคำตอบของท่าโลภได้
ข้อควรระวังคือท่านี้ตอบได้แค่จำนวน ไม่ได้ตอบว่าชุดไหน โจทย์ข้อนี้จึงยากขึ้นอีกชั้นตรงที่ถามถึงตัวชุดเอง
| ทาง | ขอบเขตที่มันไหว | งานคร่าว ๆ |
|---|---|---|
| ลองรับทีละตัว แล้วคำนวณจำนวนรวมใหม่ทั้งชุด | N ไม่เกินราวสามพัน | 200,000 คูณ 200,000 คือสี่หมื่นล้าน |
| ตารางกระโดดสองยกกำลัง แล้วถามสี่ครั้งต่อบริษัท | N ถึง 200,000 | ราว 14.4 ล้านครั้ง |
ชุดที่เล็กที่สุดตามพจนานุกรมได้จากการไล่รับตามหมายเลข โดยรับเมื่อจำนวนรวมไม่ลด และคำถามว่าจำนวนรวมลดไหม ยุบเหลือการนับงานในช่องว่างเดียว ซึ่งตอบได้ด้วยการกระโดดตามท่าโลภแบบสองยกกำลัง
ในหน้านี้