programming.in.th · ข้อ 2023
เม่นเดินทีละช่องได้ถึงหนึ่งล้านล้านช่อง แต่คำตอบไม่ต้องเดินตามเลยสักก้าว บทนี้แปลคำว่าสีเทาเป็นประโยคเรื่องบิต แล้วยุบการนับทั้งกระดานเหลือการเดิน 21 บิตพร้อมธงสามอันว่ายังติดขอบไหน
ลูก้าเจอกระดานเกมแปลก ๆ ในห้องใต้หลังคา ขนาด R แถว C คอลัมน์
แถวนับ 0 ถึง R ลบหนึ่งจากบนลงล่าง คอลัมน์นับ 0 ถึง C ลบหนึ่งจากซ้ายไปขวา
ที่แปลกคือวิธีระบายสี ช่องหนึ่ง ๆ จะเป็นสีขาว ถ้าเลขแถวกับเลขคอลัมน์
เขียนเป็นเลขฐานสองแล้วมีเลข 1 ตรงตำแหน่งเดียวกันอย่างน้อยหนึ่งตำแหน่ง
เช่นช่อง (4, 5) เพราะ 4 คือ 100 และ 5 คือ 101 ซึ่งชนกันที่ตำแหน่งซ้ายสุด
นอกนั้นเป็นสีเทา เช่นช่อง (2, 5) เพราะ 2 คือ 010 ส่วน 5 คือ 101 ไม่ชนกันเลยสักตำแหน่ง
เม่นของลูก้าเริ่มเดินที่ช่อง (0, 0) แล้วเดินซิกแซกไปตามแนวทแยง ตามภาพข้างล่าง
ลูก้านั่งนับว่าเม่นเหยียบช่องสีเทาไปกี่ช่อง พอเหยียบครบ K ช่องเม่นก็หลับ
งานของเราคือบอกจำนวนช่องสีเทาที่เม่นเหยียบไป
อินพุต / ขอบเขต / เอาต์พุต
R กับ C บรรทัดที่สองคือ K1 ≤ R, C ≤ 1,000,000 และ 1 ≤ K ≤ R × C
โจทย์เตือนเองว่า K อาจไม่พอใส่จำนวนเต็ม 32 บิต เวลา 1 วินาที หน่วยความจำ 32 เมกะไบต์
K น้อยกว่าหนึ่งล้าน| Input | Output |
|---|---|
| 10 10 6 | 5 |
| 3 5 11 | 8 |
| 10 10 100 | 51 |
ใบ้
ชุดทดสอบครึ่งหนึ่งให้ K ไม่เกินหนึ่งล้าน ซึ่งเดินจริงทีละช่องได้สบาย
แต่ชุดเต็มให้ K ได้ถึง หนึ่งล้านล้าน เดินไม่ไหวแน่นอน
คำถามจึงกลายเป็นว่านับโดยไม่เดินได้ไหม
ลองอ่านประโยคว่า "ไม่มีเลข 1 ตรงตำแหน่งเดียวกัน" ใหม่ ถ้าบิตของสองตัวนี้ไม่เคยชนกันเลย
แล้ว r + c กับ r | c ต่างกันตรงไหน
เดินครบทั้งกระดาน ถ้ายังจับกฎของสีไม่ได้ ให้เริ่มจากแถว 0 ก่อน มันง่ายที่สุด
ตัวเลขในช่องคือลำดับก้าวของเม่น ช่องที่เป็นจุดคือช่องที่เม่นหลับก่อนจะไปถึง
เลือกไว้ 0 ช่อง
เขียนเลขแถวกับเลขคอลัมน์เป็นฐานสองในใจ ถ้าไม่มีตำแหน่งไหนเป็น 1 พร้อมกัน ช่องนั้นคือสีเทา
ถ้าทายพลาดบ่อยที่ช่องแถวสูง ๆ ลองสังเกตว่าช่อง (r, c) กับช่อง (c, r) ให้ผลเหมือนกันเสมอ เพราะเงื่อนไขมันสมมาตร กระดานทั้งกระดานจึงพับทับกันได้ตามแนวทแยง
ที่มาของแนวคิดนี้
ข้อนี้ไม่มีบั๊กให้เล่า เพราะทางที่ผมเดินตั้งแต่แรกมันถูกอยู่แล้ว สิ่งที่มีคือแรงกดดันที่บังคับให้เดินทางนั้น
ผมเริ่มจากเขียนตัวเดินจริงก่อน เพราะมันเขียนเสร็จในสองนาทีและตอบตัวอย่างในโจทย์ถูกทั้งสามชุด
แล้วค่อยเอา K เท่าที่โจทย์ให้ได้จริงไปแทน คือ 1,000,000 คูณ 1,000,000
ซึ่งเป็นหนึ่งล้านล้านก้าว ตัวเดินตัวนั้นจะใช้เวลาเป็นชั่วโมง
จุดที่ทำให้เห็นทาง คือผมกลับไปอ่านนิยามของสีอีกรอบแล้วถามว่ามันพูดถึงอะไรกันแน่
มันไม่ได้พูดถึงตำแหน่งบนกระดาน มันพูดถึงบิตของเลขสองตัว และประโยคว่าบิตไม่ชนกัน
คือประโยคเดียวกับ r + c = r | c ซึ่งแปลว่าเลขแนวทแยงกับสีของช่อง
เป็นเรื่องเดียวกันมาตลอด ผมแค่ยังไม่ได้เขียนมันด้วยคำเดียวกัน
บทเรียนที่ยกไปข้ออื่นได้คือ เวลาโจทย์นิยามอะไรด้วยเลขฐานสอง ให้ลองเขียนนิยามนั้นใหม่ เป็นความสัมพันธ์ระหว่างการบวกกับการหรือ ดูสักครั้ง มันเปลี่ยนโจทย์เรขาคณิตเป็นโจทย์นับได้บ่อยกว่าที่คิด
นิยามของโจทย์บอกว่าช่อง (r, c) เป็นสีเทาก็ต่อเมื่อ r กับ c
ไม่มีบิต 1 ตรงตำแหน่งเดียวกันเลย ซึ่งเขียนสั้น ๆ ได้ว่า r & c = 0
// ช่องสีเทาคือช่องที่บิตของแถวกับของคอลัมน์ไม่ชนกัน
// พอบิตไม่ชนกัน r + c จึงเท่ากับ r | c พอดี
// แปลว่าบนแนวทแยง r + c = d ช่องสีเทาคือคู่ที่ r เป็นสับเซตของบิต d เท่านั้น
bool grey(int r, int c){ return (r & c) == 0; }
พอบิตไม่ชนกัน การบวกก็ไม่มีตัวทด เลยสักตำแหน่ง ผลลัพธ์ของ r + c
จึงเท่ากับ r | c พอดี แปลว่าบนแนวทแยงเส้นที่ r + c = d
ช่องสีเทาคือคู่ที่ บิตของ r เป็นสับเซตของบิตของ d
และ c คือบิตที่เหลือของ d พอดี
แนวทแยงเส้นหนึ่งไม่ได้มีช่องสีเทากระจัดกระจาย มันมีเท่ากับจำนวนวิธีแบ่งบิตของเลขประจำเส้นออกเป็นสองกอง
เม่นเดินตามแนวทแยงทีละเส้น เส้นที่ d มีช่องอยู่ตายตัว
ดังนั้นถ้าเดินไป K ช่อง เม่นจะเดินจบไปเต็ม ๆ หลายเส้น
แล้วหลับกลางเส้นสุดท้ายเส้นเดียว
r + c < D ทั้งหมดD
ประเด็นที่ทำให้ข้อนี้ง่ายลงมาก คือลำดับซิกแซกมีผลแค่ในท่อนที่สอง
ท่อนแรกเป็นเซตของช่อง ไม่ใช่ลำดับของช่อง เม่นจะเดินขึ้นหรือลงก็ได้เซตเดียวกัน
ทิศทางของแนวทแยงจึงเหลือหน้าที่เดียว คือบอกว่าช่วงที่ค้างอยู่บนเส้นสุดท้าย
เริ่มจาก r น้อยไปมาก หรือจากมากไปน้อย
โน้ต · ทิศของแต่ละแนวทแยง
จากภาพกระดานข้างบน เส้นที่ d เป็นเลขคี่เดินลงล่าง คือ r เพิ่มขึ้น
ส่วนเส้นที่ d เป็นเลขคู่เดินขึ้นบน คือ r ลดลง
ตรวจได้จากสามก้าวแรกคือ (0,0) แล้ว (0,1) แล้ว (1,0)
ท่อนแรกคือการนับคู่ (r, c) ที่ผ่านเงื่อนไขสามข้อพร้อมกัน คือ r < R,
c < C และ r | c < D โดยที่บิตของสองตัวห้ามชนกัน
ท่าที่ใช้คือสร้างเลขทั้งคู่ทีละบิตจากบิตสูงลงมาบิตต่ำ แต่ละตำแหน่งเลือกได้แค่สามแบบ
คือ (0,0), (0,1) และ (1,0) ส่วน (1,1) ถูกห้ามตั้งแต่ต้น
เพราะจะทำให้บิตชนกัน
สิ่งที่ต้องจำระหว่างทางมีแค่สามอย่าง คือตอนนี้ r ยังเท่ากับ R
ในบิตที่ผ่านมาทั้งหมดหรือยัง c เท่ากับ C หรือยัง และผลรวมเท่ากับ D หรือยัง
ถ้าตัวไหนเล็กกว่าขอบไปแล้ว ตัวนั้นจะอิสระตลอดที่เหลือ เพราะบิตที่ต่ำลงไปเปลี่ยนผลไม่ได้อีก
สถานะทั้งหมดจึงมีแค่ 2 คูณ 2 คูณ 2 เท่ากับ 8 สถานะ
เลขที่ใหญ่ถึงล้านล้าน ไม่ได้แปลว่าต้องนับทีละตัว มันแปลว่าให้เปลี่ยนหน่วยการนับจากตัวเลขเป็นบิต ซึ่งมีแค่ยี่สิบเอ็ดตัว
เดินบิตของ R = 10, C = 10, D = 7
| สถานะ | เริ่ม | บิต 3 | บิต 2 | บิต 1 | บิต 0 | จบ |
|---|---|---|---|---|---|---|
| ติด R · ติด C · ติด D | · | · | · | · | · | · |
| ติด R · ติด C · หลุด D | · | · | · | · | · | · |
| ติด R · หลุด C · ติด D | · | · | · | · | · | · |
| ติด R · หลุด C · หลุด D | · | · | · | · | · | · |
| หลุด R · ติด C · ติด D | · | · | · | · | · | · |
| หลุด R · ติด C · หลุด D | · | · | · | · | · | · |
| หลุด R · หลุด C · ติด D | · | · | · | · | · | · |
| หลุด R · หลุด C · หลุด D | · | · | · | · | · | · |
แถวล่างสุดคือแถวที่หลุดขอบครบทั้งสามตัว ซึ่งเป็นแถวเดียวที่นับเป็นคำตอบ
ที่ R = 10, C = 10 และ D = 7 ได้ 19 ช่อง
ตัวเลขทุกช่องในตารางนี้คำนวณตอนสร้างหน้า ไม่ได้พิมพ์มือ
ส่วนท่อนที่สองบนเส้นสุดท้าย เราต้องนับ r ที่บิตเป็นสับเซตของ D
และอยู่ในช่วง [a, b] ซึ่งทำได้ด้วยการนับ r ที่ไม่เกิน b
แล้วลบด้วยที่ไม่เกิน a ลบหนึ่ง การนับสับเซตที่ไม่เกินค่าหนึ่ง
ก็เดินทีละบิตเหมือนกัน ต่างแค่คราวนี้มีขอบตัวเดียว
ทั้งโปรแกรมไม่มีลูปไหนยาวเกิน R + C คือใช้หาว่าเม่นหลับที่แนวทแยงเส้นไหน
ส่วนการนับใช้ 21 บิตคูณ 8 สถานะ ซึ่งคงที่ ไม่โตตามอินพุตเลย
เวลาที่วัดได้บนเครื่องผม กระดาน 1,000,000 คูณ 1,000,000 และ K เท่ากับหนึ่งล้านล้าน
ใช้เวลาไม่ถึง 30 มิลลิวินาที จากลิมิตหนึ่งวินาที คำตอบของเคสนั้นคือ 3,441,847,383 ช่อง
ซึ่งเป็นจำนวนช่องสีเทาทั้งกระดาน
ท่าหลักของข้อนี้เรียกว่าการนับทีละหลัก (digit DP) คือแทนที่จะไล่ค่าทีละค่า เราสร้างค่านั้นทีละหลักแล้วจำแค่ว่า "ตอนนี้ยังติดขอบบนอยู่ไหม" ท่านี้ใช้ได้ทุกครั้งที่โจทย์ถามว่า มีเลขกี่ตัวในช่วงหนึ่งที่หน้าตาของหลักเป็นแบบนี้ ไม่ว่าจะฐานสองหรือฐานสิบ
ทบทวนพื้นฐาน · ทำไมต้องมีธงว่าติดขอบ
ถ้าไม่มีขอบบน การนับเลข n บิตที่บิตไม่ชนกันคือ 3 ยกกำลัง n ตรง ๆ
เพราะแต่ละบิตเลือกได้สามแบบอิสระกัน ความยากทั้งหมดมาจากคำว่าน้อยกว่า ที่ผูกอยู่กับขอบ
ธงติดขอบคือวิธีจัดการคำว่าน้อยกว่าให้เป็นเรื่องเฉพาะที่ ตราบใดที่บิตที่ผ่านมาเท่ากับขอบทุกตัว บิตถัดไปยังห้ามเกินขอบ แต่วินาทีที่มีบิตไหนน้อยกว่าขอบ ทุกบิตที่เหลือก็อิสระทันที เพราะต่อให้เติม 1 ทั้งหมดก็ยังไม่มีวันไล่ทันขอบได้
ถ้าอยากเห็นท่าเดียวกันในบริบทที่ต่างออกไป ลองอ่าน นับก่อน แล้วค่อยเดิน ซึ่งใช้การนับเพื่อเลี่ยงการสร้างของจริงเหมือนกัน
| ทาง | ขอบเขตที่มันไหว | งานคร่าว ๆ ต่อหนึ่งเคส |
|---|---|---|
| เดินทีละช่องตามที่โจทย์เล่า | K ไม่เกินหนึ่งล้าน | 1,000,000 ก้าว |
| นับด้วยการเดินบิต | K ถึงหนึ่งล้านล้าน | 21 บิต คูณ 8 สถานะ |
ประโยคว่าบิตไม่ชนกัน คือประโยคเดียวกับว่าบวกแล้วไม่มีตัวทด พอเขียนแบบนั้นได้ ช่องสีเทาบนแนวทแยงก็กลายเป็นสับเซตของเลขประจำเส้น และการนับทั้งกระดาน ก็ยุบเหลือการเดิน 21 บิตพร้อมธงสามอันว่ายังติดขอบไหน
ในหน้านี้