programming.in.th · ข้อ 2041

นักชิม: หนึ่งวันคือการคูณเมทริกซ์หนึ่งครั้ง

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

★★★★★ dpbinary liftingstate space อ่าน 16 นาที เผยแพร่ 11 กันยายน 2026 อัปเดต 20 ตุลาคม 2026

โจทย์ · นักชิมที่ต้องกลับถึงบ้านให้พอดีวัน

อาณาจักรเอลฟ์มี N เมือง เมืองที่ i ทำอาหารที่ให้ความพึงพอใจ C[i] หน่วย ถนนในอาณาจักรเป็นทางเดียวทั้งหมด M เส้น ถนนเส้นหนึ่งพาจากเมือง u ไปเมือง v และใช้เวลา w วัน ออกจาก u วันที่ d ก็ไปถึง v วันที่ d + w

นักชิม W เริ่มที่เมือง 1 ในวันที่ 0 และต้องกลับมารายงานตัวที่เมือง 1 ในวันที่ T พอดี ห้ามหยุดพักระหว่างทาง พอถึงเมืองไหนเขาชิมทันทีแล้วออกเดินทางต่อในวันนั้นเลย กลับมาเมืองเดิมกี่รอบก็ได้ความพึงพอใจเท่าเดิมทุกรอบ นอกจากนี้ยังมีเทศกาลอาหาร k งาน งานที่ i จัดวันที่ t ที่เมือง x ถ้าเขาอยู่ที่เมืองนั้นพอดีวันนั้นก็ได้เพิ่มอีก y หน่วย เราต้องบอกว่าความพึงพอใจรวมที่มากที่สุดเป็นเท่าไร

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

EXAMPLE
InputOutput
3 4 11 0
1 3 4
1 2 1
2 1 3
2 3 2
3 1 4
13
4 8 16 3
3 1 2 4
1 2 1
1 3 1
1 3 2
3 4 3
2 3 2
3 2 1
4 2 1
4 1 5
3 3 5
1 2 5
5 4 20
39
1 วัน 3 วัน 2 วัน 4 วัน 1 1 2 3 3 4 วันที่ อยู่เมือง ได้เพิ่ม รวม 0 เมือง 1 +1 1 1 เมือง 2 +3 4 4 เมือง 1 +1 5 5 เมือง 2 +3 8 7 เมือง 3 +4 12 11 เมือง 1 +1 13
อาณาจักรของตัวอย่างแรก ตัวเลขขาวในวงคือหมายเลขเมือง ตัวเลขทองคือความพึงพอใจของเมืองนั้น ลูกศรคือถนนทางเดียวพร้อมจำนวนวันที่ใช้ ทางขวาคือบัญชีของแผน 1 → 2 → 1 → 2 → 3 → 1 ทีละครั้งที่เหยียบเมือง จนได้ 13 หน่วยในวันที่ 11

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

คำตอบมีหน่วยเป็นความพึงพอใจรวม ไม่ใช่จำนวนวันหรือจำนวนเมืองที่แวะ และเป็น -1 เมื่อกลับถึงเมือง 1 ในวันที่ T พอดีไม่ได้

นับทั้งวันที่ 0 และวันที่ T เพราะเขาชิมตั้งแต่วันแรกที่ยืนอยู่เมือง 1 และชิมอีกครั้งตอนกลับถึง แผนที่โจทย์ยกมาคือ 1 → 2 → 1 → 2 → 3 → 1 (บัญชีทางขวาของรูปข้างบน) รวม 1 + 3 + 1 + 3 + 4 + 1 = 13

ชุดที่สองมีเทศกาล 3 งาน อินพุตให้มาเป็นวันที่ 3, 1, 5 ซึ่งไม่ได้เรียงตามวัน แผนที่ชนะคือ 1 → 3 → 4 → 2 → 3 → 4 → 1 ไปตรงงานเดียวคือ วันที่ 5 ที่เมือง 4 ได้เพิ่ม 20 อีก 2 งานไม่คุ้มที่จะแวะ รวมได้ 39

ใบ้

ถ้า T เล็ก ข้อนี้คือตารางธรรมดา ช่องหนึ่งช่องคือ “วันที่ d ยืนอยู่เมือง v แล้วได้มากที่สุดเท่าไร” ไล่จากวันที่ 0 ไปจนถึงวันที่ T แล้วอ่านคำตอบที่ช่องของเมือง 1

การจดคำตอบย่อยลงตารางแล้วหยิบมาใช้ซ้ำแบบนี้เรียกว่า DP (dynamic programming อ่านว่า “ไดนามิก โปรแกรมมิง”) ปัญหาคือ T ใหญ่ได้ถึง 1,000,000,000 ตารางนั้นมีแถวเป็นพันล้านแถว

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

ลองเอง · พาเขาเที่ยวเองสี่อาณาจักร

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

 

1 วัน 3 วัน 2 วัน 4 วัน 1 1 2 3 3 4

ชุดนี้ไม่มีเทศกาล

 

กดถนนที่อยากไป วงกลมสีทองคือเมือง 1 ซึ่งเป็นทั้งจุดเริ่มและจุดจบ

 

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

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

เฉลย ตอนที่ 1 · ช้าแต่ถูก: ไล่ทุกเส้นทาง แล้วจดเป็นตารางรายวัน

ชุด “เมืองอร่อยที่สุด” ในเกมบอกไว้แล้วว่าเลือกถนนที่คุ้มที่สุดทีละก้าวไม่ได้ คนที่เด้งเก็บเมือง 3 จนเพลินจะกลับบ้านไม่ทันวันที่ 9 ต้องมองทั้งเส้นทางพร้อมกัน

ไล่จากช้าไปเร็วมีสามขั้น ตอนนี้เล่าสองขั้นแรก

วิธีที่ 1

ไล่ทุกเส้นทาง

ทุกวันที่เพิ่ม งานคูณด้วยจำนวนถนนที่ออกจากเมือง

ทุกเมืองลองถนนทุกเส้นที่ออกไป จนครบ T วัน แล้วเก็บเส้นที่จบที่เมือง 1

