programming.in.th · ข้อ 2040

Tournament: ใครชนะไม่สำคัญ ก้อนที่ถูกยุบสำคัญกว่า

แทรกอัศวินที่มาสายลงในแถวตรงไหนก็ได้ ให้เขาชนะมากที่สุด กับดักอยู่ที่คำตอบเป็นตำแหน่งที่แทรก ไม่ใช่จำนวนรอบที่ชนะ กุญแจคือก้อนของตำแหน่งที่ถูกยุบในแต่ละรอบเหมือนกันหมดไม่ว่าจะแทรกตรงไหน คิดครั้งเดียวใช้ได้ทุกตำแหน่ง แล้วมองว่าเขาไม่มีวันตายจนถึงรอบที่แพ้จริง

★★★★☆ BITdsuoffline อ่าน 12 นาที 11 กันยายน 2026

โจทย์ · อัศวินมาสาย

ทหาร N คนยืนเรียงเป็นเส้นตรง นับตำแหน่งจาก 0 ถึง N-1 ทุกคนมีค่าพลังไม่ซ้ำกัน ตั้งแต่ 0 คืออ่อนที่สุด ถึง N-1 คือแรงที่สุด เจ้าของงานจะเรียกประลองทั้งหมด C รอบ รอบหนึ่งเขาเอ่ยเลขสองตัวคือ S กับ E ทุกคนที่ยืนอยู่ในช่วง S ถึง E ของแถวตอนนั้น ต้องสู้กัน คนที่พลังมากที่สุดชนะและยืนอยู่ที่เดิม ที่เหลือออกจากสนาม แล้วคนที่ยังอยู่ก็เบียดเข้าหากันและนับเลขตำแหน่งกันใหม่

ตอนนี้มีคนยืนอยู่แล้ว N-1 คน ตามลำดับที่โจทย์ให้มา เหลืออัศวินหนึ่งคนที่มาสาย พลังของเขาคือ R เราเลือกได้ว่าจะให้เขาไปแทรกตรงไหนของแถว เป้าหมายคือให้เขาชนะให้ได้มากรอบที่สุด รอบที่เขาไม่ได้ถูกเรียกลงสนามไม่นับ และพอเขาแพ้ครั้งแรกเขาก็ออกจากสนามเหมือนคนอื่น

อินพุต / ขอบเขต / เอาต์พุต

EXAMPLE
InputOutput
5 3 3
1
0
2
4
1 3
0 1
0 1
1
10 7 3
6
2
9
8
1
5
4
0
7
4 5
1 2
0 1
4 5
3 5
1 3
0 1
1

อ่านตัวอย่างนี้ยังไง

ชุดแรกอ่านว่า มีคน 5 คน ประลอง 3 รอบ คนมาสายมีพลัง 3 อีก 4 บรรทัดถัดมาคือแถวเดิม (1, 0, 2, 4) แล้ว 3 บรรทัดสุดท้าย คือคู่ S E ของแต่ละรอบ (1 3 / 0 1 / 0 1) เลขคู่นี้เป็นตำแหน่งในแถวขณะนั้น ไม่ใช่ตำแหน่งตั้งต้น พอจบรอบหนึ่งแถวสั้นลงและนับเลขใหม่ รอบต่อไปคำว่า "ตำแหน่ง 0" จึงหมายถึงคนละคนกับรอบที่แล้วได้

ทีนี้ถึงจุดที่พลาดกันมากที่สุด เอาต์พุตมีหน่วยเป็นตำแหน่งที่แทรก ไม่ใช่จำนวนรอบที่ชนะ ถ้าเราลองแทรกครบทุกช่องของชุดแรก จำนวนรอบที่เขาชนะคือ 1, 2, 2, 2, 0 ตามลำดับช่อง 0 ถึง 4 ค่ามากที่สุดคือ 2 แต่คำตอบที่โจทย์เฉลยคือ 1 ใครที่คิดถูกทุกอย่างแล้วพิมพ์ 2 ออกไปจะผิด ทั้งที่คิดไม่ผิดเลย เลข 1 มาจากช่องที่ได้ 2 รอบซึ่งมีอยู่ 3 ช่อง (ช่อง 1, 2, 3) แล้วหยิบช่องที่เลขน้อยที่สุด

