programming.in.th · ข้อ 2003

Miners: วิธีหาว่าอดีตส่วนไหนทิ้งได้ จนแสนชิ้นเหลือ 256 สถานะ

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

★★★☆☆ dpstate compression อ่าน 9 นาที 5 กันยายน 2026

โจทย์ · เหมืองถ่านหินสองแห่งกับคนงานที่เบื่ออาหารซ้ำ

มีเหมืองถ่านหินอยู่สองแห่ง แต่ละแห่งมีคนงานประจำอยู่ของตัวเอง คนงานจะขุดถ่านก็ต่อเมื่อมีอาหารส่งไปถึง และอาหารมีสามชนิดคือ M (meat เนื้อ), F (fish ปลา) และ B (bread ขนมปัง)

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

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

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

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

EXAMPLE
InputOutput
6
MBMFFB
12
16
MMBMBBBBMMMMMBMB
29

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

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

ปัญหาคือถ่านที่ได้ตอนนี้ไม่ใช่ทั้งหมดของสิ่งที่เราจ่ายไป การส่งของเข้าเหมืองหนึ่งเปลี่ยนความจำของเหมืองนั้น ไปอีกสองตาข้างหน้า ท่าที่ได้ถ่านเท่ากันวันนี้จึงทิ้งเหมืองไว้คนละสภาพ กองสั้น ๆ แค่ห้าชิ้นอย่าง MMFFF ก็หักวิธีโลภได้แล้ว

กอง MMFFF · วิธีโลภ ได้ 7
ชิ้นส่งไปคู่ล่าสุดของเหมืองนั้นได้ถ่านรวม
1. M เหมือง 1 · · 1 1
2. M เหมือง 1 · M 1 2
3. F เหมือง 1 M M 2 4
4. F เหมือง 1 M F 2 6
5. F เหมือง 1 F F 1 7
สองชิ้นแรกเป็น M เหมือนกัน วิธีโลภมองว่ายังไงก็ได้ 1 เท่ากันทั้งคู่ จึงกองไว้ที่เหมืองเดิม พอ F ตามมา เหมือง 2 ที่ยังว่างเปล่าให้ได้แค่ 1 มันจึงไม่เคยถูกใช้เลยทั้งกอง

แต่ถ้ายอมทิ้งของชิ้นที่สองไว้อีกเหมืองหนึ่งตั้งแต่แรก ทั้งที่ตอนนั้นได้ถ่านเท่ากันเป๊ะ ผลรวมปลายทางจะขยับขึ้นเป็น 8

กอง MMFFF · แผนที่ดีที่สุด ได้ 8
ชิ้นส่งไปคู่ล่าสุดของเหมืองนั้นได้ถ่านรวม
1. M เหมือง 2 · · 1 1
2. M เหมือง 1 · · 1 2
3. F เหมือง 2 · M 2 4
4. F เหมือง 1 · M 2 6
5. F เหมือง 1 M F 2 8
ชิ้นที่ 2 ยอมได้ถ่านเท่าเดิม แต่ซื้อเหมืองที่มี M ค้างไว้เพิ่มมาอีกหนึ่งแห่ง ของสามชิ้นหลังจึงมีที่ลงที่ได้ 2 ทุกชิ้น

ใบ้

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

ลองเอง · แจกของให้สองเหมือง

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

0 / 0

เหมือง 1

เหมือง 2

กองกับดักสั้นแค่ห้าชิ้น แต่ถ้าเดินตามสัญชาตญาณ "ชิ้นนี้ที่ไหนได้เยอะกว่า" จะจบไม่ถึงคะแนนเต็ม

ลองเล่นดูก่อนนะครับ พอมีคำตอบในใจแล้ว ค่อยไปดูเฉลย

เฉลย ตอนที่ 1 · อดีตทั้งกองยุบเหลือสี่ช่อง

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

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

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

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

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

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

แปลว่าอดีตทั้งกอง ไม่ว่าจะยาวเป็นแสนชิ้น ยุบลงเหลือของที่ต้องจำแค่สี่ช่อง คือคู่ล่าสุดของเหมือง 1 กับคู่ล่าสุดของเหมือง 2 เรียกมันว่า (x₁, y₁) และ (x₂, y₂) โดย y คือชิ้นล่าสุด แต่ละช่องเป็นได้ 4 ค่า คือ M, F, B หรือ "ยังว่าง" สำหรับตอนที่เหมืองนั้นเพิ่งเริ่ม

เหมือง 1 ได้รับของไปแล้ว M B F M ... x₁ y₁ เหมือง 2 ได้รับของไปแล้ว B B M F ... x₂ y₂ ลืมได้ ต้องจำ ลืมได้ ต้องจำ สถานะเดียวที่ต้องแบก (x₁, y₁, x₂, y₂) 4 × 4 × 4 × 4 = 256 แบบ
ของที่แจกไปแล้วเป็นแสนชิ้น แต่กล่องที่ต้องแบกต่อไปมีแค่สี่ช่อง ที่เหลือถูกลืมได้อย่างปลอดภัยเพราะมันโผล่ในหน้าต่างของชิ้นถัดไปไม่ได้แล้ว (วาดประกอบโดยผู้เขียน)

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

เฉลย ตอนที่ 2 · สมการ

ให้ dp[s] คือถ่านรวมที่มากที่สุด ที่ทำได้เมื่อแจกของไปแล้ว i ชิ้น แล้วลงเอยที่สถานะ s พอดี ถ้าสถานะไหนไปไม่ถึงก็ปล่อยว่างไว้ ส่วนกำไรของการวางของหนึ่งชิ้นเขียนเป็นฟังก์ชันสั้น ๆ ได้แบบนี้

อ่านว่า นับจำนวนสมาชิกที่ไม่ซ้ำกันของ x, y, c โดยตัดช่องว่างทิ้ง ถ้าเหมืองยังไม่เคยได้ของเลย ทั้ง x และ y เป็นช่องว่าง เหลือแค่ c ตัวเดียว ได้ถ่าน 1 ซึ่งตรงกับที่โจทย์บอกว่า "ถ้ายังมีไม่ครบสามชิ้นก็ดูเท่าที่มี"

ทีนี้ของชิ้นถัดไปคือ c ยืนอยู่ที่สถานะเดิมหนึ่งตัว มันมีทางไปแค่สองทาง สมการจึงเขียนแบบผลัก คือยืนที่ต้นทางแล้วส่งค่าออกไปยังปลายทาง (ใช้ dp' แทนตารางของรอบถัดไป และ ⇐ แปลว่าเก็บค่าที่มากกว่าไว้)

สองบรรทัดนี้อ่านเป็นภาษาคนได้ทีละบรรทัด โดยมีหลักปักหมุดคือ y ของเหมืองไหนก็ตาม แปลว่า "ชิ้นล่าสุดของเหมืองนั้น" เสมอ ดังนั้นเหมืองที่เพิ่งรับของ ชิ้นล่าสุดของมันย่อมกลายเป็น c ทันที

  • บรรทัดบน ของเข้าเหมือง 1 คู่ของเหมือง 1 เลื่อนไปหนึ่งช่อง y₁ ที่เคยเป็นชิ้นล่าสุด ถอยไปเป็นชิ้นก่อนหน้า และ c เข้ามาเป็นชิ้นล่าสุด กลายเป็น (y₁, c) ส่วน x₁ ตัวเก่า หลุดออกจากความจำถาวร เพราะมันอยู่ห่างจากชิ้นล่าสุดสามช่องแล้ว ถ่านที่ได้คิดจากคู่ก่อนเลื่อน คือ g(x₁, y₁, c) เพราะหน้าต่างที่คนงานมองคือของใหม่บวกสองชิ้นก่อนหน้าจริง ๆ ส่วนคู่ของเหมือง 2 คัดลอกมาทั้งดุ้น ไม่มีอะไรเกิดขึ้นกับมัน
  • บรรทัดล่าง ของเข้าเหมือง 2 เรื่องเดียวกันเป๊ะ แค่สลับข้าง คู่ของเหมือง 2 เลื่อนเป็น (y₂, c) ถ่านคิดจาก g(x₂, y₂, c) ส่วนคู่ของเหมือง 1 คัดลอกมาเฉย ๆ

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

RECURRENCE
ทางเลือกของชิ้นที่ iสถานะปลายทางถ่านที่บวกเพิ่ม
ส่งเข้าเหมือง 1 (y₁, c), (x₂, y₂) g(x₁, y₁, c)
ส่งเข้าเหมือง 2 (x₁, y₁), (y₂, c) g(x₂, y₂, c)
ทั้งข้อมีแค่สองแถวนี้ ไม่มีคอลัมน์เงื่อนไข เพราะทั้งสองทางใช้ได้เสมอ

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

เดินตารางให้ดูหนึ่งกอง

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

STATE TABLE
เหมือง 1 (x₁ y₁) เหมือง 2 (x₂ y₂) ถ่านรวม

จำนวนสถานะโตขึ้นได้เร็วก็จริง แต่มันชนเพดาน 256 แล้วหยุด ต่อให้กองยาวเป็นแสนชิ้น ตารางนี้ก็สูงไม่เกิน 256 แถวเสมอ

โค้ด C++

แทนที่จะเก็บสถานะเป็นสตริงหรือ map เราอัด (x₁, y₁, x₂, y₂) เป็นเลขฐานสี่สี่หลัก ได้ตัวเลข 0 ถึง 255 พอดี ตารางจึงเป็นอาเรย์ธรรมดายาว 256 ช่อง เข้าถึงด้วยดัชนีตรง ๆ ไม่ต้องแฮชอะไรเลย

หัวใจของทั้งข้อ
// หัวใจของทั้งข้อ: หนึ่งสถานะ แตกได้สองทาง เท่านั้น
int t1 = (y1 << 6) | (f << 4) | (x2 << 2) | y2;   // ของชิ้นนี้เข้าเหมือง 1
int v1 = cur[st] + coal(x1, y1, f);
if (v1 > nxt[t1]) nxt[t1] = v1;

int t2 = (x1 << 6) | (y1 << 4) | (y2 << 2) | f;   // ของชิ้นนี้เข้าเหมือง 2
int v2 = cur[st] + coal(x2, y2, f);
if (v2 > nxt[t2]) nxt[t2] = v2;

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

งานต่อของหนึ่งชิ้นคือวน 256 สถานะ สถานะละสองทาง ทั้งกองจึงเป็น O(256n) ที่ n = 100 000 คือราวห้าสิบล้านครั้งของงานเบา ๆ เครื่องผมวิ่งจบใน 56 มิลลิวินาที จากลิมิต 1.5 วินาที ส่วนหน่วยความจำใช้อาเรย์ int สองชุด ชุดละ 256 ช่อง รวมสองกิโลไบต์ ห่างจากลิมิต 16 เมกะไบต์อยู่มาก

ดูโค้ดเต็ม
miners.cpp
#include <bits/stdc++.h>
using namespace std;

// จำนวนชนิดอาหารที่ไม่ซ้ำกันในสามช่องล่าสุด (0 = ช่องว่าง ไม่นับ)
int coal(int a, int b, int c) {
    bool seen[4] = {false, false, false, false};
    seen[a] = seen[b] = seen[c] = true;
    return (int)seen[1] + (int)seen[2] + (int)seen[3];
}

int code(char c) { return c == 'M' ? 1 : (c == 'F' ? 2 : 3); }

int main() {
    int n;
    string s;
    if (!(cin >> n >> s)) return 0;

    // สถานะคือ (x1, y1, x2, y2) ช่องละ 4 ค่า อัดเป็นเลขฐานสี่สี่หลัก จึงมี 256 ช่อง
    const int S = 256;
    vector<int> cur(S, -1), nxt(S, -1);   // -1 = สถานะนี้ยังไปไม่ถึง (0 เป็นแต้มที่ถูกต้องได้)
    cur[0] = 0;                           // เริ่มต้น เหมืองทั้งสองยังว่าง

    for (int i = 0; i < n; i++) {
        int f = code(s[i]);
        fill(nxt.begin(), nxt.end(), -1); // ปลายทางถูกเขียนซ้ำได้ ต้องล้างก่อนทุกรอบ
        for (int st = 0; st < S; st++) {
            if (cur[st] < 0) continue;
            int x1 = st >> 6, y1 = (st >> 4) & 3, x2 = (st >> 2) & 3, y2 = st & 3;

            // ส่งเข้าเหมือง 1: คู่ล่าสุดของเหมือง 1 เลื่อนเป็น (y1, f) ส่วนเหมือง 2 ไม่ขยับ
            int t1 = (y1 << 6) | (f << 4) | (x2 << 2) | y2;
            int v1 = cur[st] + coal(x1, y1, f);
            if (v1 > nxt[t1]) nxt[t1] = v1;

            // ส่งเข้าเหมือง 2
            int t2 = (x1 << 6) | (y1 << 4) | (y2 << 2) | f;
            int v2 = cur[st] + coal(x2, y2, f);
            if (v2 > nxt[t2]) nxt[t2] = v2;
        }
        cur.swap(nxt);
    }

    cout << *max_element(cur.begin(), cur.end()) << '\n';
    return 0;
}
ของแถม: ตัวตรวจสอบที่ผมใช้ก่อนส่ง

ก่อนเชื่อโค้ดข้างบน ผมสุ่มกองสั้น ๆ ยาวไม่เกิน 12 ชิ้น หกร้อยกอง (บางกองจงใจใช้อาหารแค่ชนิดเดียวหรือสองชนิด) แล้วเทียบคำตอบกับตัวลองทุกแบบข้างล่างนี้ ตรงกันทั้งหมด แล้วค่อยเทียบกับตัวอย่างสองกองในโจทย์อีกที

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

int coal(int a, int b, int c) {
    bool seen[4] = {false, false, false, false};
    seen[a] = seen[b] = seen[c] = true;
    return (int)seen[1] + (int)seen[2] + (int)seen[3];
}
int code(char c) { return c == 'M' ? 1 : (c == 'F' ? 2 : 3); }

int main() {
    int n; string s;
    cin >> n >> s;
    int best = 0;
    for (long long mask = 0; mask < (1LL << n); mask++) {
        vector<int> q[2];
        int tot = 0;
        for (int i = 0; i < n; i++) {
            int f = code(s[i]);
            vector<int>& v = q[mask >> i & 1];
            int a = v.size() >= 2 ? v[v.size() - 2] : 0;
            int b = v.size() >= 1 ? v.back() : 0;
            tot += coal(a, b, f);
            v.push_back(f);
        }
        best = max(best, tot);
    }
    cout << best << '\n';
}

แกะตัวอย่างในโจทย์

ตัวอย่างแรก MBMFFB มีจุดที่ชวนสะดุด เพราะแผนที่ดีที่สุดของมันคือเทของลงเหมืองเดียวทั้งกอง อีกเหมืองไม่ได้แตะเลยแม้แต่ชิ้นเดียว

กอง MBMFFB · แผนที่ดีที่สุด ได้ 12
ชิ้นส่งไปคู่ล่าสุดของเหมืองนั้นได้ถ่านรวม
1. M เหมือง 1 · · 1 1
2. B เหมือง 1 · M 2 3
3. M เหมือง 1 M B 2 5
4. F เหมือง 1 B M 3 8
5. F เหมือง 1 M F 2 10
6. B เหมือง 1 F F 2 12
กองนี้สลับชนิดอาหารมาให้เองอยู่แล้ว การแยกออกไปอีกเหมืองมีแต่จะรีเซ็ตหน้าต่างให้กลับไปเริ่มนับใหม่จากศูนย์

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

ท่าที่ติดมือกลับไป

ท่าที่ข้อนี้สอนมีชื่อว่าการบีบสถานะ (state compression) คือเราไม่เก็บอดีตทั้งกอง เก็บเฉพาะส่วนของอดีตที่ยังมีสิทธิ์เปลี่ยนอนาคต ในข้อนี้คืออาหารสองมื้อล่าสุดของแต่ละเหมือง ที่เหลือทิ้งได้หมดเพราะไม่มีสูตรคะแนนข้อไหนมองย้อนไปไกลกว่านั้น

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

สรุปบรรทัดเดียว

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

แหล่งที่มา

  1. โจทย์ Miners บน programming.in.th ข้อ 2003 programming.in.th/tasks/2003 (สืบค้น 5 กันยายน 2026)
  2. ต้นทางของโจทย์คือ International Olympiad in Informatics 2007 วันแข่งที่สอง จัดที่เมืองซาเกร็บ ประเทศโครเอเชีย 15 ถึง 22 สิงหาคม 2007