programming.in.th · ข้อ 2032

ยิงธนู: เลิกจำลองกระดาน แล้วเปลี่ยนสนามทั้งสนามให้เป็นค่าน้อยสุดของช่วง

รื้อเฉลยจากลูกบาศก์ลงมาเหลือ N log N ทีละชั้น เริ่มจากตัดข้อมูลที่ไม่มีผลทิ้ง แล้วเปลี่ยนการจำลองเป็นสูตรปิด และปิดท้ายด้วยเหตุผลว่าทำไมไม่ต้องลองจุดเริ่มต้นครบทุกจุด

★★★★★ simulationinvariantbinary searchad hoc อ่าน 26 นาที เผยแพร่ 6 กันยายน 2026 อัปเดต 19 ตุลาคม 2026

โจทย์ · แทรกแถวเข้าไปในทัวร์นาเมนต์ที่หมุนไม่หยุด

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

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

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

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

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

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

หมายเลขทักษะเขียนบรรทัดละคน ตัวแรกคือเราเสมอ

ตรงไหนที่ทำให้ข้อนี้ไม่ง่าย

ท่าแรกที่ทุกคนคิดถึงคือจำลองมันตรง ๆ ลองแทรกตัวเองทุกจุด แล้วเดินการแข่งให้ครบ R รอบ ปัญหาคือตัวเลขในโจทย์ไม่ยอมให้ทำ จุดที่แทรกได้มี N จุด แต่ละจุดต้องเดิน R รอบ และหนึ่งรอบต้องจัดการทั้ง N เป้า คูณกันแล้วได้ราว N × R × N ซึ่งที่ขอบเขตบนคือราว 40,000,000,000,000,000,000 ก้าว

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

ใบ้

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

ลองเอง · เลือกจุดยืนเริ่มต้นให้จบที่เป้าน้อยที่สุด

ตัวอย่างแรกของโจทย์ สี่เป้า แปดคน

กดจุดยืนเพื่อดูว่าซ้อมจนจบแล้วเราไปอยู่เป้าไหน แล้วเลือกจุดที่ดีที่สุด

ลองไปแล้ว 0 จุด · ดีที่สุดที่เจอคือเป้า ยังไม่มี

เป้าหมายคือจบให้ได้เป้าที่เลขน้อยที่สุด ถ้ามีหลายจุดที่จบเท่ากัน โจทย์ขอจุดยืนที่เลขมากที่สุด

ลองไล่ให้ครบทุกจุดแล้วดูลำดับของผลลัพธ์ ถ้ามันไม่ได้เรียงจากน้อยไปมาก แปลว่าท่าที่อาศัยความเรียง อย่างการค้นหาแบบทวิภาค (binary search คือเดาตรงกลางแล้วตัดทิ้งครึ่งหนึ่งทุกครั้ง) ใช้ตรง ๆ ไม่ได้

เฉลย ตอนที่ 1 · โลกทั้งใบเหลือคนสองประเภท

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

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

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

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

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

  • ป้าย 1 เจอป้าย 1 คนที่เก่งกว่าในสองคนชนะแล้วออกไป อีกคนอยู่ต่อ ทั้งคู่เป็น 1
  • ป้าย 1 เจอป้าย 0 ผู้ชนะเป็น 1 ผู้แพ้เป็น 0
  • ป้าย 0 เจอป้าย 0 ผู้ชนะเป็น 0 ผู้แพ้เป็น 0

สรุปเป็นกฎเดียวว่า ทุกเป้าส่งป้ายที่สูงกว่าออกไป และเก็บป้ายที่ต่ำกว่าไว้

ตอนนี้ไม่ต้องเก็บว่าใครยืนตรงไหนอีกแล้ว เก็บแค่ตัวเลขเดียวต่อเป้า คือเป้านั้นมีคนป้าย 1 กี่คน ซึ่งเป็น 0, 1 หรือ 2 เรียกมันว่า c[i]

เป้าที่มี 1 1 ออกไปทางซ้าย 1 อยู่ต่อ 1 เป้าที่มี 1 0 ออกไปทางซ้าย 1 อยู่ต่อ 0 เป้าที่มี 0 0 ออกไปทางซ้าย 0 อยู่ต่อ 0
ทุกเป้าส่งป้ายที่สูงกว่าออกไปทางซ้าย และเก็บป้ายที่ต่ำกว่าไว้ สนามทั้งสนามจึงเล่าได้ด้วยตัวเลข 0 ถึง 2 ต่อหนึ่งเป้า (วาดประกอบโดยผู้เขียน)

แปลงกฎ "ส่งตัวสูงออก เก็บตัวต่ำไว้" ให้เป็นสมการของ c ได้ตรง ๆ โดยจำไว้ว่าเป้า 1 กับเป้า N ผิดฝาผิดตัวกว่าเพื่อน เพราะที่เป้า 1 ผู้ชนะอยู่ต่อและผู้แพ้ถูกเหวี่ยงไปเป้า N

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

วงเล็บเหลี่ยมอย่าง [c[1] ≥ 1] อ่านว่า "ถ้าเงื่อนไขในวงเล็บจริงนับหนึ่ง ไม่จริงนับศูนย์" และ c' คือค่าในรอบถัดไป

สามบรรทัดต่างกันแค่ว่าใครถูกเก็บ ใครถูกส่งต่อ

  • บรรทัดกลาง (เป้า 2 ถึง N - 1) เก็บผู้แพ้ของตัวเอง ซึ่งเป็นป้าย 1 ก็ต่อเมื่อเดิมมีป้าย 1 สองคน (ถ้ามีคนเดียว คนนั้นชนะแล้วเดินออกไป) แล้วรับผู้ชนะจากเป้าทางขวา ซึ่งเป็นป้าย 1 เมื่อเป้านั้นมีป้าย 1 อย่างน้อยหนึ่งคน
  • บรรทัดบน (เป้า 1) ที่นี่ผู้ชนะอยู่ต่อ ของที่เก็บไว้จึงเป็นป้าย 1 เมื่อมีป้าย 1 แค่หนึ่งคนก็พอ เป้า 1 จึงเป็นตะแกรงที่ดูดคนเก่งไปจอด (และเป็นเหตุให้ตอนที่ 7 ต้องแบ่งสองเฟส)
  • บรรทัดล่าง (เป้า N) ไม่มีเป้าทางขวาให้รับ มันรับผู้แพ้ของเป้า 1 ที่ถูกเหวี่ยงมาแทน จึงนับเมื่อเป้า 1 มีป้าย 1 ครบสองคน

สามบรรทัดนี้ทำให้กระดานเดินต่อได้โดยไม่ต้องรู้ว่าใครเป็นใคร รู้แค่จำนวนป้าย 1 ต่อเป้าก็พอ

เฉลย ตอนที่ 2 · เราคือคนอ่อนที่สุดในกลุ่มของตัวเอง

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

  • ถ้าเรายืนที่เป้า i แล้ว c[i] = 2 ป้าย 1 อีกคนที่ไม่ใช่เราอยู่ด้วย เราแพ้เขาแน่นอน
  • ถ้า c[i] = 1 ป้าย 1 ตัวเดียวนั้นคือเรา คู่แข่งจึงเป็นป้าย 0 และเราชนะแน่นอน

สรุปคือเราไม่ต้องรู้อะไรเกี่ยวกับคู่แข่งเลย นอกจากเลข c ของช่องที่เรายืน ชะตาของเราแต่ละรอบเขียนได้สามบรรทัด

ชะตาของเราหนึ่งรอบ
bool oppStrong = (c[pos] == 2);
if (pos == 1) pos = oppStrong ? N : 1;   // แพ้ที่เป้า 1 คือถูกส่งไปท้ายแถว
else if (!oppStrong) pos--;              // ชนะที่เป้าอื่นคือขยับไปทางซ้าย

สังเกตว่าที่เป้า 1 การ "ชนะ" ไม่ได้พาเราไปไหน เพราะผู้ชนะที่เป้า 1 ยืนอยู่ที่เดิม ส่วนการ "แพ้" ที่เป้า 1 คือหายนะเต็ม ๆ เพราะโดนส่งไปเป้า N ไกลสุด แล้วต้องไต่กลับมาใหม่

เฉลย ตอนที่ 3 · ทำไมโจทย์ถึงรับประกันว่า R ≥ 2N

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

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