กฎ "เท่ากันให้เอาช่องเลขน้อยสุด" ตรงนี้ผมอนุมานจากตัวอย่างสองชุด ไม่ได้อ่านเจอในโจทย์ และชุดที่ชี้ขาดคือชุดแรกชุดเดียว เพราะชุดที่สองจำนวนรอบสูงสุดคือ 1 ซึ่งบังเอิญเท่ากับเลขคำตอบพอดี มันจึงแยกสองความหมายไม่ออก (ชุดที่สองมีช่องที่ดีที่สุดอยู่ 6 ช่อง และช่องเลขน้อยสุดในนั้นคือ 1) ต้นฉบับของโจทย์นี้รับตำแหน่งที่ดีที่สุดช่องไหนก็ได้ ถ้าส่งแล้วโดนตีกลับทั้งที่มั่นใจในอัลกอริทึม ให้สงสัยกติกาการตัดสินเสมอกันก่อนสงสัยอัลกอริทึม

แทรกที่ช่อง 1 · ชนะ 2 รอบ 01234 รอบ 1 1 3 0 2 4 ชนะ รอบ 2 1 3 4 ชนะ รอบ 3 3 4 แพ้ จบ 4 แทรกที่ช่อง 4 · ชนะ 0 รอบ 01234 รอบ 1 1 0 2 4 3 ไม่ได้ลง รอบ 2 1 4 3 ไม่ได้ลง รอบ 3 4 3 แพ้ จบ 4
เดินตัวอย่างชุดแรกสองแบบ ซ้ายคือแทรกที่ช่อง 1 ซึ่งเป็นคำตอบ ขวาคือแทรกสุดปลายแถว กรอบทองคือคนมาสาย กรอบเขียวคือคนที่ถูกเรียกลงสนามรอบนั้น กรอบเขียวหนาคือผู้ชนะของรอบ เลขบนสุดคือตำแหน่งตั้งต้น แบบขวาไม่ได้ลงสนามสองรอบแรกเลย พอโดนเรียกก็เจอ 4 ทันที จึงได้ 0 รอบ

ใบ้

ท่าที่ตรงที่สุดคือลองแทรกให้ครบทุกช่องแล้วจำลองแถวดู แต่มีช่องให้ลอง N ช่อง แต่ละช่องต้องเดิน C รอบ แค่คูณสองตัวนี้ที่ขอบเขตใหญ่สุดก็เป็น 9,999,600,003 ครั้งแล้ว ยังไม่นับว่าการเดินหนึ่งรอบต้องกวาดคนในช่วง S ถึง E อีก

ลองกลับไปดูรูปข้างบนอีกที เทียบสองฝั่งดูว่ารอบที่หนึ่งกินคนกลุ่มไหนของแถวตั้งต้น ฝั่งซ้ายมันกินช่อง 1 ถึง 3 ฝั่งขวามันกินช่องอะไร แล้วสองฝั่งนี้เหมือนกันหรือต่างกัน

ลองเอง · เลือกช่องให้อัศวินที่มาสาย

ชุดอุ่นเครื่อง เดินตามที่อ่านมาข้างบนได้เลย ลองกดช่องท้ายแถวดูด้วยว่าเกิดอะไรขึ้น

 

กดช่องสีเทาช่องใดช่องหนึ่งเพื่อแทรกอัศวินที่มาสายลงตรงนั้น

ช่องที่ลองแล้วจะติดเลขจำนวนรอบที่ชนะไว้ กดซ้ำได้ไม่จำกัด

แถวสีน้ำเงินคืออัศวินที่ยืนอยู่แล้วพร้อมค่าพลัง ช่องสีเทาคั่นระหว่างพวกเขาคือที่ว่างที่แทรกได้ รายการรอบอยู่เหนือกระดาน ตัวเลข S กับ E นับบนแถว ณ ตอนนั้น

ลองให้ครบทั้งสี่ชุดก่อนนะครับ โดยเฉพาะชุดที่สองกับชุดที่สาม พอเริ่มเดาถูกว่าช่องไหนดีก่อนกด ค่อยไปดูเฉลย

เฉลย ตอนที่ 1 · แต่ละรอบยุบก้อนเดิม ไม่ว่าจะแทรกตรงไหน

ที่มาของแนวคิดนี้

ผมไม่ได้ลองท่าอื่นแล้วทิ้ง เพราะท่าตรง ๆ ตายตั้งแต่คูณเลข ช่องให้แทรกมี N ช่อง รอบมี C รอบ และขอบเขตบอกว่า C ไปได้ถึง N-2 แค่ 99,999 คูณ 99,997 ก็ 9,999,600,003 แล้ว ยังไม่นับการกวาดช่วงในแต่ละรอบ สิ่งที่ผมต้องหาจึงไม่ใช่ท่าที่เร็วขึ้นนิดหน่อย แต่เป็นของบางอย่างที่คิดครั้งเดียวแล้วใช้ได้กับทุกช่องพร้อมกัน