20% (T ไม่เกิน 5)

วิธีที่ 2

ตารางรายวัน

ดูถนนครบ M เส้นทุกวัน รวม T × M ครั้ง

จำค่าที่ดีที่สุดของทุกคู่ (วัน, เมือง) ไว้ เส้นทางที่มาถึงจุดเดียวกันไม่ต้องคิดต่อซ้ำ

40% (T ไม่เกิน 52,501)

วิธีเต็ม

สำเนาเมือง + คูณเมทริกซ์

ราว 7.3 × 108 ครั้งในเคสหนักสุดที่ผมสร้าง

ให้ทุกวันใช้กฎชุดเดียวกัน แล้วกระโดดข้ามวันทีละก้อน (ตอนที่ 3 ถึง 5)

100%

วิธีที่ 1 แย่ที่สุดในชุด T ไม่เกิน 5 คือถนน 50 เส้นวนอยู่ที่เมือง 1 ยาววันละเส้น ได้ 505 = 312,500,000 เส้นทาง ผมวัดได้ 1.2 วินาที ทันลิมิต 2 วินาที

วิธีที่ 2 คือ DP ที่ใบ้ไว้ มาจากข้อสังเกตเดียว สองเส้นทางที่มาถึงเมืองเดียวกันในวันเดียวกัน เดินต่อได้ทางเดียวกันทุกทาง เก็บไว้แค่เส้นที่ได้มากกว่าก็พอ

ช่องของตารางรายวัน

  • dp[d][v] คือความพึงพอใจรวมมากที่สุด ถ้าวันที่ d ยืนอยู่เมือง v โดยนับ C[v] ของวันนั้นแล้ว
  • วันที่ 0 มีช่องเดียวที่ไปถึงได้ คือ dp[0][1] = C[1]
  • ช่องอื่นถามถนน u → v ยาว w วันทุกเส้นที่เข้าเมือง v: dp[d][v] = มากสุดของ dp[d − w][u] + C[v]
  • ถ้าวันนั้นมีเทศกาลที่เมือง v ก็บวก y เพิ่มเข้าช่องนั้น
  • คำตอบคือ dp[T][1] และถ้าช่องนั้นไปไม่ถึงก็ตอบ -1
ตารางรายวันของตัวอย่างแรก
วันที่เมือง 1เมือง 2เมือง 3
01··
1·4·
2···
3··8
45··
5·8·
6···
79·12
8912·
9·12·
10··16
1113·16
· แปลว่าวันนั้นไปยืนที่เมืองนั้นไม่ได้ แถวสุดท้ายช่องเมือง 1 คือคำตอบ 13

ช่องคำตอบมาจากไหน

เมือง 1 มีถนนเข้า 2 เส้น คือ 2→1 (3 วัน) และ 3→1 (4 วัน) ช่อง dp[11][1] เลยเลือกจาก dp[8][2] + 1 = 12 + 1 = 13 กับ dp[7][3] + 1 = 12 + 1 = 13 ได้ 13

สองทางได้เท่ากัน ตัวอย่างนี้มีสองเส้นทางที่ได้ 13 แผนที่โจทย์ยกมาคือเส้นที่ผ่านเมือง 3 ในวันที่ 7 โจทย์ต้องการแค่ตัวเลข เส้นไหนก็ตอบถูก

ชุดทดสอบ
คะแนนเงื่อนไขไล่ทุกเส้นทางตารางรายวัน
20%N ≤ 5, M ≤ 50, T ≤ 5ได้ได้
20%M ≤ 50, T ≤ 52,501ไม่ทันได้
10%M ≤ 50, เมืองต่อกันเป็นวงเดียวไม่ทันไม่ทัน
15%M ≤ 50, ไม่มีเทศกาลไม่ทันไม่ทัน
10%M ≤ 50, เทศกาลไม่เกิน 10 งานไม่ทันไม่ทัน
10%M ≤ 50ไม่ทันไม่ทัน
15%ไม่มีเงื่อนไขเพิ่มไม่ทันไม่ทัน
ตารางรายวันตัวเดียวครอบทั้งสองชุดแรกได้ 40% ชุดที่เหลือปล่อย T ไปถึง 1,000,000,000

เฉลย ตอนที่ 2 · T ถึงพันล้าน ขาดไปกี่เท่า

ตารางรายวันได้ 40% แล้ว ก่อนหาท่าใหม่ ลองวัดก่อนว่ามันขาดไปเท่าไร งานของมันโตตาม T ตรง ๆ

106107108109101010111012 ตารางรายวัน · T = 52,501ตารางรายวัน · T = 1,000,000,000เมทริกซ์ · เคสหนักสุดที่สร้าง 2.6×107 ทัน5×1011 เกินงบราว 251 เท่า7.3×108 ทัน งบ 2 วินาที
จำนวนครั้งที่ต้องบวกแล้วเทียบ สเกลแบบ log (ขีดละสิบเท่า) เส้นทองคืองบ 2 วินาที

คิดเป็นตัวเลข

  • งบ: 2 วินาที × 109 ครั้งต่อวินาที = 2,000,000,000 ครั้ง
  • T = 52,501: 52,501 × 501 = 26,303,001 ครั้ง ทันสบาย
  • T = 1,000,000,000: ราว 5 × 1011 ครั้ง เกินงบราว 251 เท่า

109 ครั้งต่อวินาทีเป็นตัวเลขกะแบบระวังไว้ก่อน โค้ดเต็มของหน้านี้วัดได้ราว 1.1 ถึง 1.6 × 109 ครั้งต่อวินาที

หน่วยความจำแก้ได้ ตารางทั้งใบกิน 400,000 เมกะไบต์ก็จริง แต่ถนนยาวไม่เกิน 5 วัน เก็บแค่ 6 แถวล่าสุดก็พอ ส่วนเวลาลดตามไม่ได้

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

