programming.in.th · ข้อ 2008

เครื่องพิมพ์โบราณ: เห็นไทรเมื่อไร ต้นทุนก็ถูกล็อกทันที เหลือตัวแปรให้เลือกตัวเดียว

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

★★★☆☆ triedfsgreedy อ่าน 11 นาที 7 กันยายน 2026

โจทย์ · เครื่องพิมพ์ที่จำคำล่าสุดไว้ให้

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

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

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

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

EXAMPLE
InputOutput
3
print
the
poem
20
t
h
e
P
-
-
-
p
o
e
m
P
-
-
-
r
i
n
t
P

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

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

ของที่เราเลือกเองได้คือลำดับการพิมพ์ ตัวอย่างในโจทย์มี 3 คำ จึงมีลำดับให้เลือก 6 แบบ ไล่ดูให้ครบได้ในกระดาษแผ่นเดียว แต่โจทย์จริงให้คำมาได้ถึง 25,000 คำ จำนวนลำดับคือผลคูณของ 1 คูณ 2 คูณ 3 ไปเรื่อย ๆ จนถึง 25,000 ซึ่งยาวเกินกว่าจะเขียนลงกระดาษ

ชุดทดสอบย่อยที่ให้ 40 คะแนนวางกับดักไว้ตรงนี้พอดี พอเห็นว่า N ไม่เกิน 18 ท่าที่โผล่มาในหัวทันทีคือดีพีบนหน้ากากบิตของเซตคำที่พิมพ์ไปแล้ว ซึ่งคิดเป็นราว 4,718,592 ครั้ง สบายมากในหนึ่งวินาที ปัญหาคือมันตันสนิทตั้งแต่ N เป็น 19 และไม่มีส่วนไหนของมัน ต่อยอดไปหาเฉลยเต็มได้

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

ใบ้

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

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

ลองเอง · กดเครื่องพิมพ์ให้ครบทุกคำ

สองคำที่ใช้ส่วนหัวร่วมกัน ลองดูว่าประหยัดตรงไหน

หน้าจอตอนนี้

(ว่าง)

เติมตัวอักษรจนได้คำที่ต้องการ แล้วกดพิมพ์ ทำจนครบทุกคำในรายการ

ใช้ไปแล้ว 0 คำสั่ง · น้อยที่สุดคือ 0 คำสั่ง

คำสั่งมีสามอย่างคือเติมตัวอักษร ลบตัวท้าย และกดพิมพ์ ทุกอย่างนับเป็นหนึ่งคำสั่งเท่ากันหมด

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

เฉลย · เครื่องพิมพ์เดินอยู่บนไทร

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

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

ที่ทำให้กล้าเชื่อว่ามุมนี้ถูก คือมันไปตรงกับอีกมุมที่คิดทีหลังพอดี ต้นทุนย้ายจากคำ a ไปคำ b เท่ากับ |a| + |b| − 2 × LCP ซึ่งก็คือระยะทางบนไทรเป๊ะ ๆ และปัญหาเดินเยี่ยมให้ครบบนต้นไม้มีสูตรปิด คือสองเท่าของจำนวนเส้น ลบความลึกของจุดที่จบ สองมุมที่ตั้งต้นคนละทางแต่ลงเอยที่เลขเดียวกัน คือหลักฐานที่หนักกว่าการนั่งพิสูจน์มุมเดียวซ้ำ ๆ

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

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

แกะคำศัพท์

trie อ่านว่า "ไทร" หรือบางคนอ่าน "ทราย" ก็มี ชื่อนี้ตัดมาจากกลางคำว่า retrieval ที่แปลว่าการค้นคืน คนตั้งชื่อคือ Edward Fredkin เขาอยากให้อ่านว่า "ทรี" เหมือน tree เพราะมันคือต้นไม้ แต่คนใช้กันจริงกลับอ่าน "ไทร" เพื่อไม่ให้ชนกับคำว่า tree เวลาพูด

t h e p o e m r i n t . t h e p o e m r i n t ราก
ทุกคำสั่งของเครื่องพิมพ์คือการเดินหนึ่งเส้นบนต้นไม้นี้ ปมที่วงไว้คือคำที่ต้องกดพิมพ์ และกิ่งที่ลากเส้นหนาคือกิ่งที่ยอมค้างไว้ตอนจบ จึงเป็นกิ่งเดียวที่ไม่ต้องจ่ายขากลับ (วาดประกอบโดยผู้เขียน)

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

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

ต้นทุนของตัวอย่างในโจทย์
รายการที่มาจำนวนครั้ง
เดินลงทุกเส้นเชื่อมของไทร11
เดินขึ้นทุกเส้น ยกเว้นเส้นที่ค้างไว้ตอนจบ6
กดพิมพ์คำละหนึ่งครั้ง3
รวม2 × 11 − 5 + 320
มีตัวเดียวในสูตรที่เราเลือกได้ คือความลึกของปมที่ยอมค้างไว้ จึงต้องเลือกให้ลึกที่สุด นั่นคือปมของคำที่ยาวที่สุด ในที่นี้คือ print ยาว 5 ตัว

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

หัวใจของทั้งข้อ
void dfs(int u) {
    if (T[u].word) out.push_back('P');          // ปมนี้เป็นคำ ก็กดพิมพ์เสียตรงนี้
    int keepChild = -1;
    for (int c = 0; c < 26; c++) {
        int v = T[u].ch[c];
        if (v < 0) continue;
        if (T[v].keep) { keepChild = c; continue; }   // กิ่งของคำยาวสุด เก็บไว้ทีหลัง
        out.push_back('a' + c);
        dfs(v);
        out.push_back('-');                     // กิ่งธรรมดา ลงไปแล้วต้องถอยกลับขึ้นมา
    }
    if (keepChild >= 0) {                       // ลงกิ่งสุดท้ายแล้วค้างไว้เลย ไม่ต้องถอย
        out.push_back('a' + keepChild);
        dfs(T[u].ch[keepChild]);
    }
}

ถ้าเลือกกิ่งค้างผิด จะแพงขึ้นเท่าไร

ลองเปลี่ยนไปค้าง the ที่ยาว 3 ตัวแทน ทุกอย่างเหมือนเดิมหมด ต่างแค่ตอนจบเราหยุดอยู่ตื้นกว่า

เทียบสองทางเลือกบนคำชุดเดียวกัน
ค้างคำไหนไว้ความลึกจำนวนคำสั่ง
print (ยาวที่สุด)520
the322
ต่างกัน 2 ครั้งพอดี ซึ่งเท่ากับผลต่างของความลึกเป๊ะ ๆ ตัวเลขนี้ไม่ได้พิมพ์ไว้ แต่มาจากการเดินไทรจริงทั้งสองแบบแล้วนับคำสั่ง

อีกมุมหนึ่ง: มองเป็นปัญหาเดินเยี่ยมให้ครบ (ชุดทดสอบย่อย N ≤ 18)

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

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

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

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

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

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

cost(a, b) = |a| + |b| − 2 × ความยาวส่วนหัวที่เหมือนกัน

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

จำนวนสถานะคือ 2^18 × 18 ราว 4,718,592 สถานะ ซึ่งไหวสบายในหนึ่งวินาที แต่พอ N ขึ้นไปถึง 25,000 มันตายทันที

ดูโค้ดของชุดทดสอบย่อย
printer_sub.cpp
// เฉลยชุดทดสอบย่อย N ไม่เกิน 18 (40 คะแนน)
// มองเป็นปัญหาเดินเยี่ยมทุกคำให้ครบโดยเสียก้าวน้อยที่สุด แล้วยกดีพีบิตมาสก์มาใช้ตรง ๆ
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n; scanf("%d", &n);
    vector<string> w(n);
    for (auto& s : w) { char b[64]; scanf("%s", b); s = b; }

    auto lcp = [&](const string& a, const string& b) {
        size_t i = 0; while (i < a.size() && i < b.size() && a[i] == b[i]) i++; return (int)i;
    };
    // ค่าเดินจากคำ a ไปคำ b คือถอยจนถึงส่วนหัวที่เหมือนกัน แล้วเติมส่วนที่ต่าง
    vector<vector<int>> cost(n, vector<int>(n, 0));
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cost[i][j] = (int)w[i].size() + (int)w[j].size() - 2 * lcp(w[i], w[j]);

    const int FULL = 1 << n, INF = 1e9;
    vector<vector<int>> dp(FULL, vector<int>(n, INF));
    for (int i = 0; i < n; i++) dp[1 << i][i] = (int)w[i].size();   // จากเครื่องเปล่าไปคำแรก
    for (int m = 1; m < FULL; m++)
        for (int i = 0; i < n; i++) {
            if (dp[m][i] == INF || !(m >> i & 1)) continue;
            for (int j = 0; j < n; j++) {
                if (m >> j & 1) continue;
                int nm = m | (1 << j);
                dp[nm][j] = min(dp[nm][j], dp[m][i] + cost[i][j]);
            }
        }
    int best = INF;
    for (int i = 0; i < n; i++) best = min(best, dp[FULL - 1][i]);
    printf("%d\n", best + n);                                       // บวก n เพราะต้องกดพิมพ์ทุกคำ
}

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

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

ไล่คำสั่งทีละก้าว

กดถัดไปเพื่อไล่คำสั่งทีละครั้ง

สถานะของเครื่องพิมพ์ตลอด 20 คำสั่ง
ครั้งที่คำสั่งในเครื่องพิมพ์ออกมา
1 t t
2 h th
3 e the
4 P the the
5 - th
6 - t
7 - (ว่าง)
8 p p
9 o po
10 e poe
11 m poem
12 P poem poem
13 - poe
14 - po
15 - p
16 r pr
17 i pri
18 n prin
19 t print
20 P print print

สังเกตท้ายตาราง เครื่องจบด้วย print ค้างอยู่ ไม่มีคำสั่งลบตามหลังอีกเลย นั่นคือ 5 ครั้งที่เราประหยัดได้

บิลค่าใช้จ่าย · เทียบสองทางด้วยหน่วยเดียวกัน
ทางขอบเขตที่มันไหวงานคร่าว ๆ
มองเป็นปัญหาเดินเยี่ยมให้ครบ N ไม่เกิน 18 84,934,656 ครั้ง
เดินบนไทร N ถึง 25,000 500,000 ครั้ง
แถวบนคือการจำว่าเคยพิมพ์คำชุดไหนไปแล้ว ซึ่งเป็นหน้ากากบิตของคำทั้งหมด จำนวนสถานะจึงเป็นสองยกกำลัง N แถวล่างไม่ต้องจำอะไรแบบนั้นเลย เพราะไทรบอกอยู่แล้วว่าจากจุดนี้เดินต่อไปไหนได้ ต่างกันตรงที่แถวบนโตแบบทวีคูณตามจำนวนคำ ส่วนแถวล่างโตตามความยาวตัวอักษรรวม

โค้ด C++

ไทรใหญ่สุดได้ราว 500,000 ปม แต่ละปมเก็บลูก 26 ช่อง ก็คือราว 50 เมกะไบต์ ซึ่งยังอยู่ในลิมิต 64 เมกะไบต์ จริง ๆ คำที่ใช้ร่วมกันเยอะจะทำให้ปมน้อยกว่านี้มาก

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

ดูโค้ดเต็ม
printer.cpp
#include <bits/stdc++.h>
using namespace std;
struct Node{ int ch[26]; bool word; bool keep; Node(){ memset(ch,-1,sizeof ch); word=keep=false; } };
vector<Node> T;
string out;
int newNode(){ T.push_back(Node()); return (int)T.size()-1; }
void dfs(int u){
    if(T[u].word) out.push_back('P');
    int keepChild=-1;
    for(int c=0;c<26;c++){
        int v=T[u].ch[c]; if(v<0) continue;
        if(T[v].keep){ keepChild=c; continue; }
        out.push_back('a'+c); dfs(v); out.push_back('-');
    }
    if(keepChild>=0){ out.push_back('a'+keepChild); dfs(T[u].ch[keepChild]); }
}
int main(){
    int n; if(scanf("%d",&n)!=1) return 0;
    T.reserve(500005); newNode();
    string longest; char buf[64];
    for(int i=0;i<n;i++){
        scanf("%s",buf); string w=buf;
        if(w.size()>longest.size()) longest=w;
        int cur=0;
        for(char c:w){ int k=c-'a'; if(T[cur].ch[k]<0){ int v=newNode(); T[cur].ch[k]=v; } cur=T[cur].ch[k]; }
        T[cur].word=true;
    }
    { int cur=0; for(char c:longest){ cur=T[cur].ch[c-'a']; T[cur].keep=true; } }
    dfs(0);
    printf("%d\n",(int)out.size());
    string res; res.reserve(out.size()*2);
    for(char c:out){ res.push_back(c); res.push_back('\n'); }
    fputs(res.c_str(),stdout);
}

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

เวลาที่วัดได้บนเครื่องผม อินพุต 25,000 คำ คำละ 15 ถึง 20 ตัวอักษร ใช้เวลา 76 มิลลิวินาที จากลิมิตหนึ่งวินาที

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

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

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

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

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

แหล่งที่มา

  1. โจทย์ Type Printer บน programming.in.th ข้อ 2008 programming.in.th/tasks/2008 (สืบค้น 6 กันยายน 2026)
  2. ต้นทางของโจทย์คือ 20th International Olympiad in Informatics ที่กรุงไคโร ประเทศอียิปต์ วันแข่งที่ 1