แรงกดดันนั้นพาไปสู่คำถามว่า อะไรในโจทย์ที่ไม่เปลี่ยนเมื่อเราย้ายที่แทรก ลองนั่งเทียบสองฝั่งของรูปข้างบน จะเห็นว่าคนที่รอดต่างกันแน่นอน ฝั่งซ้ายรอบแรกเหลือ 3 ฝั่งขวาเหลือ 4 แต่ขอบของกลุ่มคนที่ถูกยุบรวมกัน อยู่ที่เดิมทั้งสองฝั่ง ตรงนี้เป็นจุดที่คิดกลับด้านกันง่ายมาก เพราะสัญชาตญาณบอกว่าถ้าคนรอดเปลี่ยน ทุกอย่างข้างหลังก็ต้องเปลี่ยนตาม

บทเรียนที่เอาไปใช้ข้ออื่นได้คือ เวลาต้องตอบคำถามเดิมซ้ำ ๆ กับอินพุตที่ต่างกันนิดเดียว ให้ไล่หาว่าส่วนไหนของกระบวนการเป็นของกลาง แล้วคิดส่วนนั้นให้จบก่อนครั้งเดียว

ตกลงกันเรื่องคำก่อน คำว่าก้อนในที่นี้หมายถึงช่วงของตำแหน่งตั้งต้น คือเลข 0 ถึง N-1 ของแถวก่อนประลองรอบแรก ไม่ใช่เลขตำแหน่งที่ขยับไปมาระหว่างเกม ทุกคนที่ยังยืนอยู่ในสนามเป็นตัวแทนของก้อนหนึ่งก้อน ตอนเริ่มเกมทุกคนเป็นตัวแทนของก้อนขนาดหนึ่งคือตัวเอง พอจบรอบที่กิน S ถึง E ก้อนของคนเหล่านั้นก็เชื่อมต่อกันเป็นก้อนเดียว โดยมีผู้ชนะเป็นตัวแทนคนใหม่

ประเด็นอยู่ตรงนี้ ก้อนใหม่คือ [A, B] เดิมเสมอ ไม่ว่าใครจะเป็นคนชนะ เพราะการชนะเปลี่ยนแค่ว่าใครถือก้อนนี้ไว้ ไม่ได้เปลี่ยนว่าก้อนกินตำแหน่งตั้งต้นไหนบ้าง และค่าพลังของใครก็ไม่เกี่ยวกับเรื่องนี้เลย ลำดับของก้อนจึงคิดครั้งเดียวจบ ใช้ได้กับทั้ง N ช่องพร้อมกัน

ช่องตั้งต้น อัศวินเดิม 0 1 2 3 4 K0 = 1 K1 = 0 K2 = 2 K3 = 4 รอบ 1 · ก้อน [1, 3] · ไม่มีใครแรงกว่า 3 · ชนะ รอบ 2 · ก้อน [0, 3] · ไม่มีใครแรงกว่า 3 · ชนะ รอบ 3 · ก้อน [0, 4] · มีคนแรงกว่า 3 อยู่ 1 คน · แพ้
ก้อนของตัวอย่างชุดแรก แถวบนคือช่องตั้งต้น 0 ถึง 4 แถวกลางคืออัศวินเดิมทั้ง 4 คน วาดคร่อมรอยต่อของช่อง เพราะ K[i] จะไปอยู่ช่อง i หรือช่อง i+1 ก็ได้ แล้วแต่ว่าแทรกคนมาสายไว้ตรงไหน แถบล่างคือก้อนที่แต่ละรอบยุบรวม สังเกตว่าคนที่อยู่ในก้อน [A, B] ครบทั้งคนคือ K[A] ถึง K[B-1] เสมอ ไม่ขึ้นกับที่แทรก กล่องแดงคือคนที่แรงกว่า 3 ซึ่งเข้ามาอยู่ในก้อนครั้งแรกตอนรอบที่ 3

การหาขอบ [A, B] ของแต่ละรอบก็แค่ถามว่า คนที่ยืนอยู่ตำแหน่งที่ S ของแถวตอนนี้ คือตัวแทนของก้อนที่เริ่มตรงไหน และคนที่ตำแหน่ง E คือก้อนที่จบตรงไหน ท่ามาตรฐานคือทำเครื่องหมายว่าช่องตั้งต้นไหน "ยังเป็นหัวก้อนอยู่" แล้วถามหาหัวก้อนลำดับที่ S ซึ่งเป็นคำถามที่ BIT ตอบได้ด้วยการไต่ลงจากบนสุด ใช้เวลา log N ต่อครั้ง ทั้งหมดจึงเป็น C log N

เฉลย ตอนที่ 2 · สมมติไปเลยว่าเขาไม่มีวันตาย

ปัญหาที่เหลือคือ พอคนมาสายแพ้แล้วเขาจะถูกถอดออกจากแถว แถวหลังจากนั้นก็เลยขึ้นกับว่าเราแทรกเขาไว้ตรงไหน ก้อนที่เราเพิ่งคิดไว้ใช้ได้เพราะมันไม่ขึ้นกับค่าพลัง แต่มันคิดจากแถวที่มีเขาอยู่ตลอดเวลา

ทางออกคือสมมติว่าเขาอมตะ คือปล่อยให้เขาอยู่ในแถวต่อไปแม้แพ้แล้ว แล้วนับเฉพาะช่วงก่อนที่เขาจะแพ้ครั้งแรก เหตุผลคือก่อนถึงรอบที่เขาแพ้ครั้งแรก แถวอมตะกับแถวจริงเหมือนกันทุกประการ เขายังไม่เคยถูกถอด ทุกอย่างจึงเดินเหมือนกันเป๊ะ และรอบที่เขาแพ้ก็ยังเป็นรอบเดียวกัน สิ่งที่ต่างกันเริ่มตั้งแต่หลังรอบนั้น ซึ่งเป็นช่วงที่เราไม่นับคะแนนอยู่แล้ว

พูดอีกแบบ คำตอบของช่อง p คือจำนวนรอบที่เขาชนะในแถวอมตะ นับไปจนถึงรอบแรกที่เขาแพ้แล้วหยุด พอคิดแบบนี้ได้ ก้อนชุดเดิมก็ใช้ได้ตลอดทั้งเกม เพราะแถวอมตะมีคนเท่าเดิมเสมอไม่ว่าจะแทรกตรงไหน

เฉลย ตอนที่ 3 · หนึ่งบิตต่อรอบ แล้วกวาดทีเดียว

สมมติเราแทรกเขาไว้ช่อง p เขาก็ยึดช่องตั้งต้นหมายเลข p เขาจะได้ลงสนามในรอบที่ j ก็ต่อเมื่อ p อยู่ในก้อน [A, B] ของรอบนั้น และเขาจะชนะรอบนั้นก็ต่อเมื่อในก้อนนั้นไม่มีใครแรงกว่าเขาเลย

คำถามคือ "ในก้อนนั้นมีใครบ้าง" คำตอบสวยกว่าที่คิด ถ้าแทรกเขาที่ช่อง p ช่องตั้งต้นหมายเลข i จะถูกครองโดย K[i] เมื่อ i มาก่อน p และถูกครองโดย K[i-1] เมื่อ i มาหลัง p ดังนั้นคนที่อยู่ในก้อน [A, B] นอกจากตัวเขาเองก็คือ K[A] ถึง K[B-1] พอดี ไม่ว่า p จะเป็นเท่าไร นี่คือสิ่งที่รูปที่สองพยายามบอกด้วยการวาดกล่องอัศวินคร่อมรอยต่อของช่อง

เงื่อนไขชนะจึงกลายเป็น "ผลรวมของ K[A] ถึง K[B-1] ที่แรงกว่า R เท่ากับศูนย์" ซึ่งเป็นคำถามผลรวมช่วงบนอาเรย์คงที่ ตอบด้วยผลรวมสะสมได้ใน O(1) และไม่มี p โผล่มาในเงื่อนไขเลย แต่ละรอบจึงเหลือข้อมูลแค่บิตเดียว คือรอบนี้เป็นรอบที่ชนะหรือรอบที่แพ้ สำหรับทุกคนที่อยู่ในก้อนพร้อมกัน

ที่เหลือคือกวาดรอบตามลำดับแล้วดูแลช่องทั้ง N ช่องไปพร้อมกัน รอบที่เป็นบิตชนะ แปลว่าทุกช่องในก้อนที่ยังไม่ตายได้แต้มเพิ่มหนึ่ง ซึ่งคือการบวกทั้งช่วงแล้วถามทีละจุด ส่วนรอบที่เป็นบิตแพ้ แปลว่าทุกช่องในก้อนที่ยังไม่ตาย ตายพร้อมกันตรงนั้น และคะแนนของมันถูกล็อกไว้ การไล่เก็บเฉพาะช่องที่ยังไม่ตายในช่วงหนึ่ง คือตัวชี้ "ช่องที่ยังไม่ตายถัดไป" แบบ DSU ซึ่งแต่ละช่องถูกแตะตอนตายครั้งเดียวในชีวิต รวมทั้งหมดจึงเป็น N ครั้ง

