programming.in.th · ข้อ 2031
ค่าจ้างต้องเป็นสัดส่วนกับคุณวุฒิ และทุกคนต้องได้ไม่ต่ำกว่าที่ขอ ผู้สมัครห้าแสนคน ลองทีละทีมไม่ทัน บทนี้ไล่จากวิธีที่ถูกแต่ช้าไปจนถึงวิธีที่ทันทุกขนาด
เรากำลังจะเปิดไซต์ก่อสร้าง มีผู้สมัคร N คน คนที่ k บอกมาสองอย่าง
คือเงินเดือนขั้นต่ำที่เขายอมรับ S ดอลลาร์ และระดับคุณวุฒิ Q
ใครที่เราจ้าง ต้องได้เงินไม่ต่ำกว่าที่เขาขอ
แต่มีกฎของสมาคมก่อสร้างมาขวางอยู่ข้อหนึ่ง คือค่าจ้างของคนที่เราจ้างต้องเป็นสัดส่วนกับคุณวุฒิพอดี
เรามีงบ W ดอลลาร์ งานนี้ไม่สนเรื่องคุณวุฒิเลย ขอแค่จำนวนคนมากที่สุด
ถามว่าจ้างได้มากที่สุดกี่คน
อินพุต / ขอบเขต / เอาต์พุต
N กับ W
จากนั้นอีก N บรรทัด บรรทัดที่ k คือ S กับ Q ของผู้สมัครคนที่ k 1 ≤ N ≤ 500,000,
1 ≤ S, Q ≤ 20,000, 1 ≤ W ≤ 10,000,000,000
(งบใหญ่เกิน 32 บิต ต้องใช้ long long) เวลา 2 วินาที หน่วยความจำ 64 เมกะไบต์
| Input | Output |
|---|---|
| 4 100 5 1000 10 100 8 10 20 1 | 2 |
| 3 4 1 2 1 3 1 3 | 3 |
| 3 40 10 1 10 2 10 3 | 2 |
อ่านตัวอย่างนี้ยังไง
ลองอ่านกฎการจ่ายเงินกับตัวอย่างที่ 3 งบ 40 ทั้งสามคนขอ 10 ดอลลาร์เท่ากัน คุณวุฒิ 1, 2, 3
คำตอบคือจำนวนคน (2) ไม่ใช่ยอดเงิน
ใบ้
กฎสัดส่วนทำให้ทั้งทีมผูกกันด้วยตัวเลขตัวเดียว คือเงินที่จ่ายต่อหนึ่งหน่วยคุณวุฒิ พอตัวเลขนี้ถูกกำหนดแล้ว ค่าจ้างของทุกคนก็ตามมาเองหมด
ลองถามว่าตัวเลขนั้นต้องสูงอย่างน้อยเท่าไร ถึงจะไม่มีใครในทีมได้ต่ำกว่าที่ขอ และในทีมหนึ่งทีม มีกี่คนที่มีเสียงในเรื่องนี้จริง ๆ
สี่คนนี้มาจากตัวอย่างในโจทย์ ลองกดเลือกดู แล้วสังเกตว่าตัวเลขค่าจ้างของทุกคนเปลี่ยนตามใคร
ยังไม่ได้เลือกใคร
กดการ์ดเพื่อจ้างหรือปลดคนนั้น ตัวเลข "ได้" คือเงินที่เขาได้จริงตามกฎสัดส่วน
การ์ดที่ขอบสีทองคือคนที่ตั้งราคาให้ทีมตอนนี้ ลองปลดเขาออกแล้วดูว่าค่าจ้างของคนอื่นเปลี่ยนไปยังไง
ลองจัดทีมให้ครบทั้งสี่ชุดก่อนนะครับ ชุดหกคนลองหลาย ๆ แบบ แล้วจดจำนวนคนที่มากที่สุดที่ทำได้ พอมีคำตอบในใจแล้ว ค่อยเปิดเฉลย
ที่มาของแนวคิดนี้
ผมเริ่มจากคำถามที่เล็กที่สุดก่อน คือถ้ามีทีมมาให้หนึ่งทีม จ่ายเท่าไร คำถามว่าทีมไหนดีที่สุดค่อยตามมาทีหลัง เพราะต้องรู้ราคาของทีมเดียวก่อนถึงจะเทียบทีมได้
กฎสัดส่วนบอกว่าทุกคนได้ Q คูณตัวเลขตัวเดียวกัน
พอเขียนเงื่อนไข "ได้ไม่ต่ำกว่าที่ขอ" ของแต่ละคนออกมา ก็เห็นว่าตัวเลขนั้นต้องไม่ต่ำกว่า S/Q ของทุกคนในทีม
ตัวเลขที่ต่ำที่สุดที่ใช้ได้คือ S/Q ที่สูงที่สุดในทีม
ที่ตัวเลขนี้ อย่างน้อยหนึ่งคนได้เงินพอดีกับที่ขอ (คนที่ S/Q สูงสุด ถ้าเสมอกันหลายคนก็ได้พอดีทุกคนที่เสมอ)
ที่เหลือได้เท่ากับหรือมากกว่าที่ขอ
บทเรียนที่ยกไปข้ออื่นได้คือ ก่อนหาทีมที่ดีที่สุด ให้เขียนราคาของทีมเดียวออกมาเป็นสูตรก่อน สูตรนั้นมักบอกเองว่าต้องตรึงตัวไหนไว้
เรียก S/Q ของแต่ละคนว่าอัตราของเขา คือเงินขั้นต่ำที่เขาต้องการต่อหนึ่งหน่วยคุณวุฒิ
ถ้าทีมจ่ายที่อัตรา r คนนั้นจะได้ r × Q
r × Q ไม่ต่ำกว่า S ก็ต่อเมื่อ r ไม่ต่ำกว่าอัตราของเขา// ค่าจ้างรวมของทีม = (อัตราสูงสุดในทีม) x (ผลรวมคุณวุฒิของทีม)
// เทียบกับงบโดยไม่ต้องหาร: sumQ * S[top] <= W * Q[top]
bool fits = sumQ * S[top] <= W * Q[top]; ทุกคนในตัวอย่างที่ 3 ขอ 10 ดอลลาร์ แต่คนที่คุณวุฒิสูงมีอัตราต่ำ เพราะเงินก้อนเท่ากันถูกหารด้วยคุณวุฒิที่มากกว่า
คนคุณวุฒิสูงถูกในแง่อัตรา แต่แพงในแง่ผลรวมคุณวุฒิ ทีมที่ดีต้องชั่งสองอย่างนี้
ทางแรกที่ผมเขียน
สูตรจากตอนที่ 1 มีสองก้อนผูกกัน คืออัตราของคนตั้งราคากับผลรวมคุณวุฒิ ท่ามาตรฐานคือตรึงก้อนหนึ่งไว้
ผมตรึงอัตราก่อน เพราะอัตรามาจากคนคนเดียว มีแค่ N ค่าให้ลอง
วิธีนี้ให้คำตอบถูก โค้ดที่ผมเขียนเรียงคุณวุฒิใหม่ทุกครั้งที่เปลี่ยนคนตั้งราคา
วัดจริงที่ N = 5,000 ใช้ 0.68 ถึง 0.74 วินาที และที่ 20,000 ใช้ 14.2 วินาที
ถ้าเรียงคุณวุฒิครั้งเดียวตั้งแต่แรก แล้วแต่ละรอบเดินตามลำดับนั้นข้ามคนที่อัตราสูงเกิน ก็ตัด log ออกเหลือราว N2
แต่ที่ N ห้าแสนยังช้าเกินอยู่ดี สิ่งที่ต้องหาต่อคือ จะเก็บงานของรอบก่อนไว้ใช้ต่อตอนเปลี่ยนคนตั้งราคาได้ยังไง
สมมติว่าคนที่ j เป็นคนตั้งราคา เรียกคนที่อัตราไม่เกินเขาว่ากลุ่มที่ใช้ได้
ทีมรับได้เฉพาะคนในกลุ่มนี้
j ค่าตัวต่อหัวคืออัตรานั้นคูณ QQ น้อยที่สุดคำตอบคือจำนวนที่หยิบได้มากที่สุด เมื่อลองให้ทุกคนเป็นคนตั้งราคา ตารางนี้ทำแบบนั้นกับตัวอย่างที่ 1 งบ 100
| คนตั้งราคา | อัตรา | คุณวุฒิที่ใช้ได้ (น้อยไปมาก) | หยิบได้ | ค่าจ้างรวม |
|---|---|---|---|---|
| คนที่ 1 | 0.005 | 1,000 | 1 | 5 |
| คนที่ 2 | 0.1 | 100, 1,000 | 1 | 10 |
| คนที่ 3 | 0.8 | 10, 100, 1,000 | 2 | 88 |
| คนที่ 4 | 20 | 1, 10, 100, 1,000 | 1 | 20 |
ตอนตรึงคนที่ j ทีมที่หยิบมาจะมีเขาหรือไม่มีก็ได้
ถ้าไม่มีเขา อัตราจริงของทีมต่ำกว่าหรือเท่ากับของเขา ค่าจ้างจริงก็อยู่ในงบ
ส่วนทีมที่ดีที่สุดของโจทย์มีคนตั้งราคาของมันอยู่หนึ่งคนเสมอ ค่ามากที่สุดจากการลองให้ทุกคนเป็นคนตั้งราคาจึงเท่ากับคำตอบพอดี
ตารางของตอนที่ 2 บอกทางไว้แล้ว เรียงผู้สมัครตามอัตราจากน้อยไปมาก แล้วเดินทีละคน
ตอนเดินมาถึงคนที่ j คนที่ผ่านมาแล้วคือกลุ่มที่ใช้ได้ของเขา
กลุ่มนี้โตขึ้นทีละคน แค่เติมคนใหม่เข้าไป
ถ้ามีหลายคนอัตราเท่ากัน ตอนเดินถึงคนแรกในกลุ่มที่เท่ากัน คนที่เหลือในกลุ่มนั้นยังไม่ได้ถูกเติมเข้ามา รอบนั้นเห็นกลุ่มที่ใช้ได้แค่บางส่วน
ตอนเดินถึงคนสุดท้ายในกลุ่มที่เท่ากัน ทุกคนถูกเติมครบแล้ว และอัตราเท่าเดิม รอบนั้นให้ทีมที่ใหญ่ที่สุดของอัตรานี้ คำตอบรวมก็ครบ
ระหว่างเดิน เราถือทีมปัจจุบันไว้ เรียกว่ามือ (เหมือนไพ่ในมือ ที่รับเข้าและทิ้งออกได้) ของที่เก็บมีสามอย่าง
sumQ: ผลรวมคุณวุฒิในมือ บวกตอนรับคนเข้า ลบตอนทิ้งคนออกbest: ขนาดมือที่ใหญ่ที่สุดที่เคยเห็น นี่คือคำตอบรอบของคนที่ j ทำสองขั้น
sumQsumQ × S[j] > W × Q[j] (คือค่าจ้างรวมที่อัตราของเขาเกินงบ เขียนแบบคูณไขว้ให้เป็นจำนวนเต็ม)
ทิ้งคนที่คุณวุฒิสูงสุดในมือออกหนึ่งคน
เหตุผลที่ทิ้งคนคุณวุฒิสูงสุด: รอบนี้ทุกคนในมือได้ที่อัตราเดียวกัน ค่าตัวของแต่ละคนคืออัตรานั้นคูณ Q
ขนาดมือหลังจบรอบคือทีมที่ใหญ่ที่สุดเมื่อคนที่ j ตั้งราคา
แกะคำศัพท์ · heap
ที่เก็บที่หยิบ "ตัวที่คุณวุฒิสูงสุด" ออกได้เร็วซ้ำไปเรื่อย ๆ คือ ฮีป (heap) อ่านว่า "ฮีป" แปลตรงตัวว่ากองของ ในที่นี้คือกองตัวเลขที่จัดให้ตัวมากที่สุดอยู่บนสุดเสมอ
ตัวที่อยู่ข้างล่างเรียงกันแค่บางส่วน พอให้หาตัวบนสุดใหม่ได้เร็วหลังหยิบออก
ใน C++ คือ priority_queue ใส่หนึ่งตัวหรือหยิบตัวบนสุดออก ใช้ราว log N ก้าว
log N (ล็อก N) คือจำนวนครั้งที่ต้องหาร N ครึ่งจนเหลือ 1 ที่ N = 500,000 คือราว 19 ทุกคนเข้าฮีปครั้งเดียวและออกได้ไม่เกินครั้งเดียว งานของฮีปทั้งหมดราว 2 × N × 19 ก้าว
คำถามที่ควรค้างอยู่ในหัวตอนนี้คือ คนที่ถูกทิ้งไปแล้วไม่เคยถูกเรียกกลับเลย ถ้าอีกหลายรอบต่อมาเขากลับเป็นคนที่ควรอยู่ในทีมล่ะ
ตอนเขาถูกทิ้ง ทุกคนที่เดินผ่านมาแล้วและคุณวุฒิน้อยกว่าเขาอยู่ในมือครบ และงบก็ยังไม่พอ ส่วนเหตุผลว่ารอบหลังจากนี้ก็ยังไม่ต้องใช้เขา ทั้งแบบแทนคนในมือและแบบเพิ่มเข้าไปอีกคน อยู่ในกล่องข้างล่าง
โจทย์บน programming.in.th บอกแค่ขอบเขตเต็ม การ์ดกับตารางข้างล่างเลยเทียบกันตามขนาด N
วิธีที่ 1
ราว N × 2N ก้าว
ทีมที่เป็นไปได้มี 2N ทีม แต่ละทีมไล่ N คนหาคนตั้งราคากับผลรวมคุณวุฒิ ถูกแน่นอน ใช้เป็นตัวตรวจ
ทันถึงราว N = 24 · วัดได้ 0.07 วินาทีที่ N = 20
วิธีที่ 2
ราว N2 + N2 log N / 2 ก้าว
แต่ละคนกรองทั้งแถว N ก้าว แล้วเรียงกลุ่มที่ใช้ได้ ซึ่งเฉลี่ยราวครึ่งแถว
ทันถึงราว N = 8,200 · วัดได้ 0.68 ถึง 0.74 วินาทีที่ N = 5,000
วิธีเต็ม
ราว 3N log N ≈ 28 ล้านก้าว ที่ N = 500,000
เรียงครั้งเดียว N log N ทุกคนเข้าฮีปครั้งเดียว และออกไม่เกินครั้งเดียว ครั้งละ log N
ทันทุกขนาด · วัดได้ไม่เกิน 0.35 วินาทีที่ N = 500,000
| วิธี | N = 20 | N = 5,000 | N = 500,000 |
|---|---|---|---|
| ลองทุกทีม | 2.1 × 107 | 7.1 × 101508 | 5 × 10150520 |
| ตรึงคนตั้งราคา | 1,264 | 1.8 × 108 | 2.6 × 1012 |
| ท่าเต็ม | 259 | 184,316 | 2.8 × 107 |
เดินผู้สมัครของตัวอย่างที่ 1 จากอัตราถูกไปแพง งบ 100
| คนที่ | S | Q | อัตรา | คุณวุฒิในมือ | ทิ้ง | ค่าจ้างรวม | ในมือ |
|---|---|---|---|---|---|---|---|
| 1 | 5 | 1,000 | 0.005 | 1,000 | · | 5 | 1 |
| 2 | 10 | 100 | 0.1 | 100 | 1,000 | 10 | 1 |
| 3 | 8 | 10 | 0.8 | 10, 100 | · | 88 | 2 |
| 4 | 20 | 1 | 20 | 1 | 100, 10 | 20 | 1 |
ขนาดในมือตลอดทางคือ 1, 1, 2, 1 ค่ามากที่สุดคือ 2 ตรงกับเอาต์พุตของตัวอย่าง
แถวสุดท้าย คนที่อัตราแพงที่สุดเข้ามาแล้วดันทุกคนออกจนเหลือตัวเขาคนเดียว
คำตอบไม่เสียหาย เพราะ best จำค่ามากที่สุดของทุกรอบไว้แล้ว
ตรวจอะไรไปแล้วบ้าง
N = 500,000: 0.14 ถึง 0.17 ถึง 0.35 วินาที สูงสุดราว 17 MB
(ลิมิต 2 วินาที 64 MB)
เวลาวัดบน WSL ด้วย g++ -O2 สามรอบ รวมเวลาอ่านอินพุต
ทั้งหมดนี้ทดสอบแล้ว ไม่ได้พิสูจน์ และยังไม่ได้ส่งเกรดเดอร์จริง บทพิสูจน์ของวิธีเต็มอยู่ในตอนที่ 3
ในหน้านี้