programming.in.th · ข้อ 2031
ค่าจ้างต้องเป็นสัดส่วนกับคุณวุฒิ และทุกคนต้องได้ไม่ต่ำกว่าที่ขอ ห้าแสนคนจึงลองทีละทีมไม่ได้ บทนี้หาว่าใครคุมราคา แล้วเดินจากคนราคาถูกไปแพง พร้อมฮีปที่คอยทิ้งคนที่แพงที่สุดในมือ
เรากำลังจะเปิดไซต์ก่อสร้าง มีผู้สมัคร N คน คนที่ k บอกมาสองอย่าง
คือเงินเดือนขั้นต่ำที่เขายอมรับ S ดอลลาร์ และระดับคุณวุฒิ Q
ใครที่เราจ้าง ต้องได้เงินไม่ต่ำกว่าที่เขาขอ
แต่มีกฎของสมาคมก่อสร้างมาขวางอยู่ข้อหนึ่ง คือค่าจ้างของคนที่เราจ้างต้องเป็นสัดส่วนกับคุณวุฒิพอดี ถ้าคนงาน A มีคุณวุฒิเป็นสามเท่าของคนงาน B เราต้องจ่าย A เป็นสามเท่าของที่จ่าย B เป๊ะ ๆ จะจ่ายเป็นเศษสตางค์ก็ได้ ไม่ต้องเป็นจำนวนเต็ม
เรามีงบ 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 ทั้งสามคนขอ 10 ดอลลาร์เท่ากัน ถ้าจ้างคนที่ 2 (คุณวุฒิ 2) กับคนที่ 3 (คุณวุฒิ 3) เราต้องจ่าย 10 กับ 15 ดอลลาร์ ทั้งคู่ได้ไม่ต่ำกว่าที่ขอ สัดส่วน 2 ต่อ 3 ถูกกฎ และรวมกัน 25 ไม่เกินงบ 40 คำตอบคือจำนวนคน ไม่ใช่ยอดเงิน
ใบ้
กฎสัดส่วนทำให้ทั้งทีมผูกกันด้วยตัวเลขตัวเดียว คือเงินที่จ่ายต่อหนึ่งหน่วยคุณวุฒิ พอตัวเลขนี้ถูกกำหนดแล้ว ค่าจ้างของทุกคนก็ตามมาเองหมด
ลองถามว่าตัวเลขนั้นต้องสูงอย่างน้อยเท่าไร ถึงจะไม่มีใครในทีมได้ต่ำกว่าที่ขอ และในทีมหนึ่งทีม มีกี่คนที่มีเสียงในเรื่องนี้จริง ๆ
สี่คนนี้มาจากตัวอย่างในโจทย์ ลองกดเลือกดู แล้วสังเกตว่าตัวเลขค่าจ้างของทุกคนเปลี่ยนตามใคร
ยังไม่ได้เลือกใคร
กดการ์ดเพื่อจ้างหรือปลดคนนั้น ตัวเลข "ได้" คือเงินที่เขาได้จริงตามกฎสัดส่วน
การ์ดที่ขอบสีทองคือคนที่ตั้งราคาให้ทีมตอนนี้ ลองปลดเขาออกแล้วดูว่าค่าจ้างของคนอื่นเปลี่ยนไปยังไง
ลองจัดทีมให้ครบทั้งสี่ชุดก่อนนะครับ โดยเฉพาะชุดหกคนงบ 110 ที่คนขอเงินน้อยที่สุดกลับเป็นตัวถ่วง พอมีคำตอบในใจแล้ว ค่อยเปิดเฉลย
ที่มาของแนวคิดนี้
ผมเริ่มจากคำถามที่เล็กที่สุดก่อน คือถ้ามีทีมมาให้หนึ่งทีม จ่ายเท่าไร
ยังไม่ถามเลยว่าทีมไหนดี เพราะถ้าคิดราคาของทีมเดียวยังไม่ออก ก็ยังไม่มีอะไรให้เพิ่มประสิทธิภาพ
กฎสัดส่วนบอกว่าทุกคนได้ Q คูณตัวเลขตัวเดียวกัน พอเขียนเงื่อนไข "ได้ไม่ต่ำกว่าที่ขอ" ของแต่ละคนออกมา
ก็เห็นว่าตัวเลขนั้นต้องไม่ต่ำกว่า S/Q ของทุกคนในทีม
ตรงนี้คือจุดที่โจทย์เปลี่ยนหน้าตา ตัวเลขที่ต่ำที่สุดที่ใช้ได้คือ S/Q ที่สูงที่สุดในทีม
แปลว่าในทีมหนึ่งทีม มีแค่คนเดียวที่ได้เงินพอดีกับที่ขอ ที่เหลือได้เกินกันหมด
และค่าจ้างรวมก็แยกเป็นผลคูณของสองก้อน คืออัตราของคนนั้นกับผลรวมคุณวุฒิของทั้งทีม
สองก้อนนี้ยังผูกกันอยู่ แต่อย่างน้อยผมก็รู้แล้วว่ากำลังสู้กับอะไร
บทเรียนที่ยกไปข้ออื่นได้คือ ก่อนหาทีมที่ดีที่สุด ให้เขียนราคาของทีมเดียวออกมาเป็นสูตรก่อน สูตรนั้นมักบอกเองว่าต้องตรึงตัวไหนไว้
เรียก S/Q ของแต่ละคนว่าอัตราของเขา คือเงินขั้นต่ำที่เขาต้องการต่อหนึ่งหน่วยคุณวุฒิ
ถ้าทีมจ่ายที่อัตรา r คนที่ k จะได้ r × Q
ซึ่งไม่ต่ำกว่า S ก็ต่อเมื่อ r ไม่ต่ำกว่าอัตราของเขา
ทีมจึงต้องจ่ายที่อัตราสูงสุดในทีม ค่าจ้างรวมคืออัตรานั้นคูณผลรวมคุณวุฒิของทั้งทีม
// ค่าจ้างรวมของทีม = (อัตราสูงสุดในทีม) x (ผลรวมคุณวุฒิของทีม)
// เทียบกับงบโดยไม่ต้องหาร: sumQ * S[top] <= W * Q[top]
bool fits = sumQ * S[top] <= W * Q[top]; ภาพนี้อธิบายตัวอย่างที่ 3 ได้ทั้งข้อ ทุกคนขอ 10 ดอลลาร์ แต่คนที่คุณวุฒิสูงมีอัตราต่ำ เพราะเงินก้อนเท่ากันถูกหารด้วยคุณวุฒิที่มากกว่า การจ้างคนคุณวุฒิสูงจึงถูกในแง่อัตรา แต่แพงในแง่ผลรวมคุณวุฒิ โจทย์ทั้งข้อคือการชั่งสองแรงนี้
ทางแรกที่ผมเขียน แล้วตีราคาทิ้ง
สูตรจากตอนที่ 1 มีสองก้อนผูกกัน ท่ามาตรฐานคือตรึงก้อนหนึ่งไว้ ผมตรึงอัตราก่อน
เพราะอัตรามาจากคนคนเดียว จึงมีแค่ N ค่าให้ลอง ส่วนผลรวมคุณวุฒิมาจากทั้งทีม
พอสมมติว่าคนที่ j เป็นคนตั้งราคา คนที่อัตราสูงกว่าเขาก็เข้าทีมไม่ได้
ส่วนคนที่อัตราไม่เกินเขา ใครเข้ามาก็จ่ายที่อัตราเดิม ราคาต่อหัวจึงขึ้นกับคุณวุฒิอย่างเดียว
อยากได้คนเยอะก็หยิบคุณวุฒิน้อยก่อน จนงบหมด
ท่านี้ถูก และผมเขียนมันเป็นโค้ดจริงไว้ใช้เป็นตัวเทียบ แต่ต้องเรียงคุณวุฒิใหม่ทุกครั้งที่เปลี่ยนคนตั้งราคา
ที่ N ห้าแสน คือราวสองแสนห้าหมื่นล้านครั้ง วัดจริงที่ N เท่ากับ 5,000 ใช้ไป 0.47 วินาทีแล้ว
สิ่งที่ต้องหาต่อจึงชัดขึ้นมาก คือจะไม่ทิ้งงานที่ทำไปแล้วตอนเปลี่ยนคนตั้งราคาได้ยังไง
มีรายละเอียดหนึ่งที่ต้องเช็กให้ชัด ตอนตรึงคนที่ j เป็นคนตั้งราคา
ผมไม่ได้บังคับให้เขาอยู่ในทีมด้วย ถ้าทีมที่หยิบมาไม่มีเขา อัตราจริงของทีมจะต่ำกว่าหรือเท่ากับของเขา
ค่าจ้างจริงจึงไม่มีทางแพงกว่าที่คิดไว้ ทีมนั้นจ่ายไหวแน่นอน
และทีมที่ดีที่สุดของโจทย์ก็มีคนตั้งราคาของมันอยู่หนึ่งคนเสมอ
คำตอบจึงเท่ากับค่ามากที่สุดจากการลองให้ทุกคนเป็นคนตั้งราคา ไม่ขาดไม่เกิน
เรียงผู้สมัครตามอัตราจากน้อยไปมาก แล้วเดินทีละคน ตอนเดินมาถึงคนที่ j
คนที่ผ่านมาแล้วทั้งหมดคือคนที่อัตราไม่เกินเขาพอดี ซึ่งก็คือกลุ่มที่ตอนที่ 2 ต้องกรองใหม่ทุกรอบ
ทีนี้กลุ่มนั้นโตขึ้นทีละคน ไม่ต้องสร้างใหม่อีกแล้ว
สิ่งที่ถืออยู่ในมือคือคุณวุฒิของทีมปัจจุบัน รับคนที่ j เข้ามา
แล้วถ้าผลรวมคุณวุฒิคูณอัตราของเขาเกินงบ ให้ทิ้งคนที่คุณวุฒิสูงสุดในมือออกทีละคนจนงบพอ
เพราะที่อัตราเดียวกัน คนคุณวุฒิสูงคือคนที่แพงที่สุด จำนวนคนในมือหลังจบรอบคือทีมที่ใหญ่ที่สุดของคนตั้งราคาคนนี้
และคำตอบคือค่ามากที่สุดของตัวเลขนี้ตลอดทาง
แกะคำศัพท์ · heap
ของที่ต้องหยิบ "ตัวที่คุณวุฒิสูงสุด" ออกได้เร็ว ๆ ซ้ำไปเรื่อย ๆ คือฮีป (heap)
คำนี้อ่านว่า "ฮีป" แปลตรงตัวว่ากองของ แบบกองผ้าที่ชิ้นบนสุดหยิบได้ทันที
ส่วนชิ้นล่าง ๆ ไม่ได้เรียงกันเป๊ะ เพราะเราไม่เคยต้องใช้มัน
ใน C++ คือ priority_queue ใส่ของหรือหยิบหัวกองออกครั้งละ log N
คำถามที่ควรค้างอยู่ในหัวตอนนี้คือ คนที่ถูกทิ้งไปแล้ว ไม่เคยถูกเรียกกลับเลย ถ้าอีกหลายรอบต่อมาเขากลับเป็นคนที่ควรอยู่ในทีมล่ะ คำตอบคือไม่มีทาง เขาถูกทิ้งตอนที่งบไม่พอแม้ที่อัตราถูกกว่า และอัตราหลังจากนี้มีแต่ขยับขึ้น ทีมที่ต้องมีเขาจึงมีแต่แพงขึ้นเรื่อย ๆ ขั้นตอนเต็มอยู่ในกล่องข้างล่าง
อัตราถูกเรียงให้ขึ้นอย่างเดียว ของที่ทิ้งไปแล้วจึงไม่มีวันกลับมาคุ้ม และทั้งข้อเหลืองานแค่เรียงหนึ่งครั้งกับฮีปหนึ่งกอง
เดินผู้สมัครของตัวอย่างที่ 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 ตรงกับเอาต์พุตของตัวอย่าง สังเกตแถวสุดท้าย คนที่อัตราแพงที่สุดเข้ามาแล้วดันทุกคนออกจนเหลือตัวเขาคนเดียว แต่คำตอบไม่เสียหาย เพราะเราเก็บค่ามากที่สุดของทุกรอบไว้แล้ว
เวลาที่วัดได้บนเครื่องผม ผู้สมัคร 500,000 คนที่ค่าสุ่มเต็มช่วง ใช้เวลา 0.32 วินาที ส่วนชุดที่งบน้อยจนฮีปต้องทิ้งคนแทบทุกรอบ ใช้ 0.29 วินาที จากลิมิต 2 วินาที ตัวเลขนี้รวมเวลาอ่านอินพุตหนึ่งล้านจำนวนด้วย
ข้อนี้มีสองจังหวะ จังหวะแรกคือเขียนราคาของตัวเลือกเดียวให้เป็นสูตรก่อน แล้วสังเกตว่ามีตัวแปรหนึ่งที่มาจากสมาชิกคนเดียว (อัตราสูงสุด) ตัวแปรแบบนั้นตรึงได้ทีละค่า จังหวะที่สองคือพอเรียงตามตัวแปรที่ตรึงแล้ว กลุ่มที่ใช้ได้โตขึ้นทีละคน งานที่ต้องเริ่มใหม่ทุกรอบจึงกลายเป็นการดูแลกองเดียวต่อเนื่องไปทั้งทาง
| ทาง | ขอบเขตที่มันไหว | งานคร่าว ๆ |
|---|---|---|
| ลองทุกทีม | N ราว 20 | 2 ยกกำลัง N ทีม ที่ N เท่ากับ 500,000 เกินจะนับ |
| ตรึงคนตั้งราคาทีละคน แล้วเรียงคุณวุฒิใหม่ทุกครั้ง | N หลักพัน | ราว 500,000 ยกกำลังสอง คือสองแสนห้าหมื่นล้าน |
| เดินอัตราจากถูกไปแพงรอบเดียว พร้อมฮีป | N ถึง 500,000 | ราว 9,500,000 ครั้ง (N คูณ log N) |
ทีมจ่ายที่อัตราของคนที่อัตราสูงสุดในทีม เดินคนตั้งราคาจากถูกไปแพง เก็บคุณวุฒิไว้ในฮีป งบเกินเมื่อไรก็ทิ้งคนคุณวุฒิสูงสุด แล้วตอบขนาดกองที่ใหญ่ที่สุดที่เคยเห็น
ในหน้านี้