หลักฐานแรกมาจากการพิมพ์กระดานออกมาดูเฉย ๆ หลังผ่านไป 2N รอบ แล้วดูอีก N รอบว่า ใครเปลี่ยนเป้าบ้าง ใครไม่เปลี่ยนเลย พอเห็นว่าคนที่วนคือลำดับ 2 ถึง N+1 พอดีเป๊ะ ผมถึงกลับไปหาเหตุผลจากกติกา

ก่อนเชื่อ ผมสุ่มมาตรวจกับ N ตั้งแต่ 2 ถึง 8 อย่างละ 150 ชุด ไม่มีชุดไหนที่ชุดคนวนไม่ใช่ 2 ถึง N+1 ถ้าเริ่มจากพยายามพิสูจน์ ผมคงยังไม่รู้ด้วยซ้ำว่าจะพิสูจน์อะไร การสังเกตก่อนจึงประหยัดเวลาไปมาก

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

คนที่เก่งรองลงมาไต่มาถึงเป้า 1 แล้วแพ้ ถูกส่งไปเป้า N ไต่กลับมาใหม่ แพ้อีก วนแบบนี้ทุก N รอบ ภาพรวมของสนามหลังทุกคนเข้าที่ (สภาวะคงตัว หรือ steady state) จึงเป็นแบบนี้

พอเข้าสภาวะคงตัว ลำดับ 1 จอดที่เป้า 1 ถาวร ลำดับ 2 ถึง N+1 กลายเป็นขบวนที่วนซ้ายรอบละหนึ่งช่อง ส่วนที่เหลือคือลำดับ N+2 ถึง 2N ค้างนิ่งอยู่เป้าละคน

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

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

ช่วงก่อนทุกคนเข้าที่ เรียกว่าช่วงตั้งตัว ผมอ่านเงื่อนไข R ≥ 2N ว่าโจทย์ใส่มาเพื่อให้ R เลยช่วงนี้ไปแล้ว

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

ไม่ต้องเดินครบ R รอบ

หลังช่วงตั้งตัว ทุก N รอบสนามกลับมาหน้าเดิม เดินให้พ้นช่วงตั้งตัว แล้วเดินต่อแค่เศษที่หารคาบไม่ลงตัว (mod คือเศษจากการหาร เช่น 10 mod 4 = 2)

โค้ดรอบแรกเผื่อไว้ที่ 3N แทน 2N เพราะเผื่ออีกหนึ่งคาบราคาถูก ได้ T ไม่เกิน 4N รอบ ไม่ว่า R จะใหญ่แค่ไหน ส่วนโค้ดเต็มใช้ 2N ตามที่อธิบายข้างบน

เฉลย ตอนที่ 4 · เดินกระดานให้ดูหนึ่งชุด

ลองเอากระดาน c กติกาแพ้ชนะของเรา และจำนวนรอบ T มาเดินบนตัวอย่างจริง

เอาตัวอย่างที่สองมาเดิน หมายเลขทักษะของเราคือ 2 จากทั้งหมด 8 คน จึงมีป้าย 1 อยู่แค่ 2 คน (นับตัวเราเองด้วย) และเราเลือกเริ่มที่เป้า 2

กดถัดไปเพื่อเดินกระดานทีละรอบ

ตัวอย่างที่ 2 · เริ่มที่เป้า 2
รอบ c[1]c[2]c[3]c[4] เรายืนที่ คู่แข่ง ผล
1 1[1]00 เป้า 2 ป้าย 0 เราชนะ ชนะ ขยับไปเป้า 1
2 [2]000 เป้า 1 ป้าย 1 เก่งกว่าเรา แพ้ ถูกส่งไปเป้า 4
3 100[1] เป้า 4 ป้าย 0 เราชนะ ชนะ ขยับไปเป้า 3
4 10[1]0 เป้า 3 ป้าย 0 เราชนะ ชนะ ขยับไปเป้า 2
5 1[1]00 เป้า 2 ป้าย 0 เราชนะ ชนะ ขยับไปเป้า 1
6 [2]000 เป้า 1 ป้าย 1 เก่งกว่าเรา แพ้ ถูกส่งไปเป้า 4
7 100[1] เป้า 4 ป้าย 0 เราชนะ ชนะ ขยับไปเป้า 3
8 10[1]0 เป้า 3 ป้าย 0 เราชนะ ชนะ ขยับไปเป้า 2
9 1[1]00 เป้า 2 ป้าย 0 เราชนะ ชนะ ขยับไปเป้า 1

ช่องที่ใส่วงเล็บเหลี่ยมคือเป้าที่เรายืน ไล่คอลัมน์ c ลงมาจะเห็นเลข 1 ไหลไปทางซ้ายรอบละช่อง รวมแล้วเราเจอป้าย 0 (ชนะ) 7 รอบ เจอป้าย 1 (แพ้) 2 รอบ และจบที่เป้า 1

ทำแบบเดียวกันกับทุกจุดที่แทรกได้ แล้วเทียบกัน

เป้าที่ไปจบ เทียบทุกจุดที่แทรกได้
เริ่มที่เป้า 1234 ตอบ
ตัวอย่างที่ 1 จบที่เป้า 4224 3
ตัวอย่างที่ 2 จบที่เป้า 4123 2
ตัวอย่างแรกจบที่เป้า 2 ได้จากสองจุดเริ่มต้น กติกาข้อรองจึงเข้ามาตัดสิน ให้เลือกจุดเริ่มที่มากกว่า ได้คำตอบ 3 ส่วนตัวอย่างที่สองมีจุดเดียวที่จบถึงเป้า 1

แถวบนของตารางนี้เตือนอะไรบางอย่าง ค่าที่ได้คือ 4 2 2 4 ซึ่งไม่ได้เรียงทางเดียว เริ่มขวากว่าไม่ได้แปลว่าจบขวากว่า

จึงข้ามการลองบางจุดด้วยการค้นหาแบบทวิภาคตรง ๆ ไม่ได้ ตอนที่ 8 จะกลับมาแก้เรื่องนี้

โค้ด C++ รอบแรก · ท่าตรงไปตรงมาที่ได้ 20 คะแนน

โค้ดเดินตามสี่ตอนที่เล่ามา สร้างกระดาน c ของจุดเริ่มต้นหนึ่งจุด เดินไม่เกิน 4N รอบ แต่ละรอบขยับตำแหน่งเราด้วยเลขตัวเดียว แล้วอัปเดตกระดานทั้งแถว

งานต่อหนึ่งจุดเริ่มต้นคือ 4N รอบ คูณ N เป้า ทั้งข้อจึงเป็น O(N³) (อ่านว่า "โอ เอ็นกำลังสาม" แปลว่างานโตตาม N คูณกันสามครั้ง) ค่าคงที่ต่ำเพราะวงในบวกแค่เลข 0 กับ 1

เวลาที่วัดได้บนเครื่องผม · R เต็มขอบเขต
Nเวลา (มิลลิวินาที)เทียบกับลิมิต 2 วินาที
200 38 เหลือเวลา 1,962 มิลลิวินาที
500 501 เหลือเวลา 1,499 มิลลิวินาที
800 2,053 เกินลิมิต 53 มิลลิวินาที
1,200 6,388 เกินลิมิต 4,388 มิลลิวินาที
ชุด 20 คะแนนคือ N ≤ 200 จบใน 38 มิลลิวินาที ที่ N = 500 ยังทัน แต่ที่ N = 800 เกินลิมิตแล้ว ห่างจากชุด 60 คะแนน (N ≤ 5,000) อีกไกล
ดูโค้ดเต็ม
archery.cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    long long N, R;
    scanf("%lld %lld", &N, &R);
    vector<int> s(2 * N);
    for (int i = 0; i < 2 * N; i++) scanf("%d", &s[i]);
    int me = s[0];
    vector<int> o(s.begin() + 1, s.end());     // คนอื่น 2N-1 คน เรียงจากซ้ายไปขวา

    // หลัง 2N รอบระบบวนคาบ N ส่วนที่เกินจึงตัดทิ้งได้ เดินจริงไม่เกิน 4N รอบ
    long long T = (R <= 3 * N) ? R : 3 * N + ((R - 3 * N) % N);

    vector<int> c(N + 2), nc(N + 2);
    long long bestFinal = LLONG_MAX, bestStart = -1;

    for (long long k = 1; k <= N; k++) {
        // แทรกตัวเองที่เป้า k แล้วคนอื่นจับคู่กันตามลำดับเดิม
        for (long long i = 1; i <= N; i++) {
            int a, b;
            if (i < k)       { a = o[2 * i - 2]; b = o[2 * i - 1]; }
            else if (i == k) { a = o[2 * i - 2]; b = me; }
            else             { a = o[2 * i - 3]; b = o[2 * i - 2]; }
            c[i] = (a <= me) + (b <= me);      // นับคนที่เราไม่ชนะ (รวมตัวเราเอง)
        }

        long long pos = k;
        for (long long q = 0; q < T; q++) {
            // เราคือคนที่อ่อนที่สุดในกลุ่ม "ไม่แพ้ให้เรา" คู่แข่งจึงแข็งกว่าเรา
            // ก็ต่อเมื่อช่องนี้มีคนกลุ่มนั้นอยู่สองคน คือเรากับอีกคนหนึ่ง
            bool oppStrong = (c[pos] == 2);
            if (pos == 1) pos = oppStrong ? N : 1;   // แพ้ที่เป้า 1 คือถูกส่งไปท้ายแถว
            else if (!oppStrong) pos--;              // ชนะที่เป้าอื่นคือขยับไปทางซ้าย

            for (long long i = 1; i <= N; i++) {
                if (i == 1)      nc[1] = (c[1] >= 1) + (N >= 2 ? (c[2] >= 1) : 0);
                else if (i < N)  nc[i] = (c[i] == 2) + (c[i + 1] >= 1);
                else             nc[N] = (c[N] == 2) + (c[1] == 2);
            }
            if (N == 1) nc[1] = c[1];
            swap(c, nc);
        }

        if (pos < bestFinal) { bestFinal = pos; bestStart = k; }
        else if (pos == bestFinal) bestStart = k;    // เสมอกันเลือกเป้าเริ่มต้นที่มากที่สุด
    }

    printf("%lld\n", bestStart);
    return 0;
}
ของแถม: ตัวตรวจสอบที่ผมใช้ก่อนส่ง

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

