programming.in.th · ข้อ 2032
รื้อเฉลยจากลูกบาศก์ลงมาเหลือ N log N ทีละชั้น เริ่มจากตัดข้อมูลที่ไม่มีผลทิ้ง แล้วเปลี่ยนการจำลองเป็นสูตรปิด และปิดท้ายด้วยเหตุผลว่าทำไมไม่ต้องลองจุดเริ่มต้นครบทุกจุด
สนามยิงธนูมีเป้าเรียงเป็นเส้นตรง N เป้า หมายเลข 1 ถึง N จากซ้ายไปขวา
มีนักยิง 2N คน ยืนประจำเป้าละสองคน แล้วแข่งกันเป็นรอบ ๆ ทุกรอบทุกเป้าจะได้ผู้ชนะหนึ่งคนและผู้แพ้หนึ่งคน
จากนั้นทุกคนย้ายที่พร้อมกันตามกติกาสามข้อ
N ขยับไปเป้าที่อยู่ทางซ้ายมือหนึ่งเป้าN และผู้ชนะที่เป้า 1 ยืนอยู่ที่เดิมN
นักยิงทุกคนมีหมายเลขทักษะ 1 ถึง 2N ไม่ซ้ำกัน โดยเลขน้อยกว่าคือเก่งกว่า และคนเก่งกว่าชนะเสมอ
ไม่มีเซอร์ไพรส์ ไม่มีดวง การแข่งจะดำเนินไป R รอบ
คุณมาถึงสนามเป็นคนสุดท้าย อีก 2N - 1 คนยืนเรียงแถวรอไว้เรียบร้อยแล้ว สิ่งที่คุณทำได้อย่างเดียวคือ
แทรกตัวเองเข้าไปในแถว ณ จุดใดจุดหนึ่ง แล้วจากนั้นสองคนซ้ายสุดของแถวไปเข้าเป้า 1 สองคนถัดไปเข้าเป้า 2
ไล่ไปจนครบ เป้าหมายของคุณคือจบการแข่งขันที่เป้าหมายเลขน้อยที่สุดเท่าที่จะเป็นไปได้
และถ้ามีหลายทางที่ทำได้เท่ากัน ให้เลือกทางที่เริ่มต้นที่เป้าหมายเลขมากที่สุด
อินพุต / ขอบเขต / เอาต์พุต
N และ R จากนั้นอีก 2N บรรทัด
บรรทัดแรกของชุดนี้คือหมายเลขทักษะของตัวเรา ที่เหลืออีก 2N - 1 บรรทัด
คือหมายเลขทักษะของคนอื่นเรียงตามที่เขายืนอยู่ในแถวจากซ้ายไปขวา
1 ≤ N ≤ 200,000 และ 2N ≤ R ≤ 1,000,000,000
หมายเลขทักษะเป็น 1 ถึง 2N ไม่ซ้ำกัน เวลา 2 วินาที หน่วยความจำ 64 เมกะไบต์
N ≤ 5,000 และในนั้นมี 20 คะแนน
สำหรับเทสที่ N ≤ 200 | Input | Output |
|---|---|
| 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 คือเดาตรงกลางแล้วตัดทิ้งครึ่งหนึ่งทุกครั้ง) ใช้ตรง ๆ ไม่ได้
ที่มาของแนวคิดนี้
สิ่งแรกที่ผมทำคือเขียนตัวจำลองคนจริงครบ 2N คน แล้วยอมรับว่ามันช้าเกินไป จากนั้นถามคำถามเดียวคือ หมายเลขทักษะของคนอื่นมีผลอะไรกับชะตากรรมของเราบ้าง นอกจากมันมากหรือน้อยกว่าของเรา พอตอบว่าไม่มีเลย ข้อมูลที่ต้องเก็บก็หายไปเกือบหมดในบรรทัดเดียว
คำถามแบบนี้ใช้ได้กับโจทย์อีกหลายข้อ ลองไล่ดูว่าอินพุตส่วนไหนที่ถ้าเปลี่ยนแล้วคำตอบไม่เปลี่ยน ส่วนนั้นคือส่วนที่ทิ้งได้
คำตอบของใบ้สั้นมาก เราแพ้ทุกคนที่หมายเลขน้อยกว่าเรา และชนะทุกคนที่หมายเลขมากกว่าเรา ตลอดกาล ทุกรอบ
ทุกเป้า ดังนั้นสำหรับเรา คนอื่นบนสนามมีแค่สองประเภท ติดป้าย 1 ให้คนที่เราไม่ชนะ
และ 0 ให้คนที่เราชนะ
แล้วป้ายพวกนี้ขยับยังไงเมื่อเวลาผ่านไป ตอบได้จากกติกาโดยไม่ต้องรู้หมายเลขจริงของใครเลย เพราะผู้ชนะคือคนที่เก่งกว่า และคนที่เก่งกว่าย่อมมีป้ายไม่ต่ำกว่าคู่ของตัวเอง ไล่ทั้งสามกรณีได้แบบนี้
1 เจอป้าย 1 คนที่เก่งกว่าในสองคนชนะแล้วออกไป อีกคนอยู่ต่อ ทั้งคู่เป็น 11 เจอป้าย 0 ผู้ชนะเป็น 1 ผู้แพ้เป็น 00 เจอป้าย 0 ผู้ชนะเป็น 0 ผู้แพ้เป็น 0สรุปเป็นกฎเดียวว่า ทุกเป้าส่งป้ายที่สูงกว่าออกไป และเก็บป้ายที่ต่ำกว่าไว้
ตอนนี้ไม่ต้องเก็บว่าใครยืนตรงไหนอีกแล้ว เก็บแค่ตัวเลขเดียวต่อเป้า คือเป้านั้นมีคนป้าย 1 กี่คน
ซึ่งเป็น 0, 1 หรือ 2 เรียกมันว่า c[i]
แปลงกฎ "ส่งตัวสูงออก เก็บตัวต่ำไว้" ให้เป็นสมการของ c ได้ตรง ๆ โดยจำไว้ว่าเป้า 1
กับเป้า N ผิดฝาผิดตัวกว่าเพื่อน เพราะที่เป้า 1 ผู้ชนะอยู่ต่อและผู้แพ้ถูกเหวี่ยงไปเป้า N
ทุกบรรทัดข้างล่างตอบคำถามเดียวกัน คือเป้านี้จะมีป้าย 1 กี่คนในรอบถัดไป คำตอบมาจากสองส่วนเสมอ คือคนที่เป้านี้เก็บไว้เอง บวกคนที่รับมาจากเป้าทางขวา
วงเล็บเหลี่ยมอย่าง [c[1] ≥ 1] อ่านว่า "ถ้าเงื่อนไขในวงเล็บจริงนับหนึ่ง ไม่จริงนับศูนย์"
และ c' คือค่าในรอบถัดไป
สามบรรทัดต่างกันแค่ว่าใครถูกเก็บ ใครถูกส่งต่อ
N - 1) เก็บผู้แพ้ของตัวเอง ซึ่งเป็นป้าย 1
ก็ต่อเมื่อเดิมมีป้าย 1 สองคน (ถ้ามีคนเดียว คนนั้นชนะแล้วเดินออกไป)
แล้วรับผู้ชนะจากเป้าทางขวา ซึ่งเป็นป้าย 1 เมื่อเป้านั้นมีป้าย 1 อย่างน้อยหนึ่งคน
1
เมื่อมีป้าย 1 แค่หนึ่งคนก็พอ เป้า 1 จึงเป็นตะแกรงที่ดูดคนเก่งไปจอด (และเป็นเหตุให้ตอนที่ 7 ต้องแบ่งสองเฟส)
N) ไม่มีเป้าทางขวาให้รับ มันรับผู้แพ้ของเป้า 1 ที่ถูกเหวี่ยงมาแทน
จึงนับเมื่อเป้า 1 มีป้าย 1 ครบสองคน
สามบรรทัดนี้ทำให้กระดานเดินต่อได้โดยไม่ต้องรู้ว่าใครเป็นใคร รู้แค่จำนวนป้าย 1 ต่อเป้าก็พอ
ตอนที่แล้วเราติดป้ายให้คนอื่น ทีนี้ตัวเราเองล่ะ เราชนะทุกคนที่ป้าย 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 ไกลสุด แล้วต้องไต่กลับมาใหม่
ค้างไว้ตั้งแต่ก่อนลองเอง ว่าทำไมโจทย์ต้องรับประกันว่ารอบมากพอ คำตอบคือระบบนี้เดินไปสักพักแล้วจะ เข้าวงรอบ คือหน้าตาสนามกลับมาซ้ำเดิมเป็นจังหวะ
ที่มาของแนวคิดนี้
หลักฐานแรกมาจากการพิมพ์กระดานออกมาดูเฉย ๆ หลังผ่านไป 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 ค้างนิ่งอยู่เป้าละคน
| ตัวอย่าง | คนที่วนไม่หยุด | คนที่ค้างอยู่กับที่ | ตรงกับ 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 | ตรง |
เหตุผลอ่านออกได้จากกติกา ที่เป้า 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 ตามที่อธิบายข้างบน
ลองเอากระดาน c กติกาแพ้ชนะของเรา และจำนวนรอบ T มาเดินบนตัวอย่างจริง
เอาตัวอย่างที่สองมาเดิน หมายเลขทักษะของเราคือ 2 จากทั้งหมด 8 คน จึงมีป้าย
1 อยู่แค่ 2 คน (นับตัวเราเองด้วย) และเราเลือกเริ่มที่เป้า 2
กดถัดไปเพื่อเดินกระดานทีละรอบ
| รอบ | c[1] | c[2] | c[3] | c[4] | เรายืนที่ | คู่แข่ง | ผล |
|---|---|---|---|---|---|---|---|
| 1 | 1 | [1] | 0 | 0 | เป้า 2 | ป้าย 0 เราชนะ | ชนะ ขยับไปเป้า 1 |
| 2 | [2] | 0 | 0 | 0 | เป้า 1 | ป้าย 1 เก่งกว่าเรา | แพ้ ถูกส่งไปเป้า 4 |
| 3 | 1 | 0 | 0 | [1] | เป้า 4 | ป้าย 0 เราชนะ | ชนะ ขยับไปเป้า 3 |
| 4 | 1 | 0 | [1] | 0 | เป้า 3 | ป้าย 0 เราชนะ | ชนะ ขยับไปเป้า 2 |
| 5 | 1 | [1] | 0 | 0 | เป้า 2 | ป้าย 0 เราชนะ | ชนะ ขยับไปเป้า 1 |
| 6 | [2] | 0 | 0 | 0 | เป้า 1 | ป้าย 1 เก่งกว่าเรา | แพ้ ถูกส่งไปเป้า 4 |
| 7 | 1 | 0 | 0 | [1] | เป้า 4 | ป้าย 0 เราชนะ | ชนะ ขยับไปเป้า 3 |
| 8 | 1 | 0 | [1] | 0 | เป้า 3 | ป้าย 0 เราชนะ | ชนะ ขยับไปเป้า 2 |
| 9 | 1 | [1] | 0 | 0 | เป้า 2 | ป้าย 0 เราชนะ | ชนะ ขยับไปเป้า 1 |
ช่องที่ใส่วงเล็บเหลี่ยมคือเป้าที่เรายืน ไล่คอลัมน์ c ลงมาจะเห็นเลข 1 ไหลไปทางซ้ายรอบละช่อง
รวมแล้วเราเจอป้าย 0 (ชนะ) 7 รอบ เจอป้าย 1 (แพ้) 2 รอบ และจบที่เป้า 1
ทำแบบเดียวกันกับทุกจุดที่แทรกได้ แล้วเทียบกัน
| เริ่มที่เป้า | 1 | 2 | 3 | 4 | ตอบ |
|---|---|---|---|---|---|
| ตัวอย่างที่ 1 จบที่เป้า | 4 | 2 | 2 | 4 | 3 |
| ตัวอย่างที่ 2 จบที่เป้า | 4 | 1 | 2 | 3 | 2 |
ตัวอย่างแรกจบที่เป้า 2 ได้จากสองจุดเริ่มต้น กติกาข้อรองจึงเข้ามาตัดสิน ให้เลือกจุดเริ่มที่มากกว่า
ได้คำตอบ 3 ส่วนตัวอย่างที่สองมีจุดเดียวที่จบถึงเป้า 1
แถวบนของตารางนี้เตือนอะไรบางอย่าง ค่าที่ได้คือ 4 2 2 4 ซึ่งไม่ได้เรียงทางเดียว
เริ่มขวากว่าไม่ได้แปลว่าจบขวากว่า
จึงข้ามการลองบางจุดด้วยการค้นหาแบบทวิภาคตรง ๆ ไม่ได้ ตอนที่ 8 จะกลับมาแก้เรื่องนี้
โค้ดเดินตามสี่ตอนที่เล่ามา สร้างกระดาน c ของจุดเริ่มต้นหนึ่งจุด เดินไม่เกิน 4N รอบ
แต่ละรอบขยับตำแหน่งเราด้วยเลขตัวเดียว แล้วอัปเดตกระดานทั้งแถว
งานต่อหนึ่งจุดเริ่มต้นคือ 4N รอบ คูณ N เป้า ทั้งข้อจึงเป็น O(N³)
(อ่านว่า "โอ เอ็นกำลังสาม" แปลว่างานโตตาม N คูณกันสามครั้ง) ค่าคงที่ต่ำเพราะวงในบวกแค่เลข 0 กับ 1
| N | เวลา (มิลลิวินาที) | เทียบกับลิมิต 2 วินาที |
|---|---|---|
| 200 | 38 | เหลือเวลา 1,962 มิลลิวินาที |
| 500 | 501 | เหลือเวลา 1,499 มิลลิวินาที |
| 800 | 2,053 | เกินลิมิต 53 มิลลิวินาที |
| 1,200 | 6,388 | เกินลิมิต 4,388 มิลลิวินาที |
N ≤ 200 จบใน 38 มิลลิวินาที ที่ N = 500 ยังทัน
แต่ที่ N = 800 เกินลิมิตแล้ว ห่างจากชุด 60 คะแนน (N ≤ 5,000) อีกไกล
โค้ดรอบแรกได้ 20 คะแนน ก่อนคิดท่าใหม่ ลองวัดก่อนว่าที่ขอบเขตเต็มมันห่างจากงบแค่ไหน และห่างเพราะงานส่วนไหน ตัวเลขจะบอกเองว่าต้องตัดตรงไหน
วิธีที่ 1
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) ไม่มีวิธีกลางของมันเองในหน้านี้ วิธีเต็มกินไปพร้อมกันเลย
โค้ดรอบแรกที่ N = 200,000 ใช้ราว 308 วัน คำถามคือควรตัดอะไรก่อน
ระหว่างจำนวนจุดเริ่ม กับงานต่อหนึ่งจุด
ต่อให้ลองจุดเริ่มแค่จุดเดียว ก็ยังใช้ราว 2.2 นาที เกินงบ 67 เท่า ลดจำนวนจุดเริ่มอย่างเดียวไม่มีทางพอ ต้องลดงานต่อหนึ่งรอบด้วย
คิดกลับจากงบ หนึ่งรอบใช้ได้เท่าไร
แปลว่าในหนึ่งรอบ เราแตะได้แค่ไม่กี่ช่อง ห้ามไล่กระดานทั้งแถว
แผนมีสองชั้น ตอนที่ 6 และ 7 ทำให้หนึ่งรอบเหลืองานคงที่ แล้วตอนที่ 8 ลดจุดเริ่มจาก N จุดเหลือไม่กี่สิบ
สองทางที่ผมลองก่อน แล้วทิ้ง
ทางแรกคือบีบกระดานเป็นบิต เพราะการเดินกระดานหนึ่งรอบเขียนเป็น
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 บอกว่าชะตาของเราแต่ละรอบขึ้นกับเลขช่องเดียว คือช่องที่เรายืน แต่โค้ดรอบแรกเดินกระดานทั้งแถวเพื่อรู้เลขช่องนั้น ตอนนี้จะหาเลขช่องเดียวนั้นโดยไม่เดินกระดาน
กระดานที่จะใช้ทั้งสองตอน
N = 6 หมายเลขทักษะเรียงตามอินพุตคือ 6 12 7 8 10 11 3 2 5 9 4 1 ตัวแรกคือเรา หมายเลข 6 n คือจำนวนคนแข็งต่อเป้า ต่างจาก c ของตอนที่ 1 ตรงที่ไม่นับตัวเรา
คนแข็งชนะเราทุกคน การเคลื่อนที่ของพวกเขาจึงไม่ขึ้นกับเรา และ c ของช่องที่เรายืนคือ n บวกหนึ่ง
| เป้า | สองคนในเป้า | คนแข็ง (หมายเลขน้อยกว่า 6) |
|---|---|---|
| 1 | 12, 7 | 0 |
| 2 | 8, 10 | 0 |
| 3 | เรา, 11 | 0 |
| 4 | 3, 2 | 2 |
| 5 | 5, 9 | 1 |
| 6 | 4, 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 รอบมีเพดานได้หลายแบบ ซึ่งคิดได้โดยไม่ต้องเดินทีละรอบ
สูตรการไหล
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
ค่าน้อยสุดคือ 3 ที่ j = 2 อ่านว่า "รอ 2 รอบให้คนแข็งเดินมาถึง แล้วปล่อยรอบละคน" นับตรง ๆ จากการเดินกระดานได้ 3 ตรงกับการนับตรง ๆ
เดินกระดานคนแข็งให้ดูทีละรอบ จะเห็นช่วงที่เป้า 2 ต้องรอ และช่วงที่ปล่อยได้รอบละคน
กดถัดไปเพื่อดูคนแข็งไหลทีละรอบ
| รอบที่ผ่าน | n[1] | n[2] | n[3] | n[4] | n[5] | n[6] | เป้า 2 รอบนี้ | F |
|---|---|---|---|---|---|---|---|---|
| 0 | 0 | [0] | 0 | 2 | 1 | 2 | ไม่ปล่อย | 0 |
| 1 | 0 | [0] | 1 | 2 | 1 | 1 | ไม่ปล่อย | 0 |
| 2 | 0 | [1] | 1 | 2 | 1 | 0 | ปล่อย 1 | 0 |
| 3 | 1 | [1] | 1 | 2 | 0 | 0 | ปล่อย 1 | 1 |
| 4 | 2 | [1] | 1 | 1 | 0 | 0 | ปล่อย 1 | 2 |
| 5 | 2 | [1] | 1 | 0 | 0 | 1 | จบ | 3 |
ช่องในวงเล็บเหลี่ยมคือเป้า 2 ส่วนคอลัมน์ F คือจำนวนที่ออกไปแล้วก่อนรอบนั้น
ช่องที่ถูกรวมเป็นช่วงติดกันเสมอ จึงใช้ผลรวมสะสมได้ ให้ 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
| y | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|
| P[y] | 0 | 0 | 0 | 2 | 3 | 5 |
| G[y] = P[y] − y | -2 | -3 | -4 | -3 | -3 | -2 |
// ให้ 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
ที่มา และสามรอบที่ยังผิด
สูตรผิดตั้งแต่ครั้งแรกที่เอาไปเทียบกับการเดินกระดานจริงทุกช่องทุกรอบ ผมนับว่าผิดกี่จุด รอบแรกผิดพันกว่าจุด และทุกจุดที่ผิดมีค่าติดลบ นั่นคือเบาะแสว่ามีคนออกจากเป้า 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 จึงทำตัวเหมือนเป้าธรรมดาที่มีคนแข็ง n₁ − 1 คน
แทนค่านี้ลงในสูตรแทน n ของเป้า 1
ผมสุ่มกระดานเล็ก ๆ มาเทียบทุกช่องทุกรอบรวม 18,264 จุด ไม่มีจุดไหนต่างกันเลย แต่มีเงื่อนไขข้อเดียว คือเป้า 1 ต้องมีคนแข็งอยู่แล้ว
ระวัง
ถ้าเป้า 1 ยังว่างจากคนแข็ง n₁ − 1 ติดลบ สูตรจึงนับว่ามีคนออกจากเป้า 1 ไปแล้วทั้งที่ยังไม่มีใครไปถึง
เคสนี้ไม่หายาก ตัวอย่างประจำตอนก็เป็น เป้า 1 ถึง 3 ว่างทั้งหมด
เฟสแรกคือช่วงที่ยังไม่มีคนแข็งไปถึงเป้า 1 ช่วงนั้นไม่มีใครถูกเหวี่ยงกลับไปท้ายแถวเลย ระบบเป็นเส้นตรงล้วน สูตรเดิมใช้ได้โดยให้เส้นทางไปจบที่เป้า 1
เฟสแรกยาวเท่าไรก็รู้ทันที คือ i₀ − 1 รอบ เมื่อ i₀ คือเป้าซ้ายสุดที่มีคนแข็ง
เพราะคนแข็งตัวนั้นไม่มีใครขวางข้างหน้า จึงเดินซ้ายได้ทุกรอบ
เฟสสองต้องรู้ว่าตอนจบเฟสแรก (รอบที่ 3) แต่ละเป้ามีคนแข็งกี่คน ใช้สูตรการไหลของเฟสแรกหา F ของทุกเป้าที่รอบ 3 ในเฟสแรกเป้า 1 ยังไม่ปล่อยใคร จึงนับว่าเลยเป้า N ไปไม่มีคนแข็ง แล้ว n = n(0) + ไหลเข้า − ไหลออก
F ของเป้า i ที่รอบ 3 คือค่าน้อยสุดของหน้าต่างกว้าง 4 ช่องที่เริ่มที่เป้า i เลื่อนหน้าต่างไปทีละเป้า เก็บด้วย deque (คิวที่หยิบเข้าออกได้ทั้งสองหัว) ซึ่งทั้งแถวใช้เวลาราว N
| เป้า | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| n(0) | 0 | 0 | 0 | 2 | 1 | 2 |
| ออกไปแล้ว F | 0 | 1 | 2 | 3 | 3 | 2 |
| n ที่รอบ 3 | 1 | 1 | 1 | 2 | 0 | 0 |
เฟสสองเป็นวง เป้า N ต่อกับเป้า 1 วิธีที่โค้ดใช้คือคลี่วงเป็นเส้นตรงยาว โดยวางเป้า 1 ถึง N ซ้ำกันไปเรื่อย ๆ ทุกช่องที่เป็นเป้า 1 ใช้ค่า n ลบหนึ่ง ผลรวมสะสมกับหน้าต่างจึงทำงานบนเส้นนี้เหมือนตอนที่ 6 ทุกอย่าง
| เป้า | 1 | 2 | 3 | 4 | 5 | 6 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ค่าที่ใช้ | 0 | 1 | 1 | 2 | 0 | 0 | 0 | 1 | 1 | 2 | 0 | 0 |
ตำแหน่งของเราบนเส้นนี้ลดหนึ่งทุกครั้งที่ก้าวซ้าย รวมถึงตอนแพ้ที่เป้า 1 ซึ่งคือการถอยไปช่องเป้า N ที่อยู่ทางซ้ายของมันบนเส้น
หน้าต่างยังขยายทีละขอบเหมือนเดิม ส่วนโค้ดบวกค่าเลื่อน OFF ไว้ไม่ให้ดัชนีติดลบ
ตอนที่ 6 และ 7 ทำให้หนึ่งจุดเริ่มใช้งานราว N แต่ยังต้องลองครบ N จุด จำตารางในตอนที่ 4 ได้ไหม เป้าที่ไปจบไม่ได้เรียงทางเดียว จึงค้นหาแบบทวิภาคบนค่านั้นตรง ๆ ไม่ได้ ต้องหาค่าอื่นที่เรียงกันจริง
ค่านั้นคือตำแหน่งบนวงที่คลี่ออกเป็นเส้นตรง ทุกก้าวของเราเป็นก้าวซ้าย รวมถึงตอนแพ้ที่เป้า 1 ซึ่งนับเป็นก้าวจาก 1 ไป 0 และช่อง 0 บนเส้นก็คือเป้า N ของวงก่อนหน้า
| u | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| เป้า | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 |
| บล็อก | -1 | -1 | -1 | -1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
ตำแหน่งที่คลี่แล้ว
u(k) = k − (จำนวนก้าวซ้ายทั้งหมดเมื่อเริ่มที่เป้า k)
เป้าที่จบ f(k) = ((u(k) − 1) mod N) + 1 และบล็อกคือ ⌊(u − 1) / N⌋ (หารแล้วปัดลง)
| เริ่มที่เป้า 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(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)
| ลักษณะอินพุต | เวลา (มิลลิวินาที) | เทียบกับลิมิต 2 วินาที |
|---|---|---|
| อินพุตสุ่ม | 252 | เหลือเวลา 1,748 มิลลิวินาที |
| เรียงจากเก่งไปอ่อน (เราคือลำดับ 1) | 28 | เหลือเวลา 1,972 มิลลิวินาที |
| เรียงจากอ่อนไปเก่ง | 407 | เหลือเวลา 1,593 มิลลิวินาที |
| เราอ่อนที่สุด คนแข็งกองท้ายแถว | 409 | เหลือเวลา 1,591 มิลลิวินาที |
| เราลำดับ 2 คนเก่งสุดอยู่ท้ายแถว | 468 | เหลือเวลา 1,532 มิลลิวินาที |
| ตรวจอะไร | ขอบเขต | ผล |
|---|---|---|
| กระดาน 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 เทส |
| ท่อนในโค้ด | เล่าไว้ที่ |
|---|---|
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 และกล่องบักตอนเสมอกันข้างบน |
หมายเลขทักษะของคนอื่นสำคัญแค่ว่ามากหรือน้อยกว่าของเรา สนามจึงยุบเหลือเลข 0 ถึง 2 ต่อเป้า คูณเลขแล้วรู้ว่าต้องตัดงานต่อรอบก่อน เลยเลิกเดินกระดานแล้วนับการไหล ซึ่งยุบเหลือค่าน้อยสุดของช่วง
สุดท้ายตำแหน่งที่คลี่ออก u(k) ไม่ลดในทุกกระดานที่ตรวจ จุดเริ่มทั้ง N จุดจึงเหลือให้ประเมินแค่ไม่กี่สิบครั้ง
ในหน้านี้