เป้าหมายที่ตัวเลขบอก

  • ถ้ารู้ผลของการเดิน 1 วัน แล้วรวมเป็นผลของ 2, 4, 8, … วันได้ ไปถึงพันล้านใช้แค่ 30 ชั้น
  • รวมผลสองช่วงเข้าด้วยกันต้องลองทุกคู่ (จุดเริ่ม, จุดจบ) กับทุกจุดพักตรงกลาง ถ้ามีจุดให้ยืน S แบบ ก็เป็น S × S × S ครั้งต่อชั้น
  • งบต่อชั้นคือ 2,000,000,000 ÷ 30 ≈ 66,666,667 ครั้ง รากที่สามของเลขนี้คือราว 405 S เลยต้องไม่เกิน 405

เมือง 50 เมืองอยู่ใต้ 405 สบาย ที่ยังติดคือการรวมแบบนี้ต้องการให้ทุกวันใช้กฎชุดเดียวกัน แต่ถนนยาว 1 ถึง 5 วัน คนที่ออกจากเมืองวันนี้อาจยังไม่ถึงไหนในวันพรุ่งนี้

เฉลย ตอนที่ 3 · หั่นเมืองจนถนนทุกเส้นยาวหนึ่งวัน

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

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

ตัวเลขที่ทำให้กล้าหั่นเมืองเป็นสำเนาคือ w ไม่เกิน 5 กับ N ไม่เกิน 50 คูณกันได้ 250 แบบ อยู่ใต้เพดาน 405 ของตอนที่ 2 ถ้า w ยาวถึงหลักพัน ท่านี้ใช้ไม่ได้

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

นึกภาพ

ซื้อตั๋วรถไปเมือง v ตั๋วเขียนว่าอีกกี่วันถึง ทุกคืนตัวเลขบนตั๋วลดลงหนึ่ง พอเหลือ 0 เขาก็ยืนอยู่ที่ v แล้วต้องซื้อตั๋วใบใหม่ในวันนั้นเพราะห้ามพัก

สองคนที่ถือตั๋วเหมือนกันทุกตัวอักษร จะมาจากถนนเส้นไหนก็ตาม เก็บไว้แค่คนที่สะสมได้มากกว่าก็พอ

สถานะใหม่ (v, j) อ่านว่า “เมือง v อีก j วันถึง”

  • W คือความยาวถนนที่ยาวที่สุดในอินพุต
  • (v, 0) แปลว่าวันนี้ยืนอยู่เมือง v
  • (v, j) ที่ j ตั้งแต่ 1 ถึง W − 1 แปลว่าอยู่บนถนน และอีก j วันจะถึงเมือง v
  • ทุกเมืองมีสำเนา W ตัว (j = 0 ถึง W − 1) รวมเป็นสถานะ S = N × W ตัว ซึ่งไม่เกิน 50 × 5 = 250

เมื่อมองแบบนี้ การเดินทางทั้งอาณาจักรเหลือกฎสองข้อ และทั้งสองข้อใช้เวลาหนึ่งวันเท่ากัน

  • ถนน u → v ยาว w วัน: ก้าวจาก (u, 0) ไป (v, w − 1) และได้ C[v] ตอนนี้ ที่เป็น w − 1 เพราะก้าวนี้เองกินไปแล้วหนึ่งวัน ถนนยาว 1 วันจึงพาไปถึง (v, 0) ในพรุ่งนี้พอดี
  • อยู่บนถนน: ก้าวจาก (v, j) ไป (v, j − 1) ได้ 0 และไปทางอื่นไม่ได้
กฎสองข้อของหนึ่งวัน
// สร้างเมทริกซ์ของหนึ่งวัน จากถนนกับโซ่สำเนาเมือง
// id(v, j) = หมายเลขสถานะของสำเนาที่ j ของเมือง v
for (auto [u, v, w] : edges)
    A[id(u, 0)][id(v, w - 1)] = max(A[id(u, 0)][id(v, w - 1)], C[v]);  // ออกเดินทาง จ่ายรางวัลตรงนี้
for (int v = 0; v < n; v++)
    for (int j = 1; j < W; j++)
        A[id(v, j)][id(v, j - 1)] = 0;                                  // อยู่ระหว่างทาง ผ่านไปหนึ่งวัน

รางวัล C[v] ถูกจ่ายตั้งแต่ออกเดินทาง ทั้งที่มื้อนั้นยังไม่ได้กิน ตอนที่ 6 อธิบายว่าทำไมไม่นับเกิน รูปข้างล่างกางขาสุดท้ายของแผนในตัวอย่างแรกออกเป็นการก้าววันละครั้ง

(2,0) วันที่ 5 (3,1) วันที่ 6 +4 ถนน 2→3 (3,0) วันที่ 7 (1,3) วันที่ 8 +1 ถนน 3→1 (1,2) วันที่ 9 (1,1) วันที่ 10 (1,0) วันที่ 11
ขาสุดท้ายของตัวอย่างแรก กางเป็นการก้าววันละครั้ง กล่องเขียวคือสำเนา j = 0 ซึ่งแปลว่าเหยียบเมืองนั้นจริงในวันนั้น กล่องเทาคือยังอยู่บนถนน ถนน 3→1 ที่ยาว 4 วันกลายเป็นสี่ก้าว และรางวัล +1 ของเมือง 1 ถูกจ่ายตั้งแต่ก้าวแรก

ตัวอย่างแรกมีกี่สถานะ

  • ถนนที่ยาวที่สุดคือ 3→1 ยาว 4 วัน ได้ W = 4
  • แต่ละเมืองมีสำเนา j = 0 ถึง 3 รวม 4 ตัว
  • 3 เมือง × 4 = 12 สถานะ ตอนที่ 4 ใช้ชุดนี้ต่อ

เฉลย ตอนที่ 4 · หนึ่งวันคือการคูณเมทริกซ์หนึ่งครั้ง

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

กฎสองข้อของตอนที่ 3 เขียนลงตารางสี่เหลี่ยมได้ ช่องแถว p หลัก q บอกว่าวันนี้อยู่ p แล้วพรุ่งนี้อยู่ q ได้กี่หน่วย ตารางตัวเลขแบบนี้เรียกว่าเมทริกซ์ (matrix) ตั้งชื่อว่า A

เมทริกซ์ของหนึ่งวัน (ตัวอย่างแรก)
จาก \ ไป(1,0)(1,1)(1,2)(1,3)(2,0)(3,0)(3,1)
(1,0)····3··
(1,1)0······
(1,2)·0·····
(1,3)··0····
(2,0)··1···4
(3,0)···1···
(3,1)·····0·
แถวคือวันนี้ หลักคือพรุ่งนี้ · คือก้าวไม่ได้ เลข 0 คือการเลื่อนบนถนน เลขอื่นคือ C[v] ของเมืองปลายทาง ตารางเต็ม 12 × 12 มีช่องที่ก้าวได้ 13 ช่อง (4 จากถนน และ 9 จากการเลื่อน) ที่เห็นนี้คือ 8 ช่องในบรรดา 7 สถานะที่ไปถึงจากเมือง 1

อีก 5 สถานะไม่มีทางไปถึงจาก (1,0) เพราะสำเนา (v, j) ต้องเริ่มจากถนนที่เข้าเมือง v และยาวกว่า j วัน ถนนเข้าเมือง 2 ยาวสุด 1 วัน (2,1) (2,2) (2,3) เลยไม่มีใครเข้าถึง ถนนเข้าเมือง 3 ยาวสุด 2 วัน (3,2) (3,3) เลยไม่มีใครเข้าถึง

กฎของหนึ่งวัน

ใหม่[q] = มากสุดของ เก่า[p] + A[p][q] เมื่อไล่ p ทุกสถานะ (ข้ามช่องที่เป็น ·)

เครื่องเดินข้างล่างทำกฎนี้กับตัวอย่างแรกทีละวัน ในคำอธิบาย อ่าน ← ว่า “รับค่ามาจาก” ช่อง (1,2) วันที่ 9 เป็นช่องแรกที่มีให้เลือกสองทาง คือถนนจาก (2,0) (12 + 1) หรือเลื่อนจาก (1,3) (13 + 0) แล้วเลือกค่าที่มากกว่า

คอลัมน์สีเขียวคือสำเนา j = 0 ตรงกับตารางรายวันของตอนที่ 1 ทุกช่อง

ตัวอย่างแรก 7 สถานะ เดินทีละวัน

วันที่(1,0)(1,1)(1,2)(1,3)(2,0)(3,0)(3,1)
01······
1····4··
2··5···8
3·5···8·
45··9···
5··9·8··
6·99···12
799···12·
89··1312··
9··13·12·16
10·1313··1616
111313·17·16·

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

ได้ 3 + 1 = 4 และเครื่องเดินวันที่ 2 ช่อง (1,2) ก็เป็น 1 + 4 = 5 ถ้ามีจุดพักหลายตัว ก็บวกสองท่อนของแต่ละตัว แล้วเลือกค่ามากสุด

คูณเมทริกซ์แบบมากสุดบวก

(A·B)[p][q] = มากสุดของ A[p][m] + B[m][q] เมื่อไล่จุดพัก m ทุกตัว

หน้าตาเหมือนการคูณเมทริกซ์ที่เรียนกันในคณิตศาสตร์ เปลี่ยนแค่ “บวก” เป็น “มากสุด” และ “คูณ” เป็น “บวก”

ระบบตัวเลขแบบนี้เรียกว่าพีชคณิตมากสุดบวก (max-plus algebra อ่านว่า “แม็กซ์-พลัส”) บางตำราเรียกพีชคณิตเขตร้อน (tropical) ที่มาของชื่อที่เล่ากันบ่อยคือนักวิจัยฝรั่งเศสตั้งให้เป็นเกียรติแก่ Imre Simon นักวิทยาการคอมพิวเตอร์ชาวบราซิลที่เกิดในฮังการี

คูณเมทริกซ์แบบมากสุดบวก
// หนึ่งวันคือการคูณหนึ่งครั้ง แต่บวกกลายเป็นมากสุด และคูณกลายเป็นบวก
// C[i][j] = ค่ามากที่สุดของ A[i][k] + B[k][j] เมื่อไล่ k ทุกตัว
for (int i = 0; i < S; i++)
    for (int k = 0; k < S; k++) {
        if (A[i][k] <= HALF) continue;          // ไปจากสถานะ i ถึง k ไม่ได้ ข้าม
        for (int j = 0; j < S; j++)
            C[i][j] = max(C[i][j], A[i][k] + B[k][j]);
    }

การคูณนี้จะใส่วงเล็บตรงไหนก็ได้ผลเท่ากัน เดิน a วัน ต่อ b วัน ต่อ c วัน คือการต่อสามท่อนเป็นทางเดียว จะต่อสองท่อนแรกก่อนหรือสองท่อนหลังก่อน ก็ได้ทางเดินชุดเดียวกันมาเลือก

ความจริงข้อนี้ทำให้ยกกำลังได้ เมทริกซ์ของ 4 วันคือเมทริกซ์ของ 2 วันคูณตัวเอง เช่นช่อง (1,0) ไป (1,0) ของ 4 วันได้ 4 ตรงกับเครื่องเดินวันที่ 4 ที่ช่อง (1,0) เป็น 1 + 4 = 5

เก็บเมทริกซ์ของ 1, 2, 4, 8, … วันไว้ทุกชั้น ตารางแบบนี้เรียกว่าตารางกระโดด (binary lifting) เป็นท่าเดียวกับที่ข้อห้องประชุมใช้กระโดดข้ามงาน

เดิน 11 วันด้วยการกระโดด

  • 11 = 1 + 2 + 8 คือบิตที่ 0, 1, 3 ของเลขฐานสองติดอยู่
  • เอาเวกเตอร์วันที่ 0 คูณกับชั้น 1 วัน, 2 วัน, 8 วัน ตามลำดับ
  • หลังคูณแต่ละชั้นได้แถววันที่ 1, 3, 11 ของเครื่องเดินครบทุกช่อง และช่อง (1,0) จบที่ 13

ทุกชั้นเป็นกำลังของ A ตัวเดียวกัน คูณสลับลำดับกันได้ จะเริ่มจากบิตต่ำหรือบิตสูงก็ได้เมทริกซ์ของ 11 วันเหมือนกัน

การคูณเวกเตอร์กับเมทริกซ์ใช้ S × S ครั้ง ถูกกว่าคูณเมทริกซ์สองใบ S เท่า โค้ดเลยคูณเมทริกซ์แค่ตอนสร้างชั้น ส่วนการเดินหน้าใช้เวกเตอร์ล้วน

ที่ขอบเขตใหญ่สุด S = 250 สร้างชั้นด้วยการยกกำลังสองซ้ำ 29 ครั้ง ครั้งละ 15,625,000 การบวกเทียบ ทั้ง 30 ชั้นกินหน่วยความจำ 15 เมกะไบต์ จากลิมิต 512

เฉลย ตอนที่ 5 · เทศกาลหั่นเส้นเวลาออกเป็นช่วง

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

ตัวอย่างที่สองมี 4 เมือง ถนน 8 เส้น T = 16 และเทศกาลตามลำดับในอินพุตคือ วันที่ 3 เมือง 3 +5, วันที่ 1 เมือง 2 +5, วันที่ 5 เมือง 4 +20

เทศกาลมีไม่เกิน 200 งาน เส้นเวลาถูกหั่นเป็นช่วงได้ไม่เกิน 201 ช่วง ทำแบบนี้

  1. เรียงเทศกาลตามวัน
  2. กระโดดจากวันปัจจุบันไปถึงวันของงานถัดไป
  3. บวก y เข้าช่อง (x, 0) ซึ่งแปลว่ายืนอยู่เมือง x พอดีวันนั้น
  4. ทำซ้ำจนหมดงาน แล้วกระโดดช่วงสุดท้ายไปถึงวันที่ T
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 เมือง 2 +5 เมือง 3 +5 เมือง 4 +20 1 วัน 2 วัน 2 วัน 11 วัน = 8 + 2 + 1
เส้นเวลาของตัวอย่างที่สอง งาน 3 งานหั่น 16 วันออกเป็น 4 ช่วง แต่ละช่วงกระโดดตามบิตของความยาวช่วง ช่วงสุดท้ายยาว 11 วัน เลยกระโดด 3 ครั้งคือ 8 แล้ว 2 แล้ว 1

ช่อง (x,0) ของแต่ละงานก่อนและหลังบวก เมื่อเดินตัวอย่างที่สองแบบนี้

  • วันที่ 1 ช่อง (2,0): 4 เป็น 9
  • วันที่ 3 ช่อง (3,0): 11 เป็น 16
  • วันที่ 5 ช่อง (4,0): 9 เป็น 29
  • ช่วงสุดท้ายพาไปถึงวันที่ 16 ช่อง (1,0) ได้ 39

ทุกงานได้บวกเข้าช่องของตัวเอง แต่เส้นทางที่ชนะผ่านแค่งานวันที่ 5 ค่าในช่องอื่นแพ้ไปเองตอนเลือกค่ามากสุดในวันถัด ๆ ไป

  • โค้ดบวกเฉพาะช่องที่ไปถึงได้ ช่อง · เก็บค่าลบมหาศาล −1018 ไว้ ต่อให้บวกเทศกาลทุกงานรวมกันไม่เกิน 2 × 1011 มันก็ยังต่ำกว่าเส้นแบ่ง −5 × 1017 อยู่ดี การเช็กก่อนบวกทำให้ความถูกต้องไม่ต้องพึ่งเลขชุดนี้
  • โจทย์สัญญาว่าแต่ละงานจัดคนละวัน โค้ดก็ยังบวกทุกงานของวันเดียวกันให้ครบก่อนกระโดดต่อ เผื่อไว้

เฉลย ตอนที่ 6 · จ่ายรางวัลตั้งแต่ออกเดินทาง ทำไมไม่นับเกิน

ค้างไว้ตั้งแต่ตอนที่ 3 คือกฎข้อแรกบวก C[v] ตอนก้าวจาก (u, 0) ไป (v, w − 1) ถ้าวันที่ T มาถึงก่อนเขาไปถึงเมือง v ก็เท่ากับแจกรางวัลของมื้อที่ยังไม่ได้กิน

ข้อที่ค้ำทั้งหมด

ทางเดินใดก็ตามที่จบที่ (1,0) ในวันที่ T ประกอบด้วยถนนครบเส้นต่อกัน เพราะเมื่อก้าวเข้า (v, w − 1) แล้ว ทางออกเดียวคือเลื่อนลงทีละหนึ่งจนถึง (v, 0) รางวัลทุกก้อนที่จ่ายล่วงหน้าในวัน d เลยได้ลงจอดในวัน d + w ซึ่งไม่เกิน T

ดูของจริงจากเครื่องเดินในวันที่ 11

  • ช่อง (1,3) = 17 รวม C[1] ที่จ่ายล่วงหน้าไว้แล้ว
  • ช่องนั้นต้องเลื่อนอีก 3 วันถึงจะเป็น (1,0) คือวันที่ 14 เลยวันที่ 11 ไปแล้ว
  • คำตอบอ่านจาก (1,0) = 13 เท่านั้น ค่า 17 ไม่มีทางไหลเข้ามา

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

เฉลย ตอนที่ 7 · โค้ดเต็มและการตรวจ

ดูโค้ดเต็มขอบเขต
dl_full.cpp
// NOI 2020 "Delicacy" (programming.in.th 2041) - full constraints.
// (max,+) matrix exponentiation over a time-expanded graph.
//
// Every edge weight is 1..5, so split each city v into W copies (v,j),
// j = 0..W-1, where W = max edge weight.  State (v,j) means "I will land on
// city v in j more days".  (v,0) means "I am standing on v right now".
// One day = one application of the matrix A:
//     (u,0) --edge u->v of weight w--> (v, w-1)   gaining C[v]
//     (v,j) --> (v,j-1)  (j >= 1)                 gaining 0
// The reward C[v] is paid the moment the trip is committed; that is fine
// because a walk is only ever read off at a (x,0) state, where no trip is
// pending.
//
// Start vector: (1,0) = C[1], everything else unreachable.
// Advance day by day with binary lifting over A^(2^b); at each festival day t
// hop to day t, add y to the entry of state (x,0), continue.  Answer is the
// entry of (1,0) after exactly T days.
#include "dl_case.h"
#include <algorithm>
#include <vector>
#include <cstdio>

namespace dlfull {

using ll = long long;
static const ll NEG = -1000000000000000000LL;  // "unreachable"
static const ll HALF = -500000000000000000LL;  // anything <= HALF is unreachable
                                               // (every real value is >= 0)

struct MaxPlus {
    int S = 0;

    // C = A (max,+) B
    void mul(const std::vector<ll> &A, const std::vector<ll> &B, std::vector<ll> &C) const {
        for (int i = 0; i < S; i++) {
            ll *c = &C[(size_t)i * S];
            for (int j = 0; j < S; j++) c[j] = NEG;
            const ll *a = &A[(size_t)i * S];
            for (int k = 0; k < S; k++) {
                ll av = a[k];
                if (av <= HALF) continue;
                const ll *b = &B[(size_t)k * S];
                for (int j = 0; j < S; j++) {
                    ll t = av + b[j];
                    if (t > c[j]) c[j] = t;
                }
            }
            // Re-normalise: finite + NEG is still astronomically negative but
            // not exactly NEG; snapping it back stops it creeping upward over
            // the ~35 products we chain together.
            for (int j = 0; j < S; j++)
                if (c[j] <= HALF) c[j] = NEG;
        }
    }

    // out = v (max,+) B   (v is a row vector)
    void vmul(const std::vector<ll> &v, const std::vector<ll> &B, std::vector<ll> &out) const {
        for (int j = 0; j < S; j++) out[j] = NEG;
        for (int i = 0; i < S; i++) {
            ll av = v[i];
            if (av <= HALF) continue;
            const ll *b = &B[(size_t)i * S];
            for (int j = 0; j < S; j++) {
                ll t = av + b[j];
                if (t > out[j]) out[j] = t;
            }
        }
        for (int j = 0; j < S; j++)
            if (out[j] <= HALF) out[j] = NEG;
    }
};

ll solve(const Case &cs) {
    const int n = cs.n;
    const ll T = cs.T;

    int W = 1;
    for (const auto &e : cs.E) W = std::max(W, e[2]);

    MaxPlus mp;
    mp.S = n * W;
    const int S = mp.S;
    auto id = [&](int v, int j) { return v * W + j; };

    // A = one-day transition matrix.
    std::vector<ll> A((size_t)S * S, NEG);
    for (const auto &e : cs.E) {
        int u = e[0], v = e[1], w = e[2];
        ll &cell = A[(size_t)id(u, 0) * S + id(v, w - 1)];
        cell = std::max(cell, cs.C[v]);
    }
    for (int v = 0; v < n; v++)
        for (int j = 1; j < W; j++) A[(size_t)id(v, j) * S + id(v, j - 1)] = 0;

    // Binary powers of A up to the highest bit of T.
    int B = 0;
    while ((1LL << (B + 1)) <= T) B++;
    std::vector<std::vector<ll>> pw;
    pw.push_back(A);
    for (int b = 1; b <= B; b++) {
        std::vector<ll> nxt((size_t)S * S);
        mp.mul(pw[b - 1], pw[b - 1], nxt);
        pw.push_back(std::move(nxt));
    }

    std::vector<ll> vec((size_t)S, NEG), tmp((size_t)S);
    vec[id(0, 0)] = cs.C[0];  // day 0: standing on city 1, already eaten there

    auto advance = [&](ll d) {
        for (int b = 0; d > 0; b++, d >>= 1) {
            if (d & 1) {
                mp.vmul(vec, pw[b], tmp);
                vec.swap(tmp);
            }
        }
    };

    std::vector<std::array<ll, 3>> F = cs.F;
    std::sort(F.begin(), F.end(),
              [](const std::array<ll, 3> &a, const std::array<ll, 3> &b) { return a[0] < b[0]; });

    ll now = 0;
    for (size_t i = 0; i < F.size(); i++) {
        ll t = F[i][0];
        if (t > T || t < now) continue;  // defensive; the statement gives 1 <= t <= T
        advance(t - now);
        now = t;
        // Apply every festival held on day t.  Two festivals on the same day in
        // the same city stack; in different cities they simply land on
        // different states, which is exactly right.
        size_t j = i;
        while (j < F.size() && F[j][0] == t) {
            int x = (int)F[j][1];
            ll &cell = vec[id(x, 0)];
            if (cell > HALF) cell += F[j][2];
            j++;
        }
        i = j - 1;
    }
    advance(T - now);

    ll ans = vec[id(0, 0)];
    return ans <= HALF ? -1 : ans;
}

}  // namespace dlfull

#ifndef DL_NO_MAIN
int main() {
    Case cs = readCase();
    printf("%lld\n", dlfull::solve(cs));
    return 0;
}
#endif

ค่าที่มากที่สุดที่เป็นไปได้คือ 52,501 ต่อวัน คูณ 1,000,000,000 วัน บวกเทศกาลอีก 200 งาน งานละถึง 1,000,000,000 เกิน int ไปไกล ทุกอย่างเป็น long long

NEG คือเลขลบมหาศาลที่แทน · (ไปไม่ถึง) ส่วน HALF คือเส้นแบ่งครึ่งทาง ค่าไหนต่ำกว่าเส้นนี้ถือว่าไปไม่ถึง หลังคูณแต่ละครั้งโค้ดดึงค่าพวกนั้นกลับเป็น NEG พอดี มันจะได้ไม่ค่อย ๆ ไต่ขึ้นตลอด 30 ทอดที่คูณต่อกัน

แผนที่ของโค้ดเต็ม
ส่วนในโค้ดอธิบายอยู่ที่
struct MaxPlus (mul, vmul)ตอนที่ 4 · กฎของหนึ่งวัน และการคูณแบบมากสุดบวก
// A = one-day transition matrixตอนที่ 3 · กฎสองข้อของสำเนาเมือง
// Binary powers of A ... และ advance()ตอนที่ 4 · ตารางกระโดด
sort F แล้ว // Apply every festival held on day tตอนที่ 5 · เทศกาลหั่นเส้นเวลา
ans = vec[id(0, 0)] และคืน -1ตอนที่ 6 · อ่านคำตอบที่ (1,0) เท่านั้น
NEG กับ HALFย่อหน้าใต้โค้ด
ดูโค้ดตารางรายวัน (ชุดที่ T ไม่เกิน 52,501)
dl_sub.cpp
// NOI 2020 "Delicacy" - subtask solver for T <= 52501 (also used as an oracle).
// Plain DP over days, written in pull form: dp[d][v] = best satisfaction for
// standing on city v on day d, already counting C[v] and any festival of day d.
// A cell asks every road u->v of length w where it could have come from:
//     dp[d][v] = max over roads (u, v, w) with w <= d of dp[d - w][u] + C[v]
#include "dl_case.h"
#include <algorithm>
#include <vector>
#include <cstdio>

namespace dlsub {

using ll = long long;
static const ll NONE = -1;  // unreachable (real values are always >= 0)

ll solve(const Case &cs) {
    const int n = cs.n;
    const ll T = cs.T;

    // roads grouped by destination city: (source, weight)
    std::vector<std::vector<std::pair<int, int>>> in(n);
    for (const auto &e : cs.E) in[e[1]].push_back({e[0], e[2]});

    // festivals bucketed by day
    std::vector<std::vector<std::pair<int, ll>>> fest((size_t)T + 1);  // (city, bonus)
    for (const auto &f : cs.F) {
        if (f[0] >= 0 && f[0] <= T) fest[(size_t)f[0]].push_back({(int)f[1], f[2]});
    }

    std::vector<std::vector<ll>> dp((size_t)T + 1, std::vector<ll>(n, NONE));
    dp[0][0] = cs.C[0];

    for (ll d = 0; d <= T; d++) {
        if (d > 0) {
            for (int v = 0; v < n; v++) {
                ll best = NONE;
                for (const auto &pr : in[v]) {
                    if (pr.second > d) continue;
                    ll from = dp[(size_t)(d - pr.second)][pr.first];
                    if (from == NONE) continue;
                    best = std::max(best, from + cs.C[v]);
                }
                dp[(size_t)d][v] = best;
            }
        }
        for (const auto &fb : fest[(size_t)d]) {
            if (dp[(size_t)d][fb.first] != NONE) dp[(size_t)d][fb.first] += fb.second;
        }
    }
    return dp[(size_t)T][0];  // NONE == -1, which is exactly the required output
}

}  // namespace dlsub

#ifndef DL_NO_MAIN
int main() {
    Case cs = readCase();
    printf("%lld\n", dlsub::solve(cs));
    return 0;
}
#endif

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

ดูตัวไล่ทุกเส้นทางที่ใช้เทียบ
dl_brute.cpp
// NOI 2020 "Delicacy" - brute force for tiny cases.
// Literally enumerates every walk out of city 1: at each city pick an outgoing
// road, recurse, stop when day == T.  No DP table, no memoisation, no matrix -
// it shares nothing with the other two solvers except the input struct.
#include "dl_case.h"
#include <algorithm>
#include <vector>
#include <cstdio>

namespace dlbrute {

using ll = long long;

struct Runner {
    const Case *cs = nullptr;
    std::vector<std::vector<std::pair<int, int>>> adj;
    ll best = -1;

    ll bonusAt(ll day, int city) const {
        ll s = 0;
        for (const auto &f : cs->F)
            if (f[0] == day && (int)f[1] == city) s += f[2];
        return s;
    }

    void rec(ll day, int city, ll sum) {
        if (day == cs->T) {
            if (city == 0 && sum > best) best = sum;
            return;
        }
        for (const auto &pr : adj[city]) {
            ll nd = day + pr.second;
            if (nd > cs->T) continue;
            rec(nd, pr.first, sum + cs->C[pr.first] + bonusAt(nd, pr.first));
        }
    }
};

ll solve(const Case &cs) {
    Runner r;
    r.cs = &cs;
    r.adj.assign(cs.n, {});
    for (const auto &e : cs.E) r.adj[e[0]].push_back({e[1], e[2]});
    r.rec(0, 0, cs.C[0] + r.bonusAt(0, 0));
    return r.best;
}

}  // namespace dlbrute

#ifndef DL_NO_MAIN
int main() {
    Case cs = readCase();
    printf("%lld\n", dlbrute::solve(cs));
    return 0;
}
#endif

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

ดูส่วนอ่านอินพุตที่ทั้งสามโปรแกรมใช้ร่วมกัน
dl_case.h
// Shared *data* declaration only (no algorithm code) so that the three
// independent solvers can be linked into one stress-test binary verbatim.
#ifndef DL_CASE_H
#define DL_CASE_H
#include <vector>
#include <array>
#include <cstdio>

struct Case {
    int n = 0, m = 0, k = 0;
    long long T = 0;
    std::vector<long long> C;                // C[i], cities 0-indexed
    std::vector<std::array<int, 3>> E;       // {u, v, w}, cities 0-indexed
    std::vector<std::array<long long, 3>> F; // {t, x, y}, x 0-indexed
};

inline Case readCase() {
    Case c;
    long long T = 0;
    if (scanf("%d %d %lld %d", &c.n, &c.m, &T, &c.k) != 4) return c;
    c.T = T;
    c.C.assign(c.n, 0);
    for (int i = 0; i < c.n; i++) {
        if (scanf("%lld", &c.C[i]) != 1) return c;
    }
    c.E.assign(c.m, {0, 0, 0});
    for (int i = 0; i < c.m; i++) {
        int u = 0, v = 0, w = 0;
        if (scanf("%d %d %d", &u, &v, &w) != 3) return c;
        c.E[i] = {u - 1, v - 1, w};
    }
    c.F.assign(c.k, {0, 0, 0});
    for (int i = 0; i < c.k; i++) {
        long long t = 0, x = 0, y = 0;
        if (scanf("%lld %lld %lld", &t, &x, &y) != 3) return c;
        c.F[i] = {t, x - 1, y};
    }
    return c;
}
#endif

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

ตรวจอะไรไปแล้วบ้าง

ทั้งสามโปรแกรมตอบตัวอย่างได้ 13 กับ 39 และตรงกันใน 3,000 เคสสุ่มจิ๋ว (เมืองไม่เกิน 4, T ไม่เกิน 9) กับอีก 1,500 เคสจิ๋วที่ปล่อยให้เทศกาลซ้ำวัน โค้ดเต็มตรงกับตารางรายวันใน 3,000 เคสขนาดกลาง (T ไม่เกิน 3,000)

T ใหญ่ตรวจบนอาณาจักรที่เมืองต่อกันเป็นวงเดียว ซึ่งนับคำตอบได้ด้วยสูตรตรง 3,000 เคส T สุ่มถึง 1,000,000,000 มี 1,382 เคสที่ T ตั้งแต่ 229 ขึ้นไป คือใช้ชั้นกระโดดครบทั้ง 30 ชั้น และ 1,315 เคสเก็บเทศกาลได้จริง

ขอบเขตของการตรวจนี้คือกราฟหลายรูปแบบที่ T เล็ก กับ T ใหญ่บนวงเดียว ส่วนที่ครอบทุกขนาดคือเหตุผลในตอนที่ 3 ถึง 6

เวลาและหน่วยความจำ
โปรแกรม · เคสวินาทีเมกะไบต์
เต็ม · N = 50, M = 501, T = 1,000,000,000, เทศกาล 200 งานห่างกัน 222 − 1 วัน0.45 ถึง 0.6418.1
เต็ม · ขนาดเดียวกัน ไม่มีเทศกาล0.21 ถึง 0.2718.1
ตารางรายวัน · N = 20, M = 50, T = 52,5010.0114.3
ไล่ทุกเส้นทาง · ถนน 50 เส้นวนที่เมือง 1, T = 51.23.2
วัดบนเครื่องผม (WSL, g++ -O2) ด้วยโค้ดที่ดึงออกมาจากหน้านี้ โค้ดเต็มรันเคสละหกรอบ ตัวอื่นรอบเดียว หน่วยความจำคือค่าสูงสุดที่ระบบรายงาน ลิมิตคือ 2 วินาทีและ 512 เมกะไบต์ ในเคสแรก 200 จาก 201 ช่วงยาว 222 − 1 วัน ต้องคูณเวกเตอร์ช่วงละ 22 ครั้ง ช่วงสุดท้ายยาว 161,139,400 วันใช้ 13 ครั้ง รวม 4,413 ครั้ง งานทั้งหมดราว 7.3 × 108 การบวกเทียบ ส่วน 8.3 × 108 เป็นขอบบนแบบหลวมที่ถือว่าทุกช่วงมี 30 บิต

ทดสอบตัวเอง

ถ้าถนนยาวได้ถึง 1,000 วัน สถานะจะมีกี่ตัว และยังใช้ท่านี้ได้ไหม

50 × 1,000 = 50,000 สถานะ คูณเมทริกซ์หนึ่งครั้งก็ราว 1.3 × 1014 ครั้งแล้ว เกินเพดาน 405 ของตอนที่ 2 ไปมาก ท่านี้ใช้ได้เพราะถนนยาวไม่เกิน 5 วันเท่านั้น

ชุด “ชุดสุดท้าย” ในเกมมีถนน 1→2 (1 วัน), 2→1 (1 วัน), 2→3 (1 วัน), 3→2 (1 วัน), 1→3 (2 วัน), 3→1 (2 วัน) และ T = 7 ถ้าเอาเข้าเครื่องเดิน ช่อง (1,0) ของวันที่ 7 จะเป็นอะไร

เป็น · ให้เมือง 1 กับ 3 เป็นฝั่งคู่ และเมือง 2 เป็นฝั่งคี่ ถนน 1 วันทุกเส้นพาข้ามฝั่ง ถนน 2 วันทุกเส้นพากลับฝั่งเดิม วันที่ยืนอยู่เมือง 1 จึงเป็นวันคู่เสมอ ส่วน 7 เป็นเลขคี่ โค้ดเห็นช่องนั้นเป็น NEG แล้วตอบ -1

ทำไมโค้ดเต็มคูณเวกเตอร์ตอนเดินแต่ละช่วง แทนที่จะคูณเมทริกซ์ทั้งใบ

ต้องการแค่ค่าที่เริ่มจากเมือง 1 วันที่ 0 ซึ่งเป็นแถวเดียว คูณเวกเตอร์ราคา 62,500 ต่อครั้ง ส่วนคูณเมทริกซ์สองใบราคา 15,625,000 ถ้าทำแบบหลังทุกช่วงของ 201 ช่วง จะแพงขึ้น 250 เท่า

ปิดท้าย

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

แหล่งที่มา

  1. โจทย์ นักชิม (Delicacy) บน programming.in.th ข้อ 2041 programming.in.th/tasks/2041 (สืบค้น 11 กันยายน 2026)
  2. ต้นฉบับจาก The 37th CCF National Olympiad in Informatics (NOI 2020) ตามที่ระบุไว้ท้ายโจทย์
  3. อาณาจักรสามชุดหลังในกล่อง “ลองเอง” ผมแต่งขึ้นเอง เพราะตัวอย่างในโจทย์ไม่มีชุดที่ตอบ -1 และไม่มีชุดที่ลงโทษการเดินแบบตะกละ คำตอบของทั้งสี่ชุดคิดด้วยการไล่ทุกเส้นทางตอนบิลด์ ถ้าไม่ตรงกับที่หน้านี้อ้าง หน้านี้จะไม่ถูกสร้าง