ผลการเทียบทั้งหมดรวมไว้ที่เดียวในหัวข้อโค้ดเต็มขอบเขต

brute.cpp
// ตัวตรวจสอบแบบซื่อ ๆ จำลองคนจริงทุกคนครบ R รอบ ใช้ได้แค่ N เล็ก ๆ กับ R ไม่กี่ร้อย
// เอาไว้สุ่มเทียบกับโค้ดจริงก่อนส่ง ไม่ใช่โค้ดที่ส่งเข้าระบบตัดสิน
#include <bits/stdc++.h>
using namespace std;

int N; long long R;
int me;
vector<int> o;

int simulate(int k, long long rounds) {
    vector<int> line;
    for (int i = 0; i < 2 * k - 2; i++) line.push_back(o[i]);
    line.push_back(me);
    for (int i = 2 * k - 2; i < (int)o.size(); i++) line.push_back(o[i]);

    vector<array<int, 2>> t(N + 1);
    for (int i = 1; i <= N; i++) { t[i][0] = line[2 * i - 2]; t[i][1] = line[2 * i - 1]; }

    for (long long q = 0; q < rounds; q++) {
        vector<int> win(N + 1), los(N + 1);
        for (int i = 1; i <= N; i++) {
            win[i] = min(t[i][0], t[i][1]);       // เลขลำดับน้อยกว่าคือเก่งกว่า
            los[i] = max(t[i][0], t[i][1]);
        }
        vector<array<int, 2>> nt(N + 1);
        for (int i = 1; i <= N; i++) {
            if (i == 1)      nt[i] = {win[1], N >= 2 ? win[2] : los[1]};
            else if (i < N)  nt[i] = {los[i], win[i + 1]};
            else             nt[i] = {los[N], los[1]};
        }
        if (N == 1) nt[1] = t[1];
        t = nt;
    }
    for (int i = 1; i <= N; i++) if (t[i][0] == me || t[i][1] == me) return i;
    return -1;
}

int main() {
    scanf("%d %lld", &N, &R);
    scanf("%d", &me);
    o.resize(2 * N - 1);
    for (int i = 0; i < 2 * N - 1; i++) scanf("%d", &o[i]);

    int best = INT_MAX, bestK = -1;
    for (int k = 1; k <= N; k++) {
        int f = simulate(k, R);
        if (f < best) { best = f; bestK = k; }
        else if (f == best) bestK = k;
    }
    printf("%d\n", bestK);
}

เฉลย ตอนที่ 5 · ขาดอีกกี่เท่า และต้องตัดตรงไหน

โค้ดรอบแรกได้ 20 คะแนน ก่อนคิดท่าใหม่ ลองวัดก่อนว่าที่ขอบเขตเต็มมันห่างจากงบแค่ไหน และห่างเพราะงานส่วนไหน ตัวเลขจะบอกเองว่าต้องตัดตรงไหน

วิธีที่ 1

จำลองคนจริงครบ R รอบ

N × R × N

แทรกตัวทุกจุด แล้วเดินคนจริงทุกคนครบ R รอบ คือ brute.cpp ที่หน้านี้ใช้เป็นตัวตรวจ

ไม่ผ่านชุดไหนเลย เพราะทุกชุดยอมให้ R ถึง 1,000,000,000

วิธีที่ 2

โค้ดรอบแรก

N × 4N × N

ย่อเป็นกระดาน c และเดินแค่ T รอบ (ตอนที่ 1 ถึง 3) แต่ยังเดินกระดานทั้งแถวทุกรอบ ทุกจุดเริ่ม

ได้ชุด N ≤ 200 (20 คะแนน)

วิธีเต็ม

นับการไหล + ลองแค่ไม่กี่จุด

ราว (จำนวนจุดที่ลอง) × N

หนึ่งรอบใช้งานคงที่ (ตอนที่ 6 และ 7) และลองจุดเริ่มแค่ไม่กี่สิบจุด (ตอนที่ 8)

ได้ครบ 100 คะแนน

ชุด 60 คะแนน (N ≤ 5,000) ไม่มีวิธีกลางของมันเองในหน้านี้ วิธีเต็มกินไปพร้อมกันเลย

10210510810111014 มิลลิวินาที จำลองครบ R รอบโค้ดรอบแรกโค้ดเต็ม (วัดจริง) 1,407 ปี เกินงบ 2.2×1010 เท่า308 วัน เกินงบ 1.3×107 เท่า468 มิลลิวินาที ทัน ลิมิต 2 วินาที
เวลาที่ N = 200,000 และ R = 1,000,000,000 สเกลแบบ log (ขีดละพันเท่า) แท่งโค้ดรอบแรกขยายจากเวลาที่วัดจริงที่ N = 1,200 ตามงานจริง (จุดเริ่ม × จำนวนรอบ T × จำนวนเป้า) แท่งจำลองครบ R รอบสมมติว่าเดินรอบละเร็วเท่าโค้ดรอบแรก ซึ่งใจดีกับมันแล้ว แท่งโค้ดเต็มคือเคสที่ช้าที่สุดที่วัดได้จริง เส้นทองคือลิมิต 2 วินาที

โค้ดรอบแรกที่ N = 200,000 ใช้ราว 308 วัน คำถามคือควรตัดอะไรก่อน ระหว่างจำนวนจุดเริ่ม กับงานต่อหนึ่งจุด

ต่อให้ลองจุดเริ่มแค่จุดเดียว ก็ยังใช้ราว 2.2 นาที เกินงบ 67 เท่า ลดจำนวนจุดเริ่มอย่างเดียวไม่มีทางพอ ต้องลดงานต่อหนึ่งรอบด้วย

คิดกลับจากงบ หนึ่งรอบใช้ได้เท่าไร

  • ตอนที่ 8 จะลดจำนวนครั้งที่ต้องประเมินจุดเริ่มเหลือไม่เกิน 77 ครั้ง แต่ละครั้งเดิน T = 400,000 รอบ
  • งบต่อรอบ = 2,000 มิลลิวินาที ÷ (77 × 400,000) ≈ 65 นาโนวินาที (หนึ่งนาโนวินาทีคือหนึ่งในพันล้านวินาที)
  • โค้ดรอบแรกใช้ราว 221,806 นาโนวินาทีต่อรอบ เพราะไล่ทั้ง 200,000 เป้า ช้ากว่างบราว 3,416 เท่า

แปลว่าในหนึ่งรอบ เราแตะได้แค่ไม่กี่ช่อง ห้ามไล่กระดานทั้งแถว

แผนมีสองชั้น ตอนที่ 6 และ 7 ทำให้หนึ่งรอบเหลืองานคงที่ แล้วตอนที่ 8 ลดจุดเริ่มจาก N จุดเหลือไม่กี่สิบ