กวาดตัวอย่างชุดแรก
รอบ S ถึง E (แถวตอนนั้น) ก้อนของช่องตั้งต้น คนแรงกว่า R ในก้อน บิต ผลต่อทุกช่องที่ยังไม่ตาย
1 1 ถึง 3 [1, 3] 0 ชนะ บวก 1 ให้ช่อง 1, 2, 3 ที่ยังไม่ตาย
2 0 ถึง 1 [0, 3] 0 ชนะ บวก 1 ให้ช่อง 0, 1, 2, 3 ที่ยังไม่ตาย
3 0 ถึง 1 [0, 4] 1 แพ้ ช่อง 0, 1, 2, 3, 4 ที่ยังไม่ตาย ตกรอบพร้อมกัน ล็อกคะแนนไว้ตรงนั้น
ค่าพลังของอัศวินเดิมคือ 1, 0, 2, 4 และคนมาสายมีพลัง 3 มีคนเดียวที่แรงกว่าเขาคือ 4 ซึ่งนั่งอยู่ปลายแถว ก้อนสองรอบแรกจึงยังไม่แตะเขา รอบที่ 3 ก้อนกินทั้งแถวพอดี บิตจึงพลิกเป็นแพ้
ผลลัพธ์ต่อช่อง
ช่องที่แทรก 01234
จำนวนรอบที่ชนะ 12220
ช่อง 0 ไม่ได้อยู่ในก้อนของรอบแรก จึงได้แค่รอบที่สอง ช่อง 4 อยู่นอกก้อนสองรอบแรกทั้งคู่ พอถึงรอบสุดท้ายก้อนกินทั้งแถว เขาเลยลงสนามครั้งเดียวแล้วแพ้เลย ได้ 0 ค่าสูงสุดคือ 2 มีอยู่ 3 ช่อง คำตอบที่ต้องพิมพ์คือช่องเลขน้อยสุดในนั้น ได้ 1 ตัวเลขแถวนี้คิดจากการจำลองแถวตรง ๆ ตอนบิลด์ แล้วเทียบกับวิธีข้างบนว่าตรงกัน

รวมค่าใช้จ่ายทั้งหมด หาก้อนด้วย BIT เป็น C log N ตรวจบิตด้วยผลรวมสะสมเป็น C บวกแต้มทั้งช่วงด้วย BIT อีกตัวเป็น C log N และถามทีละจุดตอนตายอีก N log N รวมเป็น O((N + C) log N) ซึ่งพอสำหรับขอบเขตนี้แบบเหลือ ๆ

เฉลย ตอนที่ 4 · ทำไมสัญชาตญาณในเกมถึงพาไปผิดทาง

ชุด "มีคนแรงกว่ายืนขวางอยู่" ในเกมเป็นชุดที่ลงโทษท่าไปยืนข้างคนที่อ่อนที่สุดในแถวเพราะคิดว่าจะชนะง่ายที่สุด ตำแหน่งที่สัญชาตญาณนั้นพาไปคือช่อง 3 ซึ่งได้ 0 รอบ ขณะที่ช่อง 4 ได้ 2 รอบ เหตุผลอ่านออกจากก้อนได้ตรง ๆ คือการอยู่ข้างคนอ่อนไม่ได้แปลว่าจะได้สู้กับคนอ่อน เพราะรอบหนึ่งกินทั้งก้อน ไม่ได้กินแค่เพื่อนบ้านสองข้าง สิ่งที่ต้องดูคือ ก้อนที่ครอบช่องนั้นมีคนแรงกว่าเขาอยู่หรือเปล่า

ชุด "คนมาสายแรงที่สุดในสนาม" เป็นอีกมุมหนึ่ง คนมาสายมีพลัง 6 ซึ่งแรงที่สุดในสนาม บิตของทุกรอบจึงเป็นชนะหมด คำถามเลยกลายเป็นว่าช่องไหนอยู่ในก้อนของรอบมากที่สุด ช่อง 4 อยู่ในก้อนแค่ 0 รอบ ส่วนช่อง 2 อยู่ครบ 3 รอบ ชุดนี้แยกสองเรื่องที่มักปนกันออกจากกัน คือ "แพ้หรือไม่แพ้" กับ "ได้ลงสนามหรือไม่ได้ลง"

ส่วนชุด "ดีเท่ากันหลายช่อง" มีช่องที่ดีที่สุดอยู่ 4 ช่อง คือช่อง 3, 4, 5, 6 ทุกช่องได้ 2 รอบเท่ากัน คำตอบที่ต้องพิมพ์คือ 3 ซึ่งเป็นช่องเลขน้อยที่สุดในกลุ่มนั้น ตามกติกาที่ผมอนุมานไว้ตั้งแต่ต้นหน้า

โค้ด

ดูโค้ดเต็มขอบเขต
tournament.cpp
// IOI 2012 "Tournament" (programming.in.th task 2040) - full solution, O((N + C) log N)
//
// Idea:
//   1) The merge structure is independent of the strengths and of the insert position.
//      Every surviving knight represents a contiguous block of INITIAL positions 0..N-1,
//      so round j merges current elements S_j..E_j into one block [A_j, B_j] of initial
//      positions.  A Fenwick tree with find-kth gives A_j, B_j in O(log N) each.
//   2) Treat the late knight as immortal.  Up to and including the first round he loses,
//      the immortal run is identical to the real run, so counting his wins in the immortal
//      run until the first loss is exactly the answer.
//   3) Inserting him at position p means he occupies initial position p.  He takes part in
//      round j iff p is in [A_j, B_j], and (during the prefix before his first loss) he wins
//      iff no knight stronger than R sits anywhere in [A_j, B_j] other than himself, i.e.
//      iff sum f[A_j .. B_j - 1] == 0 where f[i] = (K[i] > R).  That condition does not
//      depend on p, only on the round -> one bit per round.
//   4) Sweep the rounds in order: a winning round adds +1 to every still-alive p in
//      [A_j, B_j] (range-add / point-query Fenwick), a losing round kills every still-alive
//      p in the range (DSU "next alive" pointer), freezing its answer.  Each p dies once.
#include <bits/stdc++.h>
using namespace std;

namespace fastsol {

struct Fen {
    int n;
    vector<int> t;
    void init(int n_) { n = n_; t.assign(n + 1, 0); }
    void add(int i, int v) { for (++i; i <= n; i += i & -i) t[i] += v; }
    int sum(int i) const { int s = 0; for (++i; i > 0; i -= i & -i) s += t[i]; return s; }
    // 0-indexed k-th set position (assumes it exists)
    int kth(int k) const {
        int pos = 0, rem = k + 1, LOG = 0;
        while ((1 << (LOG + 1)) <= n) ++LOG;
        for (int pw = 1 << LOG; pw > 0; pw >>= 1) {
            if (pos + pw <= n && t[pos + pw] < rem) { pos += pw; rem -= t[pos]; }
        }
        return pos;
    }
};

// returns {best insert position (smallest one achieving the max), max number of wins}
pair<int, int> solve(int N, int C, int R, const vector<int>& K,
                     const vector<pair<int, int>>& rounds,
                     vector<int>* out = nullptr) {
    // ---- 1) block of initial positions merged by each round -------------------
    Fen act;
    act.init(N);
    for (int i = 0; i < N; ++i) act.add(i, 1);
    int cnt = N;
    vector<int> A(C), B(C);
    for (int j = 0; j < C; ++j) {
        int S = rounds[j].first, E = rounds[j].second;
        int a = act.kth(S);
        int b = (E + 1 < cnt) ? act.kth(E + 1) - 1 : N - 1;
        A[j] = a; B[j] = b;
        for (int r = S + 1; r <= E; ++r) act.add(act.kth(S + 1), -1); // absorb the losers
        cnt -= (E - S);
    }

    // ---- 2) per-round win flag ------------------------------------------------
    // pre[i] = number of original knights among K[0..i-1] that are stronger than R
    vector<int> pre(N, 0);                       // K has N-1 entries, so pre has N entries
    for (int i = 0; i + 1 < N; ++i) pre[i + 1] = pre[i] + (K[i] > R ? 1 : 0);
    vector<char> win(C);
    for (int j = 0; j < C; ++j) win[j] = (pre[B[j]] - pre[A[j]] == 0);

    // ---- 3) sweep -------------------------------------------------------------
    Fen diff;                 // range add, point query
    diff.init(N + 1);
    vector<int> nxt(N + 2);   // DSU: next still-alive position >= i
    for (int i = 0; i < N + 2; ++i) nxt[i] = i;
    function<int(int)> find = [&](int x) {
        while (nxt[x] != x) { nxt[x] = nxt[nxt[x]]; x = nxt[x]; }
        return x;
    };
    vector<int> ans(N, -1);

    for (int j = 0; j < C; ++j) {
        int a = A[j], b = B[j];
        if (win[j]) {
            diff.add(a, 1);
            if (b + 1 <= N) diff.add(b + 1, -1);
        } else {
            for (int p = find(a); p <= b; p = find(p + 1)) {
                ans[p] = diff.sum(p);   // frozen at the moment he is knocked out
                nxt[p] = p + 1;         // dead
            }
        }
    }
    for (int p = 0; p < N; ++p) if (ans[p] < 0) ans[p] = diff.sum(p);

    int bestP = 0, bestV = ans[0];
    for (int p = 1; p < N; ++p) if (ans[p] > bestV) { bestV = ans[p]; bestP = p; }
    if (out) *out = ans;
    return {bestP, bestV};
}

} // namespace fastsol

#ifndef STRESS_BUILD
int main() {
    int N, C, R;
    if (scanf("%d %d %d", &N, &C, &R) != 3) return 0;
    vector<int> K(N - 1);
    for (int i = 0; i + 1 < N; ++i) scanf("%d", &K[i]);
    vector<pair<int, int>> rounds(C);
    for (int j = 0; j < C; ++j) scanf("%d %d", &rounds[j].first, &rounds[j].second);
    pair<int, int> r = fastsol::solve(N, C, R, K, rounds);
    printf("%d\n", r.first);
    return 0;
}
#endif

ตัว solve ถูกแยกออกมาเป็นฟังก์ชันในเนมสเปซ และ main ถูกครอบด้วย #ifndef STRESS_BUILD เพราะไฟล์นี้ถูกลิงก์เข้าไปในตัวรันสเตรสเทสต์ตัวเดียวกับตัวตรวจ จะได้ไม่ต้องสร้างโพรเซสใหม่ทีละเคส ถ้าเอาไปส่งจริงก็คอมไพล์ตรง ๆ ได้เลย เพราะไม่มีใครนิยาม STRESS_BUILD ให้ พารามิเตอร์ out มีไว้ให้สเตรสเทสต์ขอเวกเตอร์คำตอบทุกช่อง ไม่ใช่แค่ช่องที่ดีที่สุด จะได้เทียบกันได้ทั้งแถว

ดูตัวตรวจที่ใช้สเตรสเทสต์
brute_tournament.cpp
// IOI 2012 "Tournament" - honest brute force oracle.
// For each of the N insert positions, build the explicit line and simulate every round
// literally: scan the range S..E, find the strongest, rebuild the line with only that
// knight kept in the range.  Count a win whenever the late knight is inside the range and
// turns out to be the strongest.  No structure is shared with the fast solution.
#include <bits/stdc++.h>
using namespace std;

namespace brutesol {

pair<int, int> solve(int N, int C, int R, const vector<int>& K,
                     const vector<pair<int, int>>& rounds,
                     vector<int>* out = nullptr) {
    int bestP = -1, bestV = -1;
    if (out) out->assign(N, 0);
    for (int p = 0; p < N; ++p) {
        vector<int> line;
        line.reserve(N);
        for (int i = 0; i < p; ++i) line.push_back(K[i]);
        line.push_back(R);
        for (int i = p; i + 1 < N; ++i) line.push_back(K[i]);

        int wins = 0;
        for (int j = 0; j < C; ++j) {
            int S = rounds[j].first, E = rounds[j].second;
            if (E >= (int)line.size()) { fprintf(stderr, "invalid round\n"); exit(1); }
            int mi = S;
            bool lateHere = false;
            for (int i = S; i <= E; ++i) {
                if (line[i] > line[mi]) mi = i;
                if (line[i] == R) lateHere = true;
            }
            if (lateHere && line[mi] == R) ++wins;
            vector<int> nl;
            nl.reserve(line.size());
            for (int i = 0; i < S; ++i) nl.push_back(line[i]);
            nl.push_back(line[mi]);
            for (int i = E + 1; i < (int)line.size(); ++i) nl.push_back(line[i]);
            line.swap(nl);
        }
        if (out) (*out)[p] = wins;
        if (wins > bestV) { bestV = wins; bestP = p; }
    }
    return {bestP, bestV};
}

} // namespace brutesol

#ifndef STRESS_BUILD
int main() {
    int N, C, R;
    if (scanf("%d %d %d", &N, &C, &R) != 3) return 0;
    vector<int> K(N - 1);
    for (int i = 0; i + 1 < N; ++i) scanf("%d", &K[i]);
    vector<pair<int, int>> rounds(C);
    for (int j = 0; j < C; ++j) scanf("%d %d", &rounds[j].first, &rounds[j].second);
    pair<int, int> r = brutesol::solve(N, C, R, K, rounds);
    printf("%d\n", r.first);
    return 0;
}
#endif

ตัวตรวจสร้างแถวขึ้นมาจริงทีละช่องที่แทรก แล้วเดินทุกรอบตามตัวหนังสือของโจทย์ คือกวาดหาคนแรงสุดในช่วง แล้วสร้างแถวใหม่ที่เหลือแค่คนนั้นในช่วงนั้น จุดสำคัญคือมันไม่รู้จักคำว่าก้อนเลย ข้ออ้างหลักของหน้านี้คือก้อนเหมือนกันทุกช่องที่แทรก ถ้าตัวตรวจใช้ข้ออ้างเดียวกัน มันก็จะตรวจได้แค่ว่าผมพิมพ์โค้ดถูกไหม ไม่ได้ตรวจว่าข้ออ้างนั้นจริงไหม

ที่รันจริง: ไล่ครบทุกกรณีของ N ตั้งแต่ 3 ถึง 7 ทุกค่า R ทุกการสลับของค่าพลังที่เหลือ และทุกลำดับรอบที่ถูกกติกา รวม 25,929,102 เคส เทียบเวกเตอร์คำตอบทุกช่องกับตัวตรวจ ไม่ต่างกันสักเคส จากนั้นสุ่มเพิ่มอีก 12,144 เคส ครอบคลุมรูปแบบรอบสี่แบบ โดยบังคับให้ R เป็นคนแรงสุด อ่อนสุด แรงรองสุด และสุ่ม ก็ไม่ต่างกันอีก รวมทั้งหมด 25,941,246 ครั้งที่เทียบกัน และตัวอย่างทั้งสองชุดในโจทย์ตอบ 1 ถูกต้อง ส่วนเวลา ที่ N เท่ากับ 100000 และ C เท่ากับ 99998 วัดได้ 0.03 ถึง 0.07 วินาที จากชุดหนักที่จัดรูปไว้ห้าแบบ เทียบกับลิมิต 1 วินาที (สังเกตว่า C ไม่เกิน N-2 ชุดที่ N กับ C เท่ากันเป๊ะจึงสร้างไม่ได้) หน้านี้ยังรันด่านตรวจของตัวเองตอนบิลด์อีก 3,034 เคสสุ่ม เทียบวิธีที่สอนกับการจำลองตรง ๆ

ท่าที่ติดมือกลับไป

ท่าแรกคือ เวลาต้องตอบคำถามเดิมซ้ำกับอินพุตที่ต่างกันนิดเดียว ให้มองหาโครงสร้างที่เป็นของกลาง ที่นี่คือลำดับของก้อน ซึ่งไม่สนใจทั้งค่าพลังและตำแหน่งที่แทรก พอแยกของกลางออกมาได้ งานที่เหลือต่อหนึ่งช่องก็เล็กลงจนกวาดทีเดียวได้ทั้งหมด

ท่าที่สองคือ การสมมติให้ตัวละครอมตะเพื่อให้กระบวนการเป็นของกลาง ใช้ได้ทุกครั้งที่การตายของตัวละคร เกิดขึ้นแค่ครั้งเดียวและเราสนใจแค่ช่วงก่อนหน้านั้น เงื่อนไขที่ต้องเช็กให้ชัดคือ โลกอมตะกับโลกจริงต้องเหมือนกันจนถึงจุดที่เราหยุดนับ ถ้าเงื่อนไขนี้ไม่จริง ท่านี้ใช้ไม่ได้

ท่าที่สามเป็นเรื่องนิสัยมากกว่าอัลกอริทึม คืออ่านให้ชัดว่าโจทย์ขออะไรเป็นเอาต์พุต ข้อนี้ตัวอย่างชุดที่สองปิดบังความต่างไว้พอดีเพราะเลขบังเอิญเท่ากัน เหลือชุดแรกชุดเดียวที่ชี้ขาด เวลาเจอโจทย์ที่คำตอบเป็น "ตำแหน่งที่ดีที่สุด" ให้เช็กตัวอย่างทุกชุดว่ามันแยก "ค่าที่ดีที่สุด" กับ "ตำแหน่งที่ให้ค่านั้น" ออกจากกันได้จริงไหม

สรุปบรรทัดเดียว

หาลำดับก้อนของตำแหน่งตั้งต้นครั้งเดียวด้วย BIT เปลี่ยนแต่ละรอบให้เหลือบิตเดียวว่าชนะหรือแพ้ ด้วยผลรวมสะสมของคนที่แรงกว่า R แล้วกวาดรอบทีเดียวโดยบวกแต้มทั้งช่วงเวลาชนะ และฆ่าทั้งช่วงเวลาแพ้ ปิดท้ายด้วยการตอบตำแหน่งที่เลขน้อยที่สุดในบรรดาช่องที่ดีที่สุด

แหล่งที่มา

  1. โจทย์ Tournament บน programming.in.th ข้อ 2040 programming.in.th/tasks/2040 (สืบค้น 11 กันยายน 2026)
  2. ต้นฉบับคือ Jousting tournament ของ International Olympiad in Informatics 2012 ที่เมือง Sirmione และ Montichiari ประเทศอิตาลี เป็นโจทย์ของวันแข่งที่สอง ioi2012.org/competition/tasks
  3. ฉบับที่ยังส่งโค้ดไปตรวจได้อยู่ ใช้ชื่อ IOI12_tournament oj.uz/problem/view/IOI12_tournament
  4. กติกา "ถ้าดีเท่ากันให้ตอบช่องเลขน้อยสุด" ในหน้านี้ ผมอนุมานจากตัวอย่างสองชุดของ programming.in.th ไม่ใช่ข้อความที่เขียนไว้ในโจทย์ ต้นฉบับของ IOI รับตำแหน่งที่ดีที่สุดช่องไหนก็ได้