programming.in.th · ข้อ 2037
ส่งจดหมายสองหมื่นฉบับตามลำดับ แค่ยืนบนถนนเส้นเดียวกับร้านก็ส่งได้ บทนี้ยุบตำแหน่งบนตารางสี่ล้านจุดเหลือสองเส้น แล้วแสดงว่าบนแต่ละเส้นมีจุดที่ควรไปยืนแค่จุดเดียว
เมืองของเทพเจ้ากรีกวางถนนเป็นตาราง ทุกจำนวนเต็ม Z มีถนนแนวนอนที่ y = Z
และถนนแนวตั้งที่ x = Z คู่พิกัดจำนวนเต็มแต่ละคู่จึงเป็นสี่แยกหนึ่งแห่ง
วันที่อากาศร้อน เทพแต่ละองค์ไปนั่งร้านกาแฟตามสี่แยกต่าง ๆ
เทพแอร์เมส (เทพผู้ส่งสาร) ต้องเดินไปตามถนนเพื่อส่งจดหมายลับให้ครบ N ฉบับ
ฉบับละหนึ่งองค์ และต้องส่งตามลำดับที่กำหนดเท่านั้น เขาเริ่มที่ (0, 0)
กติกาที่ทำให้ข้อนี้น่าสนใจคือ จะส่งจดหมายให้เทพที่ร้าน (X, Y)
แอร์เมสไม่ต้องเดินไปถึงร้าน แค่ยืนอยู่บนถนนเส้นเดียวกันก็พอ
คือยืนที่ (X, Z) หรือ (Z, Y) สำหรับจำนวนเต็ม Z ใดก็ได้
ใครจะเห็นจดหมายระหว่างทางก็ไม่เป็นไร ส่งครบแล้วแอร์เมสก็หายตัวไป
ถามว่าระยะทางเดินรวมที่น้อยที่สุดคือเท่าไร
อินพุต / ขอบเขต / เอาต์พุต
N จากนั้นอีก N บรรทัด
บรรทัดละสองจำนวน คือพิกัดร้านของเทพที่จะได้รับฉบับนั้น เรียงตามลำดับการส่ง
1 < N < 20,000, -1000 < X, Y < 1000
และในข้อมูลทดสอบ 50% มี N < 80 เวลา 3 วินาที หน่วยความจำ 16 เมกะไบต์
| Input | Output |
|---|---|
| 5 8 3 7 -7 8 1 -2 1 6 -5 | 11 |
ระยะทางนับตามถนน เดินได้แค่แนวนอนกับแนวตั้ง จาก (0, 0) ไป (3, 2) จึงเป็น 5 ช่วงถนน
ตัวอย่างนี้ฉบับแรกอยู่ที่ร้าน (8, 3) แอร์เมสอาจยืนที่ (0, 3)
หรือ (8, 0) ก็ส่งได้แล้ว คำตอบ 11 คือระยะรวมของทั้งห้าฉบับ
ใบ้
ตำแหน่งของแอร์เมสบนตารางมีสองพิกัด แต่ละพิกัดมีได้ราวสองพันค่า ถ้าจำตำแหน่งเต็ม ๆ หลังส่งทุกฉบับ ตารางจะใหญ่เกินหน่วยความจำ 16 เมกะไบต์ไปไกล
ลองดูว่าหลังส่งฉบับที่ i เสร็จ เรารู้อะไรเกี่ยวกับตำแหน่งของเขาแน่ ๆ บ้าง
แล้วจากจุดนั้น จะไปยืนบนเส้นของฉบับถัดไปตรงไหนดี
สามฉบับแรก ลองคลิกจุดที่อยากไปยืน แล้วดูว่าต้องไปถึงร้านจริง ๆ ไหม
คลิกจุดบนตารางเพื่อเดินไปยืนตรงนั้น
ตัวเลขบนช่องคือร้านของฉบับนั้น แถวกับคอลัมน์ที่เรืองสีคือถนนสองเส้นของฉบับถัดไป แอร์เมสเดินจากจุดเดิมไปจุดที่คลิกด้วยระยะตามถนน และส่งจดหมายเฉพาะตรงจุดที่หยุดยืนเท่านั้น
ลองทั้งสี่ชุดก่อนนะครับ โดยเฉพาะชุดสี่ฉบับที่การรีบเข้าหาเส้นที่ใกล้ที่สุดทำให้เสียเปรียบ พอมีคำตอบในใจแล้ว ค่อยเปิดเฉลย
ที่มาของแนวคิดนี้
ผมเริ่มจากสถานะที่ตรงที่สุดก่อน คือ "ส่งไปแล้วกี่ฉบับ และยืนอยู่ที่ (x, y) ไหน"
ตัวเลขฆ่ามันทันที พิกัดแต่ละแกนมี 2,001 ค่า สองแกนรวมกันราวสี่ล้านจุด
คูณจำนวนฉบับอีกสองหมื่นเป็นแปดหมื่นล้านสถานะ ส่วนหน่วยความจำ 16 เมกะไบต์เก็บ int ได้แค่ราวสี่ล้านตัว
แม้แต่ชั้นเดียวของตารางนี้ก็เกือบเต็มแล้ว
แรงบีบนั้นบังคับให้ถามว่าในสี่ล้านจุด มีจุดไหนบ้างที่เป็นไปได้จริงหลังส่งฉบับที่ i
คำตอบอยู่ในกติกาเอง ตอนที่ส่งฉบับที่ i แอร์เมสต้องยืนบนเส้นใดเส้นหนึ่งของร้านนั้น
พิกัดหนึ่งแกนจึงถูกตรึงไว้กับร้านแล้ว เหลือพิกัดอีกแกนแกนเดียวที่ยังเป็นอิสระ
สี่ล้านจุดยุบเหลือสองเส้น เส้นละ 2,001 จุด
บทเรียนที่ยกไปข้ออื่นได้คือ เวลาสถานะใหญ่เกินไป ให้ไปอ่านกติกาว่ามันบังคับอะไรไว้ตอนจบแต่ละขั้น ของที่ถูกบังคับไม่ต้องจำ
เรียกถนนสองเส้นที่ผ่านร้านของฉบับที่ i ว่าเส้นแนวนอน y = Yi
กับเส้นแนวตั้ง x = Xi หลังส่งฉบับที่ i
แอร์เมสยืนอยู่บนเส้นใดเส้นหนึ่งในสองเส้นนี้เสมอ สถานะจึงเขียนได้ด้วยของสองอย่าง
row[i][c] คือระยะน้อยที่สุด ถ้าหลังส่งฉบับที่ i เขายืนบนเส้นแนวนอน ที่ x = ccol[i][c] คือระยะน้อยที่สุด ถ้าหลังส่งฉบับที่ i เขายืนบนเส้นแนวตั้ง ที่ y = c
จุดเริ่ม (0, 0) นับเป็นฉบับที่ 0 ที่ร้านอยู่ตรงนั้นพอดี
แอร์เมสยืนบนทั้งสองเส้นของมันพร้อมกัน จึงเริ่มด้วย row[0][0] = col[0][0] = 0
ถ้าร้านของฉบับแรกบังเอิญอยู่แนวเดียวกับ (0, 0) ก็ส่งได้เลยโดยไม่ต้องเดิน
ซึ่งสูตรข้างล่างจะจัดการให้เอง
คำถามที่ยังค้างคือ จากจุดบนเส้นของฉบับที่ i-1 จะไปยืนตรงไหนบนเส้นของฉบับที่ i
มีสองเส้นให้เลือก และบนแต่ละเส้นมีสองพันกว่าจุด ตอนที่ 2 ลองทุกจุดก่อน ตอนที่ 3 จะตัดเหลือจุดเดียวต่อเส้น
ทางที่ตรงไปตรงมาที่สุด แล้วตีราคา
เมื่อสถานะเหลือ 2 × 2,001 ช่องต่อฉบับแล้ว ทางที่ไม่ต้องคิดอะไรเพิ่มคือ จากทุกช่อง ลองไปยืนทุกจุดบนสองเส้นของฉบับถัดไป ระยะก็คือระยะตามถนนจากตำแหน่งจริงของช่องนั้นไปจุดนั้น ผมเขียนมันเป็นโค้ดจริงไว้ใช้เป็นตัวเทียบ และมันคือทางที่ผ่านชุดย่อย 50% ได้
ราคาต่อฉบับคือ 2 × 2,001 ช่องต้นทาง คูณ 2 × 2,001 จุดปลายทาง ราวสิบหกล้านครั้ง
วัดจริงที่ N เท่ากับ 79 ใช้ 0.37 วินาที ที่ 800 ใช้ 3.8 วินาที เวลาโตตาม N เป็นเส้นตรง
พอไปถึงสองหมื่นฉบับจะเกินลิมิต 3 วินาทีไปหลายสิบเท่า
สิ่งที่ต้องหาต่อจึงชัดมาก คือในสองพันกว่าจุดบนเส้นใหม่ จุดไหนที่ควรค่าแก่การไปยืนจริง ๆ
โค้ดของท่านี้อยู่ในหัวข้อโค้ดข้างล่าง มันไม่มีข้ออ้างอะไรเลยนอกจากตอนที่ 1 จึงเป็นตัวเทียบที่ดีของท่าเต็ม ถ้าสองท่าให้คำตอบตรงกันบนเคสสุ่มจำนวนมาก ข้ออ้างของตอนที่ 3 ก็ไม่ได้ทำให้พลาดจุดไหนไป
ที่มาของแนวคิดนี้
คำถามจากตอนที่ 2 คือจุดไหนบนเส้นใหม่ที่ควรค่าแก่การไป ผมลองวาดดูหนึ่งกรณี
แอร์เมสยืนที่ P อยากไปยืนที่จุด Q บนเส้นแนวตั้งของฉบับถัดไป
ระยะตามถนนจาก P ไป Q แยกได้เป็นสองก้อนเสมอ คือก้อนซ้ายขวากับก้อนขึ้นลง
ก้อนซ้ายขวาต้องจ่ายแน่ ๆ เพื่อไปถึงเส้นนั้น ส่วนก้อนขึ้นลงเป็นแค่การขยับไปตามเส้น
ก้อนขึ้นลงนั้นเลื่อนไปจ่ายทีหลังได้ทั้งก้อน โดยราคาไม่เปลี่ยน
ถ้าไปยืนที่จุดบนเส้นที่ตรงกับ P พอดีก่อน แล้วค่อยเดินต่อตามเส้นตอนที่ต้องใช้จริง
ระยะรวมเท่ากับไปที่ Q ตรง ๆ ทุกประการ ที่ต่างคือตอนนี้เลือกได้ว่าจะเดินต่อหรือไม่
จุดอื่นบนเส้นจึงไม่เคยดีกว่าจุดที่ใกล้ที่สุด ตัวตรวจที่เดินทีละก้าวยืนยันข้ออ้างนี้ในทุกเคสที่สุ่ม
บทเรียนที่ยกไปข้ออื่นได้คือ เมื่อต้นทุนแยกเป็นก้อนที่ต้องจ่ายแน่กับก้อนที่เลื่อนไปจ่ายทีหลังได้ ให้จ่ายเฉพาะก้อนแรกไปก่อน ทางเลือกที่เหลือยังอยู่ครบ
จากตำแหน่งปัจจุบันจึงมีทางไปแค่สองทาง และทั้งสองทางเดินเป็นเส้นตรงเส้นเดียวเสมอ
ถ้าอยู่บนเส้นแนวนอนของฉบับก่อน ก็เดินขึ้นลงไปชนเส้นแนวนอนใหม่ (พิกัด x ไม่เปลี่ยน)
หรือเดินซ้ายขวาไปชนเส้นแนวตั้งใหม่ (พิกัด y ยังเป็น Y ของฉบับก่อน)
ฝั่งเส้นแนวตั้งก็สลับบทบาทกัน เขียนเป็นสูตรฝั่งแนวนอนได้แบบนี้
อ่านสูตรนี้โดยจำไว้ว่า c ในช่องปลายทางคือพิกัด x ที่แอร์เมสยืนหลังส่งฉบับที่ i
ส่วนดัชนีของช่องต้นทางคือพิกัดอีกแกนที่เขายืนหลังส่งฉบับก่อนหน้า ทางเข้าช่องปลายทางมีสองทาง
x = c
เดินขึ้นหรือลงตรง ๆ จนชนเส้นแนวนอนใหม่ พิกัด x ไม่ขยับ จึงลงช่อง c เดิมพอดี
ระยะที่บวกคือระยะห่างระหว่างสองเส้นแนวนอน ซึ่งเท่ากันทุกช่อง ทางนี้ใช้ได้ทุกช่องที่ฉบับก่อนมีค่าอยู่
x = Xi-1 ตายตัว
ที่ความสูง c' สักค่า เดินขึ้นหรือลงจนชนเส้นแนวนอนใหม่ ระยะ |c' - Yi|
และยืนที่ x = Xi-1 ช่องปลายทางของทางนี้จึงเป็นช่องเดียว คือ c = Xi-1
และเลือก c' ที่ดีที่สุดจากทุกช่องของฝั่งแนวตั้ง
ผลที่ตามมาคือแต่ละฉบับเพิ่มช่องใหม่ได้แค่หนึ่งช่องต่อฝั่ง หลังฉบับที่ i
แต่ละฝั่งจึงมีช่องที่มีค่าไม่เกิน i + 1 ช่อง (ตัวอย่างในโจทย์มีช่องรวมทั้งสองฝั่ง 2, 2, 4, 6, 7, 8 ตามลำดับ)
ส่วนสูตรฝั่ง col ได้จากการสลับ x กับ y ทั้งหมด
| ช่องปลายทาง | มาจาก | ระยะที่บวก | เงื่อนไข |
|---|---|---|---|
row[i][c] | row[i-1][c] | |Y[i-1] - Y[i]| | ทุก c |
row[i][c] | col[i-1][c'] ทุก c' | |c' - Y[i]| | เฉพาะ c = X[i-1] |
col[i][c] | col[i-1][c] | |X[i-1] - X[i]| | ทุก c |
col[i][c] | row[i-1][c'] ทุก c' | |c' - X[i]| | เฉพาะ c = Y[i-1] |
ตัวอย่างในโจทย์ ช่องที่มีค่าของแต่ละฝั่ง เขียนเป็น พิกัด · ระยะ
| ฉบับ | ร้าน | ฝั่งแนวนอน (x · ระยะ) | ฝั่งแนวตั้ง (y · ระยะ) | ดีสุด |
|---|---|---|---|---|
| 0 | (0, 0) | x=0 · 0 | y=0 · 0 | 0 |
| 1 | (8, 3) | x=0 · 3 | y=0 · 8 | 3 |
| 2 | (7, -7) | x=0 · 13, x=8 · 15 | y=0 · 9, y=3 · 10 | 9 |
| 3 | (8, 1) | x=0 · 21, x=7 · 10, x=8 · 23 | y=-7 · 15, y=0 · 10, y=3 · 11 | 10 |
| 4 | (-2, 1) | x=0 · 21, x=7 · 10, x=8 · 11 | y=-7 · 25, y=0 · 20, y=1 · 19, y=3 · 21 | 10 |
| 5 | (6, -5) | x=-2 · 25, x=0 · 27, x=7 · 16, x=8 · 17 | y=-7 · 33, y=0 · 28, y=1 · 11, y=3 · 29 | 11 |
เวลาที่วัดได้บนเครื่องผม ท่าเต็มกับจดหมาย 19,999 ฉบับที่พิกัดสุ่มเต็มช่วงใช้ 0.05 วินาที รวมเวลาอ่านอินพุตแล้ว จากลิมิต 3 วินาที
ข้อนี้ใช้สองจังหวะ จังหวะแรกคืออ่านกติกาว่ามันตรึงอะไรไว้ตอนจบแต่ละขั้น การส่งจดหมายบังคับให้ยืนบนเส้นของร้าน พิกัดหนึ่งแกนจึงไม่ต้องจำ สถานะสองมิติเหลือมิติเดียว จังหวะที่สองคือแยกต้นทุนของการเดินเป็นก้อนที่ต้องจ่ายแน่กับก้อนที่เลื่อนไปจ่ายทีหลังได้ แล้วจ่ายแค่ก้อนแรก ทางเลือกนับพันต่อสถานะจึงเหลือสองทาง
| ทาง | ขอบเขตที่มันไหว | งานคร่าว ๆ ที่ N = 20,000 |
|---|---|---|
| เดินทีละก้าวบนตารางทั้งแผ่น (ตัวตรวจ) | พิกัดแคบ ๆ | N × 2,001² ช่อง คือราว 80,080,020,000 ช่อง |
| สองเส้น ลองทุกจุดบนเส้นใหม่ (ชุดย่อย) | N ต่ำกว่า 80 | N × 2 × 2,001 × 2 × 2,001 คือราว 320,320,080,000 ครั้ง |
| สองเส้น เดินไปจุดที่ใกล้ที่สุด (ท่าเต็ม) | N ถึง 20,000 | N × 2 × 2,001 × 2 คือราว 160,080,000 ครั้ง |
หลังส่งแต่ละฉบับ แอร์เมสยืนบนเส้นใดเส้นหนึ่งของร้าน จำแค่ฝั่งกับพิกัดอีกแกน แล้วจากทุกช่องเดินตรงไปจุดที่ใกล้ที่สุดของเส้นใหม่แต่ละเส้น ได้งานฉบับละราวแปดพันครั้ง
ในหน้านี้