เฉลย ตอนที่ 6 · เลิกเดินกระดาน เปลี่ยนไปนับ "การไหล"

สองทางที่ผมลองก่อน แล้วทิ้ง

ทางแรกคือบีบกระดานเป็นบิต เพราะการเดินกระดานหนึ่งรอบเขียนเป็น E' = S | (E >> 1) กับ S' = S & (E >> 1) ได้ตรง ๆ เร็วขึ้น 64 เท่า

คูณเลขออกมาคือ 3,125 คำต่อรอบ คูณ 400,000 รอบ คูณจำนวนครั้งที่ต้องประเมินจุดเริ่ม ซึ่งตอนนั้นผมประมาณไว้ log N ≈ 18 ครั้ง ยังได้ 22,500,000,000 ตกไป

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

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

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

ตัวอย่างประจำตอนที่ 6 และ 7

กระดานที่จะใช้ทั้งสองตอน

  • N = 6 หมายเลขทักษะเรียงตามอินพุตคือ 6 12 7 8 10 11 3 2 5 9 4 1 ตัวแรกคือเรา หมายเลข 6
  • คนที่แข็งกว่าเราคือหมายเลข 1 ถึง 5 รวม 5 คน เราเลือกเริ่มที่เป้า 3

n คือจำนวนคนแข็งต่อเป้า ต่างจาก c ของตอนที่ 1 ตรงที่ไม่นับตัวเรา คนแข็งชนะเราทุกคน การเคลื่อนที่ของพวกเขาจึงไม่ขึ้นกับเรา และ c ของช่องที่เรายืนคือ n บวกหนึ่ง

n(0) มาจากไหน · แทรกตัวที่เป้า 3 แล้วจับคู่ทีละสองคน
เป้าสองคนในเป้าคนแข็ง (หมายเลขน้อยกว่า 6)
112, 70
28, 100
3เรา, 110
43, 22
55, 91
64, 12
คอลัมน์ขวาอ่านจากบนลงล่างคือ n(0) = 0, 0, 0, 2, 1, 2

นับคนที่ออก แทนการดูกระดาน

ให้ F_i(t) คือจำนวนคนแข็งที่ออกจากเป้า i ไปแล้วทั้งหมด เมื่อผ่านไป t รอบ คนที่อยู่ในเป้าตอนนี้คือคนตั้งต้น บวกคนที่ไหลเข้ามาจากเป้าทางขวา ลบคนที่ไหลออกไป

สิ่งเดียวที่ต้องรู้ต่อรอบ

n_pos(t) = n_pos(0) + F_pos+1(t) − F_pos(t)

ถ้าค่านี้อย่างน้อย 1 คู่แข่งของเราแข็งกว่า เราแพ้ ถ้าเป็น 0 เราชนะ ใช้ F แค่สองเป้า ไม่ต้องดูเป้าอื่นเลย

นึกภาพ

ให้เป้าแต่ละเป้าคือด่านเก็บเงินบนทางด่วนเลนเดียว และคนแข็งแต่ละคนคือรถหนึ่งคัน รถวิ่งจากขวาไปซ้าย ด่านที่มีรถรออยู่ปล่อยได้รอบละคัน F_i(t) จึงคือจำนวนรถที่ผ่านเป้า i ไปแล้ว

จากภาพนี้ จำนวนที่ผ่านเป้า i ใน t รอบมีเพดานได้หลายแบบ ซึ่งคิดได้โดยไม่ต้องเดินทีละรอบ

  • ปล่อยได้รอบละคน จึงผ่านได้ไม่เกิน t คน (เพดานแบบ j = 0)
  • ต้องมาถึงก่อน มองแค่ j เป้าแรกนับจากเป้า i คนแข็งที่อยู่ในช่วงนั้นตั้งแต่ต้นผ่านได้อย่างมากเท่าที่มี ส่วนคนที่อยู่ไกลกว่านั้นต้องใช้อย่างน้อย j รอบกว่าจะมาถึง จึงผ่านได้อีกไม่เกิน t − j คน

สูตรการไหล

F_i(t) = ค่าน้อยสุด เมื่อ j = 0..t ของ (t − j) + n_i(0) + n_i+1(0) + … + n_i+j−1(0)

สูตรที่มีแค่การบวกกับการเลือกค่าน้อยสุดแบบนี้ เรียกว่าแบบ min-plus (อ่านว่า มิน-พลัส) ใช้ได้เมื่อทุกเป้าบนทางเป็นเป้าธรรมดา คือเป้า 2 ถึง N ส่วนเป้า 1 รอตอนที่ 7

เพดานบอกแค่ว่าไม่เกิน ส่วนเหตุที่ค่าน้อยสุดเท่ากับคำตอบพอดี มาจากพีชคณิต ไม่ได้มาจากภาพ ทุกรอบ F เพิ่มขึ้น [n ≥ 1] พอดี และเมื่อ n ไม่ติดลบ ค่านี้เท่ากับ min(1, n) เป๊ะ

ได้สมการแบบเท่ากับทุกรอบ F_i(t+1) = min(F_i(t) + 1, n_i(0) + F_i+1(t)) แทนซ้ำลงไปจนถึงรอบ 0 ก็ยังเป็นเท่ากับตลอดทาง และผลที่ได้คือสูตรข้างบน (ไล่ทีละบรรทัดในโค้ดข้างล่าง)

ที่มาของสูตร ทีละบรรทัด
// F_i(t) = จำนวนคนแข็งที่ออกจากเป้า i ไปแล้ว เมื่อผ่านไป t รอบ
// ความจริงข้อเดียวที่ต้องเชื่อ: เป้าธรรมดาที่มีคนแข็ง ปล่อยออกไปรอบละหนึ่งคนเสมอ
//   n_i(t)   = n_i(0) + F_{i+1}(t) - F_i(t)        ตั้งต้น + ไหลเข้า - ไหลออก
//   F_i(t+1) = F_i(t) + [ n_i(t) >= 1 ]
// เมื่อ n ไม่ติดลบ [ n >= 1 ] มีค่าเท่ากับ min(1, n) แทนค่าแล้วได้
//   F_i(t+1) = min( F_i(t) + 1 , F_{i+1}(t) + n_i(0) )
// คลี่ซ้ำไปจนถึงรอบ 0 (ที่ F ทุกตัวเป็น 0) ทุกขั้นเลือกได้สองทาง
//   "ตรง" อยู่เป้าเดิม บวก 1   หรือ   "ทแยง" ขยับไปเป้าขวา บวก n(0) ของเป้าที่ผ่าน
// เดินทแยง j ครั้ง ตรง t - j ครั้ง ลำดับไม่มีผล เหลือแค่เลือก j
//   F_i(t) = min over j in [0, t] ของ  (t - j) + ( n_i(0) + ... + n_{i+j-1}(0) )

ลองกับตัวอย่างประจำตอน

คำถาม: ผ่านไป 5 รอบ เป้า 2 ปล่อยคนแข็งออกไปกี่คน แทนค่า n(0) ของเป้า 2 ถึง 6 ซึ่งคือ 0, 0, 2, 1, 2

  • j = 0: 5 + 0 = 5
  • j = 1: 4 + 0 = 4
  • j = 2: 3 + 0 = 3
  • j = 3: 2 + 2 = 4
  • j = 4: 1 + 3 = 4
  • j = 5: 0 + 5 = 5

ค่าน้อยสุดคือ 3 ที่ j = 2 อ่านว่า "รอ 2 รอบให้คนแข็งเดินมาถึง แล้วปล่อยรอบละคน" นับตรง ๆ จากการเดินกระดานได้ 3 ตรงกับการนับตรง ๆ

เดินกระดานคนแข็งให้ดูทีละรอบ จะเห็นช่วงที่เป้า 2 ต้องรอ และช่วงที่ปล่อยได้รอบละคน

กดถัดไปเพื่อดูคนแข็งไหลทีละรอบ

ตัวอย่างประจำตอน · เริ่มที่เป้า 3 · ดูเป้า 2
รอบที่ผ่าน n[1]n[2]n[3]n[4]n[5]n[6] เป้า 2 รอบนี้ F
0 0[0]0212 ไม่ปล่อย 0
1 0[0]1211 ไม่ปล่อย 0
2 0[1]1210 ปล่อย 1 0
3 1[1]1200 ปล่อย 1 1
4 2[1]1100 ปล่อย 1 2
5 2[1]1001 จบ 3

ช่องในวงเล็บเหลี่ยมคือเป้า 2 ส่วนคอลัมน์ F คือจำนวนที่ออกไปแล้วก่อนรอบนั้น

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

ทำไมสูตรยุบเหลือค่าน้อยสุดของช่วง

ช่องที่ถูกรวมเป็นช่วงติดกันเสมอ จึงใช้ผลรวมสะสมได้ ให้ P[y] คือผลรวม n(0) ของเป้า 1 ถึง y − 1 ผลรวมของเป้า i ถึง i + j − 1 ก็คือ P[i + j] − P[i] ลบกันครั้งเดียว

แทนลงในสูตรแล้วย้ายส่วนที่ไม่ขึ้นกับ j ออกมา จะเหลือ F_i(t) = t − P[i] + i + ค่าน้อยสุดของ G[i..i+t] เมื่อ G[y] = P[y] − y

P กับ G ของตัวอย่างประจำตอน (ดัชนี y ตั้งแต่ 2 ถึง 7)
y234567
P[y]000235
G[y] = P[y] − y-2-3-4-3-3-2
ค่าน้อยสุดของแถว G คือ -4 ได้ F = 5 − 0 + 2 + (-4) = 3 ตรงกับตัวเลือก j ข้างบน (ดัชนี 7 เลยเป้าสุดท้ายไปหนึ่งช่อง ใช้เป็นแค่ขอบของผลรวม)
ยุบเหลือค่าน้อยสุดของช่วง
// ให้ P[y] = ผลรวมของ n(0) ตั้งแต่เป้าลำดับที่ 1 ถึง y-1 (คลี่วงออกเป็นเส้นตรง)
// และ G[y] = P[y] - y   สูตรข้างบนจะยุบเหลือ
//     F_i(t) = t - P[i] + i + min( G[i], G[i+1], ..., G[i+t] )
// นั่นคือ "ค่าน้อยสุดของช่วง" บนอาเรย์เดียว
//
// และเพราะเราเดินไปทางซ้ายรอบละไม่เกินหนึ่งช่อง หน้าต่าง [i, i+t] จึงมีแต่ "ขยาย"
// ไม่เคยหด เก็บค่าน้อยสุดสะสมไว้ตัวเดียวก็พอ ไม่ต้องมีโครงสร้างข้อมูลอะไรเลย
ll nwL = pos, nwR = pos + (t + 1);
while (winL > nwL) { winL--; mn = min(mn, G[winL]); }
while (winR < nwR) { winR++; mn = min(mn, G[winR]); }

ช่วงดัชนีที่ต้องหาค่าน้อยสุด ขอเรียกว่าหน้าต่าง (window) คือ [pos, pos + t] ผ่านไปหนึ่งรอบ t เพิ่มหนึ่ง ถ้าเราอยู่ที่เดิม ขอบขวาขยับออกหนึ่ง ถ้าเราก้าวซ้าย pos ลดหนึ่ง ขอบซ้ายขยับออกหนึ่งและขอบขวาอยู่ที่เดิม

ทุกรอบมีขอบเดียวที่ขยาย และไม่เคยหด เก็บค่าน้อยสุดสะสมไว้ตัวเดียวก็พอ (ในโค้ดคือขอบ winL, winR กับค่าน้อยสุด mnCur) หนึ่งรอบใช้งานคงที่ ตรงตามงบของตอนที่ 5

เฉลย ตอนที่ 7 · เป้า 1 กติกากลับด้าน สูตรจึงต้องแบ่งสองเฟส

ที่มา และสามรอบที่ยังผิด

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

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

ห้ารอบของการไล่หาสูตรที่เป้า 1
ทำอะไรจุดที่ผิดจากทั้งหมดผิดกี่เปอร์เซ็นต์
ใช้สูตร min-plus ตรง ๆ ทั้งวง 1,306 11,156 11.7
ตัดค่าติดลบทิ้งด้วย max(0, ...) 774 14,394 5.4
ใช้ค่ามากสุดสะสมตามเวลาแทน 278 14,394 1.9
ทดสอบเฉพาะกรณีที่เป้า 1 มีคนแข็งอยู่แล้ว 0 18,264 0.0
แยกสองเฟส (เฉลยจริง) 0 36,510 0.0
แต่ละรอบสุ่มชุดทดสอบใหม่ จำนวนจุดทั้งหมดจึงไม่เท่ากัน ให้เทียบกันที่คอลัมน์เปอร์เซ็นต์ สองแถวกลางคือการเดาทางแก้ ซึ่งลดสัดส่วนที่ผิดลงได้แต่ไม่เคยถึงศูนย์ แถวที่สี่คือการทดลองเพื่อระบุสาเหตุ และเป็นแถวที่ทำให้เห็นทางออก

สูตรพังที่เป้า 1 เพราะกติกาที่นั่นกลับด้าน

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

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

ผมสุ่มกระดานเล็ก ๆ มาเทียบทุกช่องทุกรอบรวม 18,264 จุด ไม่มีจุดไหนต่างกันเลย แต่มีเงื่อนไขข้อเดียว คือเป้า 1 ต้องมีคนแข็งอยู่แล้ว

ระวัง

ถ้าเป้า 1 ยังว่างจากคนแข็ง n₁ − 1 ติดลบ สูตรจึงนับว่ามีคนออกจากเป้า 1 ไปแล้วทั้งที่ยังไม่มีใครไปถึง

เคสนี้ไม่หายาก ตัวอย่างประจำตอนก็เป็น เป้า 1 ถึง 3 ว่างทั้งหมด

ทางแก้ แบ่งเวลาเป็นสองเฟส

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

เฟสแรกยาวเท่าไรก็รู้ทันที คือ i₀ − 1 รอบ เมื่อ i₀ คือเป้าซ้ายสุดที่มีคนแข็ง เพราะคนแข็งตัวนั้นไม่มีใครขวางข้างหน้า จึงเดินซ้ายได้ทุกรอบ

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

รอยต่อระหว่างสองเฟส

เฟสสองต้องรู้ว่าตอนจบเฟสแรก (รอบที่ 3) แต่ละเป้ามีคนแข็งกี่คน ใช้สูตรการไหลของเฟสแรกหา F ของทุกเป้าที่รอบ 3 ในเฟสแรกเป้า 1 ยังไม่ปล่อยใคร จึงนับว่าเลยเป้า N ไปไม่มีคนแข็ง แล้ว n = n(0) + ไหลเข้า − ไหลออก

F ของเป้า i ที่รอบ 3 คือค่าน้อยสุดของหน้าต่างกว้าง 4 ช่องที่เริ่มที่เป้า i เลื่อนหน้าต่างไปทีละเป้า เก็บด้วย deque (คิวที่หยิบเข้าออกได้ทั้งสองหัว) ซึ่งทั้งแถวใช้เวลาราว N

ตัวอย่างประจำตอน · จากรอบ 0 ถึงรอบ 3 ที่เฟสแรกจบ
เป้า123456
n(0)000212
ออกไปแล้ว F012332
n ที่รอบ 3111200
แถวล่างตรงกับการเดินกระดานทีละรอบ และเป้า 1 มีคนแข็ง 1 คนแล้ว เฟสสองจึงเริ่มได้

เฟสสองเป็นวง เป้า N ต่อกับเป้า 1 วิธีที่โค้ดใช้คือคลี่วงเป็นเส้นตรงยาว โดยวางเป้า 1 ถึง N ซ้ำกันไปเรื่อย ๆ ทุกช่องที่เป็นเป้า 1 ใช้ค่า n ลบหนึ่ง ผลรวมสะสมกับหน้าต่างจึงทำงานบนเส้นนี้เหมือนตอนที่ 6 ทุกอย่าง

เส้นที่คลี่ของตัวอย่างประจำตอน (สองรอบวง)
เป้า123456123456
ค่าที่ใช้011200011200
ช่องเป้า 1 ใช้ 1 − 1 = 0 ทุกครั้งที่วงวนมาถึง

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

เฉลย ตอนที่ 8 · ไม่ต้องลองทุกจุดเริ่มต้น

ตอนที่ 6 และ 7 ทำให้หนึ่งจุดเริ่มใช้งานราว N แต่ยังต้องลองครบ N จุด จำตารางในตอนที่ 4 ได้ไหม เป้าที่ไปจบไม่ได้เรียงทางเดียว จึงค้นหาแบบทวิภาคบนค่านั้นตรง ๆ ไม่ได้ ต้องหาค่าอื่นที่เรียงกันจริง

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

เส้นที่คลี่เมื่อ N = 4 · ตำแหน่ง u ตรงกับเป้าไหน
u-3-2-1012345678
เป้า123412341234
บล็อก-1-1-1-100001111
ทุก 4 ช่องเป้าวนกลับมาซ้ำ ช่องที่เป้าซ้ำรอบเดียวกันคือหนึ่งบล็อก

ตำแหน่งที่คลี่แล้ว

u(k) = k − (จำนวนก้าวซ้ายทั้งหมดเมื่อเริ่มที่เป้า k)

เป้าที่จบ f(k) = ((u(k) − 1) mod N) + 1 และบล็อกคือ ⌊(u − 1) / N⌋ (หารแล้วปัดลง)

ตัวอย่างที่ 1 · u(k) ของทุกจุดเริ่ม
เริ่มที่เป้า kก้าวซ้ายu(k)บล็อกเป้าที่จบ f(k)
1 1 0 -1 4
2 0 2 0 2
3 1 2 0 2
4 0 4 0 4
คอลัมน์ u ไม่ลดเลย ส่วน f ไม่เรียง เพราะ f คือ u ที่ถูกพับกลับเข้าวงทุก N ช่อง ตัวอย่างนี้ u คร่อม 2 บล็อก แถวที่ไฮไลต์คือตัวแรกของแต่ละบล็อก

u(k) ไม่ลดเมื่อ k เพิ่ม ทั้งโค้ดเต็มพึ่งข้อนี้

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

ที่มีคือการตรวจด้วยการจำลองคนจริง บนกระดานสุ่มหลายขนาด ไม่เคยเจอ u ลดลง (จำนวนและขอบเขตของการตรวจอยู่ในหัวข้อโค้ดเต็ม)

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

u อยู่ระหว่าง 1 − T ถึง N และ T ในโค้ดเต็มไม่เกิน 3N − 1 ค่า ⌊(u − 1) / N⌋ อยู่ระหว่าง −3 ถึง 0 คร่อมได้ไม่เกิน 4 บล็อก

ผู้เข้าชิงมีไม่กี่ตัว
// u(k) = k - (จำนวนก้าวซ้ายทั้งหมด)  เป็นฟังก์ชันไม่ลดของ k
// เป้าสุดท้ายคือ  f(k) = ((u(k) - 1) mod N) + 1
// ในบล็อกเดียวกัน (u ที่หารด้วย N แล้วได้ผลหารเท่ากัน) f เพิ่มตาม u
// ผู้เข้าชิงจึงมีแค่ค่า u ที่เล็กที่สุดของแต่ละบล็อก ซึ่งมีไม่กี่ตัว
vector<ll> cand;
cand.push_back(u1);
for (ll v = (ll)N * (fdiv(u1 - 1, N) + 1) + 1; v <= uN; v += N)
    cand.push_back(U(firstGE(v)));          // ค้นหาแบบไบนารีได้เพราะ u ไม่ลด

ในโค้ด firstGE(v) หา k ตัวแรกที่ u(k) ไม่ต่ำกว่า v ส่วน lastLE(v) หา k ตัวสุดท้ายที่ u(k) ไม่เกิน v ทั้งสองเป็นการค้นหาแบบทวิภาคบน k

ลองกับตัวอย่างที่ 1

ผู้เข้าชิงคือ u = 0 และ 2 ถ้าสองบล็อกให้ f เท่ากัน ให้เก็บ u ตัวที่มากกว่า แล้วตอบ k มากที่สุดที่ u(k) ยังไม่เกินค่านั้น ได้ 3 ตรงกับคำตอบของโจทย์

ที่มา และสัญญาณที่ผมอ่านผ่าน

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

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

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

นับจำนวนครั้งที่โค้ดเต็มประเมิน u(k) ได้จากโครงของมัน คือ u(1) กับ u(N) สองครั้ง เส้นแบ่งบล็อกละหนึ่งการค้นหา (18 ครั้ง) บวกหนึ่ง และ lastLE อีกหนึ่งการค้นหา รวมไม่เกิน 2 + 3 × 19 + 18 = 77 ครั้ง

ที่วัดจริงกับอินพุตในตารางเวลาข้างล่าง 4 แบบเรียก 20 ถึง 39 ครั้ง ส่วนแบบที่เราเป็นลำดับ 1 ไม่เรียกเลย ครั้งละราว N งาน ทั้งข้อจึงเป็น O(N log N) (งานโตราว N คูณ log N ซึ่ง log N ของสองแสนคือราว 18)

โค้ดเต็มขอบเขต

เวลาที่วัดได้ · N = 200,000 และ R = 1,000,000,000
ลักษณะอินพุตเวลา (มิลลิวินาที)เทียบกับลิมิต 2 วินาที
อินพุตสุ่ม 252 เหลือเวลา 1,748 มิลลิวินาที
เรียงจากเก่งไปอ่อน (เราคือลำดับ 1) 28 เหลือเวลา 1,972 มิลลิวินาที
เรียงจากอ่อนไปเก่ง 407 เหลือเวลา 1,593 มิลลิวินาที
เราอ่อนที่สุด คนแข็งกองท้ายแถว 409 เหลือเวลา 1,591 มิลลิวินาที
เราลำดับ 2 คนเก่งสุดอยู่ท้ายแถว 468 เหลือเวลา 1,532 มิลลิวินาที
แถวที่เร็วที่สุดคือกรณีที่เราเป็นลำดับ 1 ซึ่งตอบได้ทันทีโดยไม่ต้องคำนวณอะไรเลย ที่เหลือเดินครบทุกขั้น เคสที่ช้าที่สุดใช้ 468 มิลลิวินาที ยังเร็วกว่าลิมิตเกิน 4 เท่า
ดูโค้ดเต็ม
archery_full.cpp
// Archery (IOI 2009) เฉลยเต็มขอบเขต N <= 200,000
//
// แนวคิดสามชั้น
//   1) ย่อโลก: คนอื่นสำคัญแค่ "แข็งกว่าเรา" หรือไม่  นับเป็นโทเคนต่อเป้า n[i] in {0,1,2}
//   2) แทนที่จะจำลองกระดานทุกรอบ ให้คิดเป็น "การไหล" F_i(t) = จำนวนโทเคนที่ออกจากเป้า i ครบ t รอบ
//      ซึ่งมีสูตรปิดแบบ min-plus:  F_i(t) = min over j ของ (t - j) + (ผลรวม n ของเป้า i..i+j-1)
//      สูตรนี้ยุบเหลือการถามค่าน้อยสุดของช่วงบนอาเรย์เดียว ตอบได้ในเวลาคงที่
//   3) เป้า 1 มีกติกากลับด้าน (ผู้ชนะอยู่ต่อ) สูตรจึงใช้ได้ต่อเมื่อเป้า 1 มีโทเคนแล้ว
//      ก่อนหน้านั้นเป็นช่วงตั้งตัวที่ไม่มีใครวนกลับ คิดแยกเป็นเฟสแรก
// สุดท้าย u(k) = k - (จำนวนก้าวซ้าย) เป็นฟังก์ชันไม่ลด จึงค้นหาคำตอบแบบไบนารีได้
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int N;
ll R, T;
int me;
vector<int> b;                 // b[j] = 1 ถ้าคนที่ j ในแถว (ไม่รวมเรา) แข็งกว่าเรา

// ---- บัฟเฟอร์ใช้ซ้ำทุกครั้งที่ประเมิน u(k) ----
vector<int> n0, nt1;
vector<int> P1, G1, P2, G2;
vector<ll> f1at;               // F1(i, t1) ของทุกเป้า

static inline int stationOf(ll y) {          // y เป็นดัชนีที่คลี่วงออกแล้ว
    ll r = y % N; if (r <= 0) r += N; return (int)r;
}

/** คืน u(k) = k - จำนวนก้าวซ้ายทั้งหมด และถ้า outPos ไม่เป็น null ก็คืนเป้าสุดท้ายด้วย */
ll evalU(int k, int* outPos) {
    for (int i = 1; i <= N; i++) {
        if (i < k)       n0[i] = b[2 * i - 2] + b[2 * i - 1];
        else if (i == k) n0[i] = b[2 * k - 2];
        else             n0[i] = b[2 * i - 3] + b[2 * i - 2];
    }
    int i0 = 0;
    for (int i = 1; i <= N; i++) if (n0[i] >= 1) { i0 = i; break; }
    ll t1 = (n0[1] >= 1) ? 0 : i0 - 1;       // เฟสแรกจบตอนโทเคนตัวแรกถึงเป้า 1

    // ---------- เฟส 1 : เป้า 1 ยังไม่ปล่อยใคร เส้นทาง min-plus จึงจบที่เป้า 1 ----------
    ll L1 = 2LL * N + 4;
    P1.assign(L1 + 2, 0);
    for (ll y = 2; y <= L1 + 1; y++) P1[y] = P1[y - 1] + ((y - 1 <= N) ? n0[y - 1] : 0);
    G1.assign(L1 + 2, 0);
    for (ll y = 0; y <= L1 + 1; y++) G1[y] = P1[y] - (int)y;

    ll pos = k, steps = 0;
    // เฟสแรกยาวไม่เกิน N รอบ และเราไม่มีทางวนข้ามเป้า 1 ในเฟสนี้ จึงเดินตรง ๆ ได้
    {
        ll winL = k, winR = k;               // หน้าต่างของ F1(pos, t)
        ll mnCur = G1[k];
        ll sL = k + 1, sR = k + 1;           // หน้าต่างของ F1(pos+1, t)
        ll mnSrc = G1[min(sL, L1)];
        for (ll t = 0; t < min(t1, T); t++) {
            ll fp = (pos == 1) ? 0 : (t - P1[pos] + pos + mnCur);
            ll src = pos + 1;
            ll fs = (stationOf(src) == 1) ? 0 : (t - P1[src] + src + mnSrc);
            bool blocked = (n0[pos] + fs - fp) >= 1;
            bool moved = (pos == 1) ? blocked : !blocked;
            if (moved) { pos--; steps++; }   // ในเฟสแรกเป้า 1 ไม่มีโทเคน เราจึงไม่มีทางถูกเตะ
            // ขยายหน้าต่างทั้งสอง (ขยายอย่างเดียว ไม่เคยหด)
            ll nwL = pos, nwR = pos + (t + 1);
            while (winL > nwL) { winL--; mnCur = min<ll>(mnCur, G1[winL]); }
            while (winR < nwR) { winR++; if (winR <= L1) mnCur = min<ll>(mnCur, G1[winR]); }
            ll nsL = pos + 1, nsR = pos + 1 + (t + 1);
            while (sL > nsL) { sL--; mnSrc = min<ll>(mnSrc, G1[sL]); }
            while (sR < nsR) { sR++; if (sR <= L1) mnSrc = min<ll>(mnSrc, G1[sR]); }
        }
    }

    if (T <= t1) { if (outPos) *outPos = (int)pos; return k - steps; }

    // ---------- สภาพกระดานตอนจบเฟสแรก ----------
    // F1(i, t1) ของทุกเป้า หาได้ด้วยหน้าต่างเลื่อนความกว้างคงที่
    f1at.assign(N + 3, 0);
    {
        deque<ll> dq;
        ll hi0 = min<ll>(1 + t1, L1);
        for (ll x = 1; x <= hi0; x++) { while (!dq.empty() && G1[dq.back()] >= G1[x]) dq.pop_back(); dq.push_back(x); }
        for (ll i = 1; i <= N + 1; i++) {
            ll hi = min(i + t1, L1);
            while (!dq.empty() && dq.front() < i) dq.pop_front();
            while (!dq.empty() && dq.back() > hi) dq.pop_back();
            for (ll x = max(i, hi0 + 1); x <= hi; x++) { while (!dq.empty() && G1[dq.back()] >= G1[x]) dq.pop_back(); dq.push_back(x); }
            hi0 = max(hi0, hi);
            f1at[i] = (stationOf(i) == 1) ? 0 : (t1 - P1[i] + i + G1[dq.front()]);
        }
    }
    nt1.assign(N + 2, 0);
    for (int i = 1; i <= N; i++) {
        int src = (i < N) ? i + 1 : 1;
        ll fs = (src == 1) ? 0 : f1at[src];
        nt1[i] = (int)(n0[i] + fs - f1at[i]);
    }

    // ---------- เฟส 2 : เป้า 1 มีโทเคนแล้ว สูตร min-plus แบบวนใช้ได้ทั้งหมด ----------
    ll OFF = T + 5;                          // เลื่อนดัชนีให้ไม่ติดลบ
    ll L2 = OFF + N + T + 5;
    P2.assign(L2 + 2, 0);
    for (ll y = 1; y <= L2 + 1; y++) {
        int s = stationOf(y - 1 - OFF);
        P2[y] = P2[y - 1] + nt1[s] - (s == 1 ? 1 : 0);
    }
    G2.assign(L2 + 2, 0);
    for (ll y = 0; y <= L2 + 1; y++) G2[y] = P2[y] - (int)y;

    ll y = pos + OFF;                        // ดัชนีที่คลี่แล้วของตำแหน่งเรา
    ll wL = y, wR = y, mnCur = G2[y];
    ll sL = y + 1, sR = y + 1, mnSrc = G2[y + 1];
    for (ll t = 0; t < T - t1; t++) {
        ll fp = t - P2[wL] + wL + mnCur;
        ll fs = t - P2[sL] + sL + mnSrc;
        int st = stationOf(y - OFF);
        bool blocked = ((ll)nt1[st] + fs - fp) >= 1;
        bool moved = (st == 1) ? blocked : !blocked;
        if (moved) { y--; steps++; }
        ll nwL = y, nwR = y + (t + 1);
        while (wL > nwL) { wL--; mnCur = min<ll>(mnCur, G2[wL]); }
        while (wR < nwR) { wR++; mnCur = min<ll>(mnCur, G2[wR]); }
        ll nsL = y + 1, nsR = y + 1 + (t + 1);
        while (sL > nsL) { sL--; mnSrc = min<ll>(mnSrc, G2[sL]); }
        while (sR < nsR) { sR++; mnSrc = min<ll>(mnSrc, G2[sR]); }
    }
    if (outPos) *outPos = stationOf(y - OFF);
    return k - steps;
}

int main() {
    scanf("%d %lld", &N, &R);
    vector<int> s(2 * N);
    for (int i = 0; i < 2 * N; i++) scanf("%d", &s[i]);
    me = s[0];
    if (me == 1) { printf("%d\n", N); return 0; }   // เก่งที่สุด อยู่เป้า 1 แน่นอน จึงเลือกเป้าเริ่มมากสุด

    b.assign(2 * N - 1, 0);
    for (int i = 1; i < 2 * N; i++) b[i - 1] = (s[i] < me) ? 1 : 0;
    T = (R <= 2LL * N) ? R : 2LL * N + ((R - 2LL * N) % N);

    n0.assign(N + 2, 0);
    auto U = [&](int k) { return evalU(k, nullptr); };

    ll u1 = U(1), uN = U(N);
    auto fOf = [&](ll u) { ll r = (u - 1) % N; if (r < 0) r += N; return r + 1; };

    // k ที่ใหญ่ที่สุดที่ u(k) <= v  (u ไม่ลด จึงค้นหาแบบไบนารีได้)
    auto lastLE = [&](ll v) {
        int lo = 1, hi = N, res = 1;
        while (lo <= hi) { int md = (lo + hi) / 2; if (U(md) <= v) { res = md; lo = md + 1; } else hi = md - 1; }
        return res;
    };
    // k ที่เล็กที่สุดที่ u(k) >= v
    auto firstGE = [&](ll v) {
        int lo = 1, hi = N, res = -1;
        while (lo <= hi) { int md = (lo + hi) / 2; if (U(md) >= v) { res = md; hi = md - 1; } else lo = md + 1; }
        return res;
    };

    auto fdiv = [&](ll a, ll m) { ll d = a / m; if ((a % m) && ((a < 0) != (m < 0))) d--; return d; };

    // u ไม่ลด แต่ช่วงของมันกว้างได้เกิน N จึงข้ามเส้นแบ่งบล็อกได้หลายครั้ง
    // ผู้เข้าชิงคือค่า u ที่เล็กที่สุดของแต่ละบล็อก เพราะภายในบล็อกเดียวกัน f เพิ่มตาม u
    vector<ll> cand;
    cand.push_back(u1);
    for (ll v = (ll)N * (fdiv(u1 - 1, N) + 1) + 1; v <= uN; v += N) {
        int kw = firstGE(v);
        if (kw > 0) cand.push_back(U(kw));
    }
    ll bestF = LLONG_MAX, bestU = 0;
    for (ll u : cand) {
        ll f = fOf(u);
        if (f < bestF || (f == bestF && u > bestU)) { bestF = f; bestU = u; }
    }
    int ans = lastLE(bestU);
    printf("%d\n", ans);
    return 0;
}
ดูบักตอนเสมอกัน กับเคสที่เล็กที่สุดที่จับมันได้

ทั้งไฟล์เหมือนโค้ดข้างบนทุกบรรทัด ต่างกันแค่ท่อนสุดท้ายที่เลือกผู้ชนะจากผู้เข้าชิง

archery_wrong.cpp · เฉพาะท่อนเลือกผู้ชนะ
// ผู้เข้าชิงคือค่า u ที่เล็กที่สุดของแต่ละบล็อก สองบล็อกให้เป้าสุดท้ายเท่ากันได้
ll bestF = LLONG_MAX, bestU = 0;
for (ll u : cand) {
    ll f = fOf(u);
    if (f < bestF) { bestF = f; bestU = u; }   // บรรทัดที่ผิด เสมอกันแล้วเก็บตัวที่เจอก่อน
}
int ans = lastLE(bestU);                       // ค่า u ที่เจอก่อนคือค่าน้อยกว่า จุดเริ่มจึงเล็กเกิน

เคสที่เล็กที่สุดที่สองเวอร์ชันตอบไม่ตรงกัน มีแค่ 2 เป้า ผมไล่ทุกการเรียงของ N เท่ากับ 2 และ 3 ทุกค่า R ตั้งแต่ 2N ถึง 4N แล้วเจอชุดนี้เป็นชุดแรก

INPUT
2 4
3
1
4
2

กระดานนี้ยืนเป้าไหนก็จบที่เป้า 1 เหมือนกันทั้งสองจุด โจทย์จึงให้ตอบจุดเริ่มที่มากที่สุด คือ 2 ผู้เข้าชิงสองตัวคือ u = −3 จากจุดเริ่ม 1 กับ u = −1 จากจุดเริ่ม 2

ทั้งคู่ให้ f = 1 เท่ากัน เวอร์ชันที่ผิดเก็บ u = −3 ไว้เพราะเจอก่อน แล้ว lastLE(−3) ก็คืนจุดเริ่ม 1 ออกมา

ทางแก้คือให้เงื่อนไขรับกรณีเสมอด้วย f == bestF && u > bestU ซึ่งเก็บ u ที่มากกว่าไว้

ตัวอย่างในโจทย์ทั้งสองชุดตอบ 3 กับ 2 เท่ากันทั้งสองเวอร์ชัน ผมจึงไม่มีทางรู้จากตรงนั้นเลยว่าพลาด

การตรวจทั้งหมดของหน้านี้ รวมไว้ที่เดียว
ตรวจอะไรขอบเขตผล
กระดาน c (ตอนที่ 1) เทียบกับจำลองคนจริงตัวอย่างในโจทย์สองชุด ทุกจุดเริ่ม (ตอนสร้างหน้า)ตรงทุกจุด
สูตรการไหล (ตอนที่ 6) เทียบกับนับตรง ๆ18,593 กรณี บนกระดานสุ่ม N ไม่เกิน 7 (ตอนสร้างหน้า)ตรงทุกกรณี
สูตรเป้า 1 และสองเฟส (ตอนที่ 7)36,510 จุด ช่วงพัฒนาตรงทุกจุด
u(k) ไม่ลด, เดิน T รอบได้เท่าเดิน R รอบ, วิธีผู้เข้าชิง (ตอนที่ 8)307 กระดาน N ไม่เกิน 7 (ตอนสร้างหน้า) และอีก 960 กระดาน N 2 ถึง 25 ด้วยสคริปต์แยกไม่เจอข้อยกเว้น
ช่วงตั้งตัวกับคนที่วน (ตอนที่ 3)7,600 กระดาน N 1 ถึง 40 ด้วยสคริปต์แยกซ้ำทุก N รอบตั้งแต่ไม่เกินรอบ N, คนวนคือลำดับ 2 ถึง N+1
โค้ดทั้งสามชุดที่ดึงออกจากหน้านี้2,000 เทส N ไม่เกิน 8 เทียบตัวจำลองคนจริง และ 500 เทส N 9 ถึง 60 (R ถึงพันล้าน) เทียบโค้ดรอบแรกผิด 0 เทส
แถวที่เขียนว่าตอนสร้างหน้า ถ้าไม่ตรงแม้ครั้งเดียว หน้านี้จะ build ไม่ผ่าน ทั้งหมดนี้คือการทดลอง ไม่ใช่บทพิสูจน์ สองบักที่จับได้ระหว่างทาง (เป้า 1 ติดลบ และการเลือกบล็อกตอนเสมอ) ตอบตัวอย่างในโจทย์ถูกทั้งสองชุด
แผนที่โค้ดเต็ม · ท่อนไหนเล่าไว้ตอนไหน
ท่อนในโค้ดเล่าไว้ที่
T = 2N + ((R - 2N) % N)ตอนที่ 3 (ไม่ต้องเดินครบ R รอบ)
if (me == 1) ... Nตอนที่ 3 (ลำดับ 1 จอดที่เป้า 1 ทุกจุดเริ่มจึงจบเท่ากัน)
b[] และ n0[] ใน evalUตอนที่ 1 และ 6 (นับคนแข็ง ไม่นับตัวเรา)
blocked = n0[pos] + fs - fp >= 1ตอนที่ 6 (สิ่งเดียวที่ต้องรู้ต่อรอบ)
P, G, winL/winR, mnCurตอนที่ 6 (ค่าน้อยสุดของหน้าต่างที่มีแต่ขยาย)
i0, t1 และ เฟส 1ตอนที่ 7 (ช่วงที่เป้า 1 ยังว่าง)
deque ตอนจบเฟสแรกตอนที่ 7 (สภาพกระดานตอนเปลี่ยนเฟส ใช้ deque คือคิวที่หยิบเข้าออกได้สองหัว)
เฟส 2: nt1[s] - (s == 1 ? 1 : 0)ตอนที่ 7 (ใช้ n ของเป้า 1 ลบหนึ่ง)
cand, firstGE, lastLEตอนที่ 8 (ผู้เข้าชิงบล็อกละตัว)
f == bestF && u > bestUตอนที่ 8 และกล่องบักตอนเสมอกันข้างบน
ในโค้ดเรียกคนแข็งว่าโทเคน (token แปลว่าเบี้ย ใช้เรียกของที่นับเป็นชิ้น ๆ) ส่วน n0 คือ n(0) ของตอนที่ 6

ทดสอบความเข้าใจ

ถ้าเราเก่งที่สุดในสนาม (หมายเลข 1) ควรตอบอะไร

ตอบ N ทุกครั้ง เราชนะทุกรอบจึงไหลไปจอดที่เป้า 1 ได้จากทุกจุดเริ่ม เพราะ T อย่างน้อย 2N รอบ ทุกจุดจบที่เป้า 1 เท่ากัน โจทย์จึงขอจุดเริ่มที่มากที่สุด โค้ดเต็มตอบบรรทัดเดียวด้วยเหตุผลนี้

เรายืนที่เป้า 5 และ c[5] = 1 รอบนี้จะเกิดอะไรขึ้น

ป้าย 1 ตัวเดียวในเป้านั้นคือตัวเราเอง คู่แข่งจึงเป็นป้าย 0 เราชนะ และขยับไปเป้า 4 (ตอนที่ 2)

ทำไมค้นหาแบบทวิภาคบน u(k) ได้ แต่บนเป้าที่จบ f(k) ไม่ได้

u ไม่ลดเมื่อ k เพิ่ม ส่วน f คือ u ที่ถูกพับกลับเข้าวงทุก N ช่อง พอ u ข้ามเส้นแบ่งบล็อก f ก็กระโดดกลับไปเลขน้อย จึงไม่เรียงทางเดียว (ตอนที่ 8)

สรุป

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

สุดท้ายตำแหน่งที่คลี่ออก u(k) ไม่ลดในทุกกระดานที่ตรวจ จุดเริ่มทั้ง N จุดจึงเหลือให้ประเมินแค่ไม่กี่สิบครั้ง

แหล่งที่มา

  1. โจทย์ Archery บน programming.in.th ข้อ 2032 programming.in.th/tasks/2032 (สืบค้น 6 กันยายน 2026)
  2. ต้นทางของโจทย์คือ 21st International Olympiad in Informatics วันแข่งที่หนึ่ง จัดที่เมืองพลอฟดิฟ ประเทศบัลแกเรีย 8 ถึง 15 สิงหาคม 2009