programming.in.th · ข้อ 2005

ปิดถนนกันซ้อม: ขอบเขตที่ดูเป็นของแถม คือกุญแจของทั้งข้อ

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

★★★★★ treedpstate compressionparity อ่าน 13 นาที 7 กันยายน 2026

โจทย์ · ตัดถนนให้ไม่เหลือเส้นทางซ้อมที่ยุติธรรม

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

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

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

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

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

EXAMPLE
InputOutput
5 8
2 1 0
3 2 0
4 3 0
5 4 0
1 3 2
3 5 2
2 4 5
2 5 1
5

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

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

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

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

ใบ้

ถนนราดยางเป็นต้นไม้ ดังนั้นถนนไม่ราดยางหนึ่งเส้นที่เชื่อม A กับ B จะสร้างวงขึ้นมาพอดีหนึ่งวง คือเส้นทางบนต้นไม้จาก A ไป B บวกกับตัวมันเอง

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

ค่า 2 · ยาว 2 · วง 3 ค่า 2 · ยาว 2 · วง 3 ค่า 5 · ยาว 2 · วง 3 ค่า 1 · ยาว 3 · วง 4 1 2 3 4 5 เขียวทึบ เก็บไว้ได้จริง แดง วงเป็นเลขคู่ ต้องปิด
ตัวอย่างในโจทย์ เมือง 5 เมืองกับถนนราดยาง 4 สายที่ทำให้เมืองทั้งหมดต่อกันเป็นต้นไม้ ส่วนเส้นโค้งด้านบนคือถนนไม่ราดยาง 4 สาย แต่ละสายเขียนไว้ว่าเส้นทางบนต้นไม้ยาวกี่สาย และวงที่มันสร้างยาวเท่าไร เส้นแดงคือเส้นที่วงเป็นเลขคู่ จึงต้องปิดโดยไม่มีทางเลือก ส่วนเส้นสีจางคือเส้นที่เก็บไว้ได้ตามกฎที่หนึ่ง แต่เส้นทางของมันทับกับเส้นที่คุ้มกว่า จึงไม่ได้เก็บตามกฎที่สอง เหลือเส้นเขียวที่เก็บได้จริง น้ำหนักรวม 5 จากค่าใช้จ่ายทั้งหมด 10 คำตอบจึงเป็น 5 (วาดประกอบโดยผู้เขียน)

ลองเอง · ปิดถนนให้ถูกที่สุด

ห้าเมืองเดียวกับที่บทความใช้

กดปิดถนนที่ไม่ราดยางได้ตามใจ แล้วกดตรวจว่าเหลือเส้นทางซ้อมที่ยุติธรรมไหม

จ่ายไปแล้ว 0 · ถูกที่สุดคือ 0

เส้นทางซ้อมที่ยุติธรรมคือวงจรที่เดินแล้วกลับมาที่เดิม โดยผ่านถนนเป็นจำนวนคู่

ตัวตรวจของเกมนี้ไม่ได้ใช้กฎย่อของเฉลยเลย มันไล่หาวงจรทุกวงจริง ๆ ถ้าเดาแล้วผิด แปลว่ากฎในหัวยังไม่ตรงกับของจริง

เฉลย · เงื่อนไขทั้งข้อยุบเหลือสองกฎ

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

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

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

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

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

ถนนไม่ราดยางเส้นหนึ่งที่มีเส้นทางบนต้นไม้ยาว len สร้างวงยาว len + 1 วงนั้นเป็นเลขคู่เมื่อ len เป็นเลขคี่ ดังนั้น

กฎที่ 1 ถนนที่เส้นทางบนต้นไม้ยาวเป็นเลขคี่ ต้องถูกปิดเสมอ ไม่มีทางเลือก

ส่วนเส้นที่เส้นทางยาวเป็นเลขคู่ จะสร้างวงยาวเป็นเลขคี่ ซึ่งเก็บไว้ได้ แต่ถ้าเราเก็บไว้สองเส้นที่เส้นทาง บนต้นไม้ใช้ถนนราดยางร่วมกันอย่างน้อยหนึ่งเส้น เราจะได้วงคี่สองวงที่มีส่วนซ้อนกัน เอาสองวงมารวมกันแล้วตัดส่วนที่ซ้อนออก จะได้วงใหม่ที่ยาว คี่ + คี่ − 2 × ส่วนซ้อน ซึ่งเป็นเลขคู่เสมอ

กฎที่ 2 ถนนที่เก็บไว้ ต้องมีเส้นทางบนต้นไม้ที่ไม่ทับกันเลยสักเส้น

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

ตัวอย่างในโจทย์ · ถนนไม่ราดยางทั้ง 4 เส้น
ถนนค่าปิดเส้นทางบนต้นไม้ ยาวตัดสิน
1 ถึง 3 2 1 - 2 - 3 2 เก็บได้ ถ้าไม่ทับใคร
3 ถึง 5 2 3 - 4 - 5 2 เก็บได้ ถ้าไม่ทับใคร
2 ถึง 4 5 2 - 3 - 4 2 เก็บได้ ถ้าไม่ทับใคร
2 ถึง 5 1 2 - 3 - 4 - 5 3 ต้องปิดเสมอ (วงยาวคู่)
ค่าปิดรวมทุกเส้นคือ 10 มีเส้นที่ต้องปิดแน่นอน 1 เส้น รวมค่าปิด 1 ที่เหลือคือปัญหาเลือกชุดที่ไม่ทับกัน
เลือกชุดที่ไม่ทับกัน จากเส้นที่เก็บได้ 3 เส้น
ชุดที่เลือกถนนราดยางที่ถูกใช้น้ำหนักรวม
1 ถึง 3 เส้นเดียว 1-2, 2-3 2
3 ถึง 5 เส้นเดียว 3-4, 4-5 2
2 ถึง 4 เส้นเดียว 2-3, 3-4 5
2 ถึง 4 2-3, 3-4 5
แถวล่างคือชุดที่ดีที่สุด คำตอบจึงเป็น 10 ลบ 5 เท่ากับ 5 ตรงกับที่โจทย์เฉลยไว้ และโจทย์ยังบอกเองด้วยว่าถ้าปิดแค่สองเส้นคือ 2-4 กับ 2-5 จะเสีย 6 ซึ่งแพงกว่า

แล้วจะเลือกชุดที่ไม่ทับกันยังไงเมื่อ N เป็นพัน

ตรงนี้แหละที่ขอบเขต "ดีกรีไม่เกิน 10" เข้ามามีบทบาท ปักรากต้นไม้ไว้ที่เมือง 1 แล้วคิดดีพีจากใบขึ้นไปหาราก นิยาม dp[v][mask] ว่าคือน้ำหนักมากที่สุดที่เก็บได้ในต้นไม้ย่อยของ v โดยที่ถนนราดยางที่ต่อ v กับลูกในเซต mask ถูกจองไปแล้วโดยเส้นทางของบรรพบุรุษ

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

หัวใจของทั้งข้อ
// g[mask] = น้ำหนักมากที่สุดในต้นไม้ย่อยของ v เมื่อลูกในเซต mask ถูกจองไปแล้ว
for (int mk = 0; mk < full; mk++) {
    long long s = sum;                         // ค่าเริ่มต้นคือปล่อยลูกทุกคนทำงานของตัวเอง
    for (int j = 0; j < k; j++) if (mk >> j & 1) s -= d0[j];
    g[mk] = s;
}
// ไล่จากเซตใหญ่ไปเซตเล็ก เพราะ g[mk] อ้างถึง g[mk | se] ซึ่งมีบิตมากกว่าเสมอ
for (int mk = full - 1; mk >= 0; mk--)
    for (auto& c : cs)                         // cs = ถนนไม่ราดยางที่มีจุดร่วมสูงสุดอยู่ที่ v
        if (!(c.se & mk)) g[mk] = max(g[mk], c.val + g[mk | c.se]);

ค่าของการเลือกถนนเส้นหนึ่งคือน้ำหนักของมัน บวกกับผลรวมของ dp ของทุกปมที่เส้นทางเดินผ่าน โดยแต่ละปมนั้นถูกจองลูกที่เส้นทางเพิ่งไต่ขึ้นมา ส่วนการไล่หน้ากากจากใหญ่ไปเล็กเป็นเรื่องจำเป็น เพราะสูตรอ้างถึง g[mask | se] ซึ่งมีบิตมากกว่าเสมอ

ทำไมถึงกล้าเชื่อสองกฎนั้น

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

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

ดูโค้ดตัวตรวจสอบ (ไล่หาวงจรทุกวง)
train_check.cpp
// ตัวตรวจสอบที่ไม่เชื่อทฤษฎีอะไรเลย ใช้ได้แค่กราฟจิ๋ว ๆ ตอนสุ่มเทียบก่อนส่ง
// ไล่ทุกวิธีเลือกว่าจะ "เก็บ" ถนนไม่ราดยางเส้นไหนไว้บ้าง แล้วไล่หาวงจรอย่างง่ายทุกวง
// ถ้าเจอวงยาวคู่แม้แต่วงเดียวก็ตัดทิ้ง ที่เหลือค่อยเอาน้ำหนักที่เก็บได้มากที่สุด
#include <bits/stdc++.h>
using namespace std;
int n;
vector<array<int,2>> E;                     // เส้นเชื่อมของกราฟที่กำลังตรวจ
vector<vector<pair<int,int>>> adj;          // (เพื่อนบ้าน, หมายเลขเส้น)
bool found;

void walk(int start, int cur, vector<char>& usedEdge, vector<char>& vis, int len) {
    for (auto& [w, ei] : adj[cur]) {
        if (usedEdge[ei]) continue;
        if (w == start) { if (len + 1 >= 2 && (len + 1) % 2 == 0) found = true; continue; }
        if (vis[w] || w < start) continue;   // บังคับให้ปมเล็กสุดของวงเป็นจุดเริ่ม กันนับซ้ำ
        vis[w] = 1; usedEdge[ei] = 1;
        walk(start, w, usedEdge, vis, len + 1);
        vis[w] = 0; usedEdge[ei] = 0;
        if (found) return;
    }
}
bool hasEvenCycle() {
    found = false;
    for (int s = 1; s <= n && !found; s++) {
        vector<char> usedEdge(E.size(), 0), vis(n + 1, 0);
        vis[s] = 1;
        walk(s, s, usedEdge, vis, 0);
    }
    return found;
}
int main() {
    int m; scanf("%d %d", &n, &m);
    vector<array<int,3>> tree, extra;
    long long total = 0;
    for (int i = 0; i < m; i++) {
        int a, b, c; scanf("%d %d %d", &a, &b, &c);
        if (c == 0) tree.push_back({a, b, c});
        else { extra.push_back({a, b, c}); total += c; }
    }
    long long best = 0;
    for (int mask = 0; mask < (1 << (int)extra.size()); mask++) {
        E.clear();
        for (auto& e : tree) E.push_back({e[0], e[1]});
        long long w = 0;
        for (size_t j = 0; j < extra.size(); j++)
            if (mask >> j & 1) { E.push_back({extra[j][0], extra[j][1]}); w += extra[j][2]; }
        adj.assign(n + 1, {});
        for (size_t i = 0; i < E.size(); i++) {
            adj[E[i][0]].push_back({E[i][1], (int)i});
            adj[E[i][1]].push_back({E[i][0], (int)i});
        }
        if (!hasEvenCycle()) best = max(best, w);
    }
    printf("%lld\n", total - best);
}

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

โค้ด C++

ที่ N ไม่เกิน 1000 และ M ไม่เกิน 5000 การหาปมร่วมที่ต่ำที่สุดไม่ต้องใช้ท่าหรู แค่ไต่ขึ้นตามพ่อทีละชั้นก็พอ และการหาผลรวม dp ตลอดเส้นทางก็ไล่ทีละปมได้ รวมแล้วเป็น M × N ซึ่งคือ 5,000,000 ครั้ง

ส่วนดีพีบนหน้ากากเป็น 2^ดีกรี คูณจำนวนถนนที่มีปมร่วมอยู่ที่ปมนั้น รวมทั้งต้นไม้แล้วไม่เกิน 5,120,000 ครั้ง ทั้งสองก้อนอยู่ในลิมิต 0.3 วินาทีสบาย ๆ

ดูโค้ดเต็ม
train.cpp
#include <bits/stdc++.h>
using namespace std;
int n,mE;
vector<int> ch[1005];              // ลูกของแต่ละปม
int par[1005],dep[1005],idxInPar[1005];
vector<vector<int>> dp;            // dp[v][mask]
struct Ex{ int a,b,w,lca,sa,sb; }; // sa,sb = บิตของลูกที่เส้นทางใช้ (0 ถ้าไม่มี)
vector<Ex> exAt[1005];
vector<pair<int,int>> tadj[1005];

void bfsTree(){
    vector<int> st{1}; par[1]=0; dep[1]=0;
    vector<char> vis(n+1,0); vis[1]=1;
    for(size_t i=0;i<st.size();i++){ int u=st[i];
        for(auto&[v,w]:tadj[u]) if(!vis[v]){ vis[v]=1; par[v]=u; dep[v]=dep[u]+1; idxInPar[v]=(int)ch[u].size(); ch[u].push_back(v); st.push_back(v); }
    }
}
int lcaOf(int a,int b){ while(dep[a]>dep[b]) a=par[a]; while(dep[b]>dep[a]) b=par[b]; while(a!=b){ a=par[a]; b=par[b]; } return a; }
// ผลรวม dp ของปมบนเส้นทางจาก x ขึ้นไปหา v (ไม่รวม v) โดยแต่ละปมห้ามใช้ลูกที่เส้นทางเพิ่งผ่านมา
long long pathSum(int x,int v,int&firstChild){
    long long s=0; int prev=-1; firstChild=0;
    while(x!=v){ int mask = prev<0 ? 0 : (1<<idxInPar[prev]); s+=dp[x][mask]; prev=x; x=par[x]; }
    if(prev>=0) firstChild = 1<<idxInPar[prev];
    return s;
}
int main(){
    scanf("%d %d",&n,&mE);
    vector<array<int,3>> extra;
    long long total=0;
    for(int i=0;i<mE;i++){ int a,b,c; scanf("%d %d %d",&a,&b,&c);
        if(c==0){ tadj[a].push_back({b,0}); tadj[b].push_back({a,0}); }
        else { extra.push_back({a,b,c}); total+=c; } }
    bfsTree();
    for(auto&e:extra){
        int a=e[0],b=e[1],w=e[2],l=lcaOf(a,b);
        int len=dep[a]+dep[b]-2*dep[l];
        if(len%2==1) continue;                       // เส้นทางยาวคี่ วงจะเป็นคู่ ต้องตัดทิ้งเสมอ
        exAt[l].push_back({a,b,w,l,0,0});
    }
    dp.assign(n+1,{});
    // ไล่แบบ post-order ด้วยสแต็ก
    vector<int> order; order.reserve(n); { vector<int> st{1}; while(!st.empty()){ int u=st.back(); st.pop_back(); order.push_back(u); for(int c:ch[u]) st.push_back(c); } }
    for(int i=(int)order.size()-1;i>=0;i--){
        int v=order[i]; int k=(int)ch[v].size(); int full=1<<k;
        vector<long long> d0(k); long long sum=0;
        for(int j=0;j<k;j++){ d0[j]=dp[ch[v][j]][0]; sum+=d0[j]; }
        vector<long long> g(full);
        for(int mk=0;mk<full;mk++){ long long s=sum; for(int j=0;j<k;j++) if(mk>>j&1) s-=d0[j]; g[mk]=s; }
        // เตรียมค่าของเส้นพิเศษที่มี lca = v
        struct Cand{ int se; long long val; };
        vector<Cand> cs;
        for(auto&e:exAt[v]){
            int ca=0,cb=0;
            long long s=pathSum(e.a,v,ca)+pathSum(e.b,v,cb);
            cs.push_back({ca|cb, s+e.w});
        }
        for(int mk=full-1;mk>=0;mk--)
            for(auto&c:cs) if(!(c.se&mk)) g[mk]=max(g[mk],c.val+g[mk|c.se]);
        dp[v]=vector<int>(full);
        for(int mk=0;mk<full;mk++) dp[v][mk]=(int)g[mk];
    }
    printf("%lld\n", total - dp[1][0]);
}

เวลาที่วัดได้บนเครื่องผม ต้นไม้ 1000 เมืองที่ทุกปมมีลูกเก้าคน พร้อมถนนไม่ราดยาง 5000 เส้น ใช้เวลา 51 มิลลิวินาที จากลิมิต 0.3 วินาที

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

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

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

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

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

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

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

แหล่งที่มา

  1. โจทย์ Training บน programming.in.th ข้อ 2005 programming.in.th/tasks/2005 (สืบค้น 6 กันยายน 2026)
  2. ต้นทางของโจทย์คือ International Olympiad in Informatics 2007 ที่เมืองซาเกร็บ ประเทศโครเอเชีย วันแข่งที่ 2