programming.in.th · ข้อ 2009

เกาะกับสะพาน: เมื่อรูปของกราฟถูกล็อกไว้แล้ว เส้นทางยาวสุดเหลือแค่สองทรง

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

★★★★★ functional graphtreedp อ่าน 16 นาที เผยแพร่ 7 กันยายน 2026 อัปเดต 19 ตุลาคม 2026

โจทย์ · เดินให้ไกลที่สุด แต่ห้ามซ้ำเกาะ

สวนสาธารณะมีเกาะ N เกาะ จากเกาะ i มีสะพานหนึ่งสะพานทอดไปยังอีกเกาะหนึ่ง ยาว L ทั้งสวนจึงมีสะพาน N สะพานพอดี สะพานเดินได้สองทาง และระหว่างเกาะคู่ไหนก็มีเรือแล่นถึงกันได้

เราอยากให้ผลรวมความยาวสะพานที่เดินข้ามมากที่สุด ภายใต้กฎว่า

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

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

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

EXAMPLE
InputOutput
7
3 8
7 2
4 2
1 4
1 9
3 4
2 3
24

ระหว่างเกาะ 2 กับเกาะ 7 มีสะพานสองเส้น (ยาว 2 ที่ออกจากเกาะ 2 และยาว 3 ที่ออกจากเกาะ 7) นับเป็นคนละสะพาน

ใบ้

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

พอเห็นรูปแล้ว ลองแยกว่า "เส้นทางที่ยาวที่สุด" ในรูปแบบนั้นมีได้กี่ทรง

ลองเอง · เดินให้ไกลที่สุดของทุกกลุ่ม

สี่เกาะกลุ่มเดียว พอให้ชินกับการเดินไม่ซ้ำเกาะ

เส้นทางตอนนี้ ยังไม่ได้เริ่มเดิน ยาว 0

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

รวมที่บันทึกไว้ 0 จากคำตอบเต็ม 0

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

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

เฉลย ตอนที่ 1 · รูปของแผนที่: ทุกกลุ่มคือวงหนึ่งวงที่มีต้นไม้ห้อย

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

ทุกเกาะมีลูกศรออกเส้นเดียว เดินตามลูกศรไปเรื่อย ๆ เกาะมีจำกัด สักวันต้องวนกลับมาเจอเกาะที่เคยผ่าน ทางที่วนนั้นคือวง (cycle อ่านว่า "ไซเคิล")

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

กลุ่มที่มี k เกาะจะมีสะพาน k เส้นพอดี (สะพานของแต่ละเกาะอยู่ในกลุ่มของมันเอง) จึงมีวงได้แค่วงเดียว

กลุ่มที่ 1 กลุ่มที่ 2 82494 23 1234567 วง
แผนที่ตัวอย่างในโจทย์ ลูกศรชี้ตามทิศของสะพาน เกาะขอบทองอยู่บนวง เส้นเขียวหนาคือทางที่ยาวที่สุดของแต่ละกลุ่ม ยาว 21 และ 3

จากรูป ทางที่ยาวที่สุดในกลุ่มหนึ่งมีได้แค่สองทรง

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

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

สะพานคู่ของเกาะ 2 กับ 7

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

เฉลย ตอนที่ 2 · เริ่มจากท่าที่ช้าแต่ถูก แล้ววัดว่าขาดไปเท่าไร

รู้รูปแล้ว ไล่จากวิธีที่ตรงที่สุดก่อน โจทย์แบ่งคะแนนไว้สองชุด

ชุดคะแนนของโจทย์
คะแนนเงื่อนไขของชุด
40N ไม่เกิน 4,000
60ขอบเขตเต็ม N ไม่เกิน 1,000,000

วิธีที่ 1

ไล่ทุกทางที่ไม่ซ้ำเกาะ

โตแบบทวีคูณ

ลองเดินทุกทางจากทุกเกาะ ใช้ได้แค่สวนไม่กี่เกาะ

ใช้เป็นตัวตรวจ

วิธีที่ 2

ตัดสะพานบนวงทีละเส้น

ราว c × ขนาดกลุ่ม แย่สุด N²

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

ได้ 40 คะแนน

วิธีเต็ม

ปอกใบ + หน้าต่างเลื่อน

เชิงเส้น ราว 8N ก้าว

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

ได้ครบ 100 คะแนน

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

งั้นลองตัดสะพานบนวงทิ้งทีละเส้น ตัดแล้วกลุ่มกลายเป็นต้นไม้ (tree) ทันที ทางที่ยาวที่สุดในต้นไม้มีชื่อว่าเส้นผ่านศูนย์กลาง (diameter อ่านว่า "ไดแอมิเทอร์")

เส้นผ่านศูนย์กลางของต้นไม้หาได้ด้วยการเดินทั่วต้นไม้สองรอบ วิธีและเหตุผลอยู่ในหัวข้อท้ายหน้า (อีกท่าของเส้นผ่านศูนย์กลาง) สำหรับตอนนี้รู้แค่ว่าแต่ละครั้งที่ตัด เราเดินทั่วกลุ่มสองรอบ

ดูโค้ดของชุด 40 คะแนน
islands_sub.cpp
// เฉลยชุดทดสอบย่อย N ไม่เกิน 4,000 (40 คะแนน)
// ความคิดหลัก: ตัดเส้นบนวงออกทีละเส้น กลุ่มนั้นจะกลายเป็นต้นไม้ทันที
// แล้วเส้นทางยาวสุดในต้นไม้ก็คือเส้นผ่านศูนย์กลาง ซึ่งหาได้ด้วยการค้นสองรอบ
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
struct E { int to, w, id; };
vector<vector<E>> g;                                // กราฟไม่มีทิศ สะพานสองเส้นระหว่างเกาะคู่เดียวกันเป็นคนละเส้น จึงต้องมีหมายเลขกำกับ

pair<int,ll> farthest(int s, const vector<char>& alive, int banId) {
    vector<ll> d(n + 1, -1);
    d[s] = 0;
    vector<int> st{s};
    int bi = s; ll bd = 0;
    while (!st.empty()) {
        int u = st.back(); st.pop_back();
        for (auto& e : g[u]) {
            int v = e.to;
            if (!alive[v] || d[v] >= 0 || e.id == banId) continue;   // ข้ามสะพานเส้นที่ตัดทิ้ง
            d[v] = d[u] + e.w;
            if (d[v] > bd) { bd = d[v]; bi = v; }
            st.push_back(v);
        }
    }
    return {bi, bd};
}

int main() {
    scanf("%d", &n);
    vector<int> f(n + 1), L(n + 1);
    g.assign(n + 1, {});
    for (int i = 1; i <= n; i++) {
        scanf("%d %d", &f[i], &L[i]);
        g[i].push_back({f[i], L[i], i});
        g[f[i]].push_back({i, L[i], i});
    }
    vector<char> seen(n + 1, 0);
    ll ans = 0;
    for (int s = 1; s <= n; s++) {
        if (seen[s]) continue;
        // เก็บสมาชิกของกลุ่มนี้
        vector<int> comp; vector<char> alive(n + 1, 0);
        vector<int> st{s}; seen[s] = 1;
        while (!st.empty()) {
            int u = st.back(); st.pop_back(); comp.push_back(u); alive[u] = 1;
            for (auto& e : g[u]) if (!seen[e.to]) { seen[e.to] = 1; st.push_back(e.to); }
        }
        // หาวงด้วยการเดินตามลูกศรจนวนกลับมาที่เดิม
        vector<int> mark(n + 1, 0);
        int u = s; while (!mark[u]) { mark[u] = 1; u = f[u]; }
        vector<int> cyc; int v = u;
        do { cyc.push_back(v); v = f[v]; } while (v != u);
        ll best = 0;
        for (size_t i = 0; i < cyc.size(); i++) {
            int a = cyc[i];                           // สะพานหมายเลข a คือเส้นที่ออกจากเกาะ a
            auto p1 = farthest(a, alive, a);          // ตัดสะพานนั้นแล้วหาเส้นผ่านศูนย์กลางของต้นไม้
            auto p2 = farthest(p1.first, alive, a);
            best = max(best, p2.second);
        }
        ans += best;
    }
    printf("%lld\n", ans);
}

เรื่องจริงที่เกิดขึ้นตามลำดับ

ตอนเขียนท่านี้ ผมเขียนโค้ดที่ตัด "สะพานระหว่างเกาะคู่นี้" แทนที่จะตัด "สะพานหมายเลขนี้" ซึ่งฟังดูเหมือนกันจนกว่าจะเจอเกาะสองเกาะที่มีสะพานถึงกันสองเส้น

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

ตั้งแต่นั้นมา เมื่อกราฟมีเส้นซ้ำได้ ผมอ้างถึงเส้นด้วยหมายเลขเส้นเสมอ

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

1061071081091010101110121013 ตัดสะพานบนวง · N = 4,000ตัดสะพานบนวง · N = 1,000,000เฉลยเต็ม · N = 1,000,000 1.6×107 ทัน1×1012 เกินงบราว 4,500 เท่า8×106 ทัน งบ 2.2 วินาที
จำนวนก้าวโดยประมาณ สเกลแบบ log (ขีดละสิบเท่า) เส้นทองคืองบ 2.2 วินาที โดยคิดหยาบ ๆ ว่าเครื่องทำได้ราว 10⁸ ก้าวต่อวินาที

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

  • งบ: 2.2 วินาที × 10⁸ ก้าว ≈ 220,000,000 ก้าว
  • N = 4,000: 4,000² = 16,000,000 ก้าว ทันสบาย
  • N = 1,000,000: 1,000,000² = 10¹² ก้าว เกินงบราว 4,500 เท่า
  • เฉลยเต็ม: ปอกใบ 1N + เดินวงที่คลี่ 2N + เข้าออกคิวสองหัว 4N + แจกหมายเลขกลุ่ม 1N = 8N ≈ 8,000,000 ก้าว

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

เฉลย ตอนที่ 3 · ทรงจมในต้นไม้: ปอกใบทิ้งทีละชั้น

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

ค่าที่เก็บไว้ที่ทุกเกาะ v

  • t1[v] ระยะไกลสุดจาก v ลงไปในต้นไม้ที่ห้อยเข้าหา v (กิ่งที่ลึกที่สุด)
  • t2[v] กิ่งที่ลึกรองลงมา โดยต้องลงทางเกาะลูกคนละเกาะกับ t1[v]
  • ทางทรงจมในต้นไม้ที่มียอดอยู่ที่ v ยาวสุด t1[v] + t2[v]

ถ้าเกาะ u มีลูกศรชี้ไปเกาะ p เรียก p ว่าพ่อ และ u ว่าลูก (ลูกอยู่ไกลวงกว่า)

จะได้ t1 กับ t2 ของเกาะหนึ่ง ต้องรู้ค่าของลูกทุกตัวก่อน วิธีที่ทำได้ในรอบเดียวคือปอกใบ เหมือนปอกหัวหอมทีละชั้นจากนอกเข้าใน ใบ (leaf) ในที่นี้คือเกาะที่ไม่มีลูกศรชี้เข้าเลย

  1. นับว่าแต่ละเกาะมีลูกศรชี้เข้ากี่เส้น ใส่เกาะที่นับได้ศูนย์ลงคิว
  2. หยิบเกาะ u ออกจากคิว ส่งกิ่ง t1[u] + ความยาวสะพานของ u ขึ้นไปให้พ่อ (บวกสะพานเพราะกิ่งที่มองจากพ่อต้องเดินข้ามสะพานนั้นลงมาก่อน) ถ้ายาวกว่า t1 หรือ t2 ของพ่อก็แทนที่
  3. ลดตัวนับของเกาะนั้นลงหนึ่ง ถ้าเหลือศูนย์ แปลว่าลูกครบทุกตัวแล้ว ใส่มันลงคิว

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

เกาะที่เข้าคิวแล้ว ลูกของมันส่งค่ามาครบทุกตัว t1 กับ t2 จึงไม่เปลี่ยนอีก และ t2 มาจากลูกคนละตัวกับ t1 เองโดยอัตโนมัติ เพราะลูกแต่ละตัวส่งค่าขึ้นมาแค่ครั้งเดียว

ปอกตัวอย่างในโจทย์ทีละเกาะ

  • หยิบเกาะ 5: ส่ง t1 ของมัน (0) + สะพาน 5 ไป 1 (9) = 9 ขึ้นไปเกาะ 1 t1 ของเกาะ 1 จึงเป็น 9 · ตัวนับของเกาะ 1 เหลือ 1
  • หยิบเกาะ 6: ส่ง t1 ของมัน (0) + สะพาน 6 ไป 3 (4) = 4 ขึ้นไปเกาะ 3 t1 ของเกาะ 3 จึงเป็น 4 · ตัวนับของเกาะ 3 เหลือ 1

ชุดที่ 4 ของเกมลึกกว่า และเห็นตอนที่ t1 เดิมถอยไปเป็น t2

  • หยิบเกาะ 1: ส่ง t1 ของมัน (0) + สะพาน 1 ไป 3 (3) = 3 ขึ้นไปเกาะ 3 t1 ของเกาะ 3 จึงเป็น 3 · ตัวนับของเกาะ 3 เหลือ 0 จึงเข้าคิว
  • หยิบเกาะ 5: ส่ง t1 ของมัน (0) + สะพาน 5 ไป 4 (5) = 5 ขึ้นไปเกาะ 4 t1 ของเกาะ 4 จึงเป็น 5 · ตัวนับของเกาะ 4 เหลือ 0 จึงเข้าคิว
  • หยิบเกาะ 3: ส่ง t1 ของมัน (3) + สะพาน 3 ไป 6 (3) = 6 ขึ้นไปเกาะ 6 t1 ของเกาะ 6 จึงเป็น 6 · ตัวนับของเกาะ 6 เหลือ 2
  • หยิบเกาะ 4: ส่ง t1 ของมัน (5) + สะพาน 4 ไป 6 (10) = 15 ขึ้นไปเกาะ 6 ยาวกว่า t1 เดิม (6) จึงขึ้นเป็น t1 = 15 และ 6 ถอยไปเป็น t2 · ตัวนับของเกาะ 6 เหลือ 1

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

การปอกใบทำสองงานในรอบเดียว คือเก็บ t1 กับ t2 อย่างที่เพิ่งเล่า และหาวงไปด้วย ที่เลือกท่านี้ก็เพราะงานที่สอง

ท่าที่คนส่วนใหญ่คิดออกก่อน (ผมด้วย) คือเดินตามลูกศรไปเรื่อย ๆ แล้วทำเครื่องหมายไว้ จนกว่าจะเจอเกาะที่เคยเจอแล้ว

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

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

ตัวอย่างในโจทย์ หลังปอกใบ
เกาะอยู่บนวงt1t2t1 + t2
1 ใช่ 9 0 9
2 ใช่ 0 0 0
3 ใช่ 4 0 4
4 ใช่ 0 0 0
5 ไม่ 0 0 0
6 ไม่ 0 0 0
7 ใช่ 0 0 0
ปอกออกไป 2 เกาะ คือเกาะ 5, 6 ที่เหลือคือ 1, 3, 4 และ 2, 7 ซึ่งเป็นวงของสองกลุ่ม ทรงจมในต้นไม้ที่ดีที่สุดของแต่ละกลุ่มคือ 9 (ยอดที่เกาะ 1) และ 0 (ยอดที่เกาะ 2)

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

เฉลย ตอนที่ 4 · ทรงข้ามวง: คลี่วงเป็นสองรอบ แล้วใช้หน้าต่างเลื่อน

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

ชื่อที่ใช้กับวงยาว c

  • เรียงเกาะบนวงตามลูกศร เป็นตำแหน่ง 0 ถึง c − 1
  • d[i] ความลึกของต้นไม้ที่ห้อยจากเกาะตำแหน่ง i (คือ t1 ของเกาะนั้น)
  • P[i] ระยะสะสม คือผลรวมความยาวสะพานบนวงจากตำแหน่ง 0 ไต่ตามลูกศรมาถึง i

ทางที่ลงต้นไม้ที่ตำแหน่ง i แล้วไต่ตามลูกศรไปถึงตำแหน่ง j ยาวเท่ากับ

ความยาวของทางทรงข้ามวง

d[i] + (P[j] − P[i]) + d[j]

ลองกับกลุ่มที่ 1 ของตัวอย่าง

  • คำถาม: ลงต้นไม้ที่เกาะ 1 ไต่ตามลูกศรไปเกาะ 3 แล้วลงต้นไม้ที่เกาะ 3 ได้เท่าไร
  • แทนค่า: d ของเกาะ 1 = 9, ไต่จากเกาะ 1 ไป 3 = 8, d ของเกาะ 3 = 4
  • คำตอบ: 9 + 8 + 4 = 21 ตรงกับการนับทาง 5-1-3-6 ตรง ๆ ในรูปแผนที่ตัวอย่าง

ปัญหาแรกคือทางที่ไต่ข้ามจุดเริ่ม เช่นไต่จากตำแหน่ง 2 วนกลับไปตำแหน่ง 0 ค่า P[j] − P[i] จะติดลบ ทั้งที่ระยะจริงเป็นบวก

นึกภาพ

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

เราทำแบบเดียวกัน คือคลี่วงเป็นสองรอบ ได้แถวยาว 2c ตำแหน่ง k คือเกาะเดียวกับตำแหน่ง k mod c (เศษของ k หารด้วย c) และ P สะสมต่อไปเรื่อย ๆ ไม่วนกลับเป็นศูนย์

กลุ่มที่ 1 คลี่เป็นสองรอบ (c = 3)
ตำแหน่ง k012345
เกาะ134134
d940940
P0810142224

ทุกทางทรงข้ามวงตอนนี้คือคู่ตำแหน่ง i < j บนแถวนี้ ที่ห่างกันไม่เกิน c − 1 เช่นไต่จากเกาะ 4 (ตำแหน่ง 2) ข้ามจุดเริ่มไปเกาะ 1 ก็คือคู่ตำแหน่ง 2 กับ 3 ได้ P[3] − P[2] = 4 เป็นบวกแล้ว

ระวัง

ระยะห่างต้องไม่เกิน c − 1 เพราะทางที่ไม่ซ้ำเกาะไต่วงได้มากที่สุด c − 1 สะพาน ถ้าปล่อยถึง c โปรแกรมจะยอมให้ไต่ครบรอบแล้วกลับมาที่เดิม ซึ่งผิดกติกา และมันจะตอบเกินจริงแบบเงียบ ๆ

ไล่ทุกคู่ตรง ๆ ยังเป็น c² อยู่ดี และวงยาวได้ถึงล้าน ต้องตัดอีกชั้น โดยจับสูตรกลุ่มใหม่ แยกส่วนที่เป็นของ j ออกจากส่วนที่เป็นของ i

จับกลุ่มใหม่

(d[j] + P[j]) + (d[i] − P[i])

เมื่อเลือก j แล้ว ก้อนแรกเป็นค่าคงที่ เหลือแค่หาค่ามากสุดของ d[i] − P[i] ในช่วง i = j − (c − 1) ถึง j − 1

ช่วงนี้ขยับไปทางขวาทีละช่องตาม j เรียกว่าหน้าต่างเลื่อน (sliding window อ่านว่า "สไลดิง วินโดว์")

ค่ามากสุดในหน้าต่างเลื่อนหาได้ด้วยคิวที่เข้าออกได้สองหัว เรียกว่า deque (อ่านว่า "เด็ค" ย่อจาก double-ended queue) อ่านเรื่องนี้แบบละเอียดได้ที่บทหน้าต่างเลื่อน

นึกภาพ

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

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

  1. ทิ้งหัวคิวที่หลุดหน้าต่างแล้ว (ห่างจาก j เกิน c − 1)
  2. หัวคิวคือ i ที่มี d − P มากที่สุด ใช้มันคิดค่าของคู่ที่จบที่ j
  3. ใส่ j ลงท้ายคิว แต่ก่อนใส่ ไล่ตัวท้ายที่ d − P ไม่มากกว่าของ j ออกให้หมด

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

กลุ่มที่ 1 คลี่เป็น 6 ตำแหน่ง หน้าต่างยาว 2

k = 0k = 1k = 2k = 3k = 4k = 5 เกาะ 1เกาะ 3เกาะ 4เกาะ 1เกาะ 3เกาะ 4 d = 9d = 4d = 0d = 9d = 4d = 0 P = 0P = 8P = 10P = 14P = 22P = 24 รอบแรกรอบสอง
kเกาะd + Pd − P คู่ที่ดีที่สุดที่จบที่ kค่า
0 1 9 9 ยังไม่มีคู่ ·
1 3 12 −4 เกาะ 1 ไป 3 21
2 4 10 −10 เกาะ 1 ไป 4 19
3 1 23 −5 เกาะ 3 ไป 1 19
4 3 26 −18 เกาะ 1 ไป 3 21
5 4 24 −24 เกาะ 1 ไป 4 19

กลุ่มที่ 2 มีวงสองเกาะ (2 กับ 7) ไม่มีต้นไม้ห้อย หน้าต่างยาวแค่ 1 คู่ที่ดีที่สุดคือไต่สะพานยาว 3 ได้ 3

หน้าต่างเลื่อนบนวงที่คลี่
// ค่าของเส้นทางที่ลงต้นไม้ที่ i ไต่วงไปทางเดียวจนถึง j แล้วลงต้นไม้ที่ j คือ
//     d[i] + d[j] + (P[j] - P[i])   =   (d[j] + P[j]) + (d[i] - P[i])
// ก้อนหลังไม่ขึ้นกับ j เลย จึงใช้หน้าต่างเลื่อนเก็บค่ามากสุดของมันได้
deque<int> dq;
for (int j = 0; j < 2 * c; j++) {
    while (!dq.empty() && dq.front() < j - (c - 1)) dq.pop_front();   // ไต่วงเกินรอบไม่ได้
    if (!dq.empty())
        local = max(local, d[j % c] + P[j] + d[dq.front() % c] - P[dq.front()]);
    ll cur = d[j % c] - P[j];
    while (!dq.empty() && d[dq.back() % c] - P[dq.back()] <= cur) dq.pop_back();
    dq.push_back(j);
}

เฉลย ตอนที่ 5 · รวมสองทรง และบักที่ตัวอย่างในโจทย์ไม่จับ

เรื่องจริงที่เกิดขึ้นตามลำดับ

โค้ดรอบแรกของผมคำนวณทรงจมในต้นไม้ไว้เรียบร้อย เก็บใส่ตัวแปร best แล้วไม่ได้เอาไปใช้เลยสักบรรทัด สิ่งที่เหลือไว้คือ (void)best; ไม่มี error ไม่มี warning

ตัวอย่างในโจทย์ตอบถูก 24 ตรงเป๊ะ เพราะทุกกลุ่มในตัวอย่างชนะด้วยทรงข้ามวงอยู่แล้ว ตัวสุ่มเทียบจับได้ที่แผนที่ 6 เกาะ โปรแกรมตอบ 18 ของจริงคือ 21

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

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

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

ตัวอย่างในโจทย์ รวมสองทรง
กลุ่มทรงข้ามวงทรงจมในต้นไม้คำตอบของกลุ่ม
1 21 9 21
2 3 0 3
รวมทุกกลุ่มได้ 21 + 3 = 24 ตรงกับคำตอบในโจทย์
ดูโค้ดที่ผมเขียนผิดรอบแรก และเคสที่จับมันได้

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

islands_wrong.cpp · เฉพาะท้ายฟังก์ชัน
    // ... ปอกใบ เก็บ t1/t2 และหาคำตอบฝั่งที่ข้ามวงมาเรียบร้อยแล้ว ...

    for(int h=(int)q.size()-1;h>=0;h--) comp[q[h]]=comp[f[q[h]]];   // แจกหมายเลขกลุ่มลงต้นไม้ที่ห้อยอยู่

    ll best=0;
    for(int u=1;u<=n;u++) best=max(best,t1[u]+t2[u]);   // เส้นทางที่จมอยู่ในต้นไม้ห้อย คิดถูกแล้ว
    (void)best;                                        // <-- บรรทัดที่ผิด: คิดเสร็จแล้วทิ้ง ไม่ได้เอาไปสู้กับ res เลย

    ll ans=0; for(ll v:res) ans+=v;                    // res มีแต่คำตอบฝั่งที่ข้ามวง
    printf("%lld\n",ans);
}

เคสที่ตัวสุ่มเทียบเจอว่าสองเวอร์ชันตอบไม่ตรงกัน มี 6 เกาะ (เป็นแผนที่เดียวกับชุดที่ 4 ของเกม)

INPUT
6
3 3
6 3
6 3
6 10
4 5
2 1

วงของกลุ่มนี้คือเกาะ 2 กับเกาะ 6 ที่เหลือห้อยอยู่ใต้เกาะ 6 ทั้งหมด ทางที่ยาวที่สุดจริงคือ 5-4-6-3-1 ยาว 21 มันลงกิ่งหนึ่งของเกาะ 6 แล้วลงอีกกิ่ง โดยไม่แตะสะพานบนวงสักเส้น

เวอร์ชันที่ทิ้ง best มองไม่เห็นทางนี้ จึงได้ทางที่ดีที่สุดของทรงข้ามวงแทน คือ 2-6-4-5 ยาว 18

ไล่ดูทุกแผนที่เล็ก ๆ แล้ว เคสที่เล็กที่สุดมี 4 เกาะ: วงเกาะ 1 กับ 2 และเกาะ 3 กับ 4 ห้อยเข้าเกาะ 1

ทรงจมในต้นไม้ 3-1-4 ยาว 11 ส่วนทรงข้ามวงได้แค่ 8 แผนที่สามเกาะทำให้บักนี้โผล่ไม่ได้ เพราะต้นไม้ห้อยมีได้แค่กิ่งเดียว และทรงข้ามวงต่อกิ่งนั้นด้วยสะพานบนวงได้ยาวกว่าเสมอ

INPUT · 4 เกาะ
4
2 2
1 1
1 6
1 5

โค้ด C++

ที่ N เป็นล้าน ทุกอย่างต้องเป็นอาเรย์แบน ๆ และห้ามเรียกฟังก์ชันตัวเองซ้อนกัน (recursion อ่านว่า "รีเคอร์ชัน") เพราะต้นไม้ที่ลึกล้านชั้นจะทำให้หน่วยความจำส่วนที่เก็บการเรียกซ้อน (stack) ล้น

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

ดูโค้ดเต็ม
islands.cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main(){
    int n; if(scanf("%d",&n)!=1) return 0;
    vector<int> f(n+1),L(n+1),indeg(n+1,0);
    for(int i=1;i<=n;i++){ scanf("%d %d",&f[i],&L[i]); indeg[f[i]]++; }
    vector<ll> t1(n+1,0),t2(n+1,0);                  // สองกิ่งที่ลึกที่สุดของแต่ละปม
    vector<int> q; q.reserve(n);
    for(int i=1;i<=n;i++) if(!indeg[i]) q.push_back(i);
    for(size_t h=0;h<q.size();h++){                  // ปอกใบทิ้งทีละชั้น เหลือไว้แต่ปมบนวง
        int u=q[h],p=f[u]; ll v=t1[u]+L[u];
        if(v>t1[p]){ t2[p]=t1[p]; t1[p]=v; } else if(v>t2[p]) t2[p]=v;
        if(--indeg[p]==0) q.push_back(p);
    }
    vector<int> comp(n+1,-1); vector<ll> res;
    vector<char> done(n+1,0);
    vector<int> cyc; vector<ll> d,P; vector<int> W;
    for(int s=1;s<=n;s++){
        if(indeg[s]==0||done[s]) continue;
        int id=(int)res.size(); cyc.clear();
        for(int u=s; !done[u]; u=f[u]){ done[u]=1; cyc.push_back(u); comp[u]=id; }
        int c=(int)cyc.size();
        d.assign(c,0); W.assign(c,0);
        for(int i=0;i<c;i++){ d[i]=t1[cyc[i]]; W[i]=L[cyc[i]]; }
        P.assign(2*c,0);
        for(int i=1;i<2*c;i++) P[i]=P[i-1]+W[(i-1)%c];
        ll local=0; deque<int> dq;                   // หน้าต่างเลื่อน หาค่ามากสุดของ d[i]-P[i]
        for(int j=0;j<2*c;j++){
            while(!dq.empty() && dq.front()<j-(c-1)) dq.pop_front();
            if(!dq.empty()) local=max(local, d[j%c]+P[j] + d[dq.front()%c]-P[dq.front()]);
            ll cur=d[j%c]-P[j];
            while(!dq.empty() && d[dq.back()%c]-P[dq.back()]<=cur) dq.pop_back();
            dq.push_back(j);
        }
        res.push_back(local);
    }
    for(int h=(int)q.size()-1;h>=0;h--) comp[q[h]]=comp[f[q[h]]];   // แจกหมายเลขกลุ่มลงต้นไม้ที่ห้อยอยู่
    for(int u=1;u<=n;u++) res[comp[u]]=max(res[comp[u]],t1[u]+t2[u]);
    ll ans=0; for(ll v:res) ans+=v;
    printf("%lld\n",ans);
}
แต่ละส่วนของโค้ด อธิบายไว้ที่ไหน
ส่วนของโค้ดอธิบายใน
อ่านอินพุต นับลูกศรชี้เข้า (indeg)ตอนที่ 3 ทรงจมในต้นไม้ ขั้นแรกของการปอกใบ
ปอกใบทิ้งทีละชั้น เก็บ t1, t2ตอนที่ 3 ทรงจมในต้นไม้
แยกวง เก็บ d, W และระยะสะสม P ยาว 2cตอนที่ 4 ทรงข้ามวง (คลี่วงเป็นสองรอบ)
หน้าต่างเลื่อน หาค่ามากสุดของ d[i] − P[i]ตอนที่ 4 ทรงข้ามวง (เครื่องเดิน)
แจกหมายเลขกลุ่มลงต้นไม้ห้อย แล้วเอา t1 + t2 มาสู้ตอนที่ 5 รวมสองทรง
รวมคำตอบทุกกลุ่มกฎเรือในโจทย์

รวมทั้งหมดผมสุ่ม 1,500 ชุดเทียบเฉลยเต็มกับตัวไล่ทุกทาง และอีก 800 ชุดเทียบเฉลยเต็มกับเฉลยชุด 40 คะแนน ตรงกันทุกชุด

เวลาที่วัดได้บนเครื่องผม สวน 1,000,000 เกาะ แบบวงสั้นเยอะ ๆ ใช้ 235 มิลลิวินาที แบบวงเดียวยาวหนึ่งล้านใช้ 240 มิลลิวินาที จากลิมิต 2.2 วินาที

อีกท่าของเส้นผ่านศูนย์กลาง · เดินทั่วต้นไม้สองรอบ

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

ถ้าโจทย์ถามแค่ตัวเลขเส้นผ่านศูนย์กลาง มีท่าที่เขียนสั้นกว่า และเป็นท่าที่โค้ดชุด 40 คะแนนใช้หลังตัดสะพานบนวง

  1. เดินทั่วต้นไม้จากเกาะไหนก็ได้ สะสมระยะไปตามทาง แล้วหาเกาะที่ไกลที่สุด เรียกว่า a
  2. เดินทั่วต้นไม้อีกรอบจาก a ระยะที่ไกลที่สุดคือเส้นผ่านศูนย์กลาง

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

ถ้าสะพานไม่มีความยาว (นับเป็นจำนวนเส้น) ก็ใช้การค้นแบบกว้าง (BFS ย่อจาก breadth-first search อ่านว่า "เบรดท์เฟิสต์เสิร์ช" คือค้นทีละชั้นจากใกล้ไปไกล) แบบโค้ดข้างล่างได้

หลักที่ทำให้ท่านี้ถูก

ถ้าความยาวสะพานไม่ติดลบ เกาะที่ไกลที่สุดจากเกาะใดก็ได้ เป็นปลายข้างหนึ่งของเส้นผ่านศูนย์กลางเส้นใดเส้นหนึ่ง (อาจมีหลายเส้นที่ยาวเท่ากัน)

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

ระวัง หลักนี้ใช้ได้แค่บนต้นไม้

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

tree_diameter.cpp
// เส้นผ่าศูนย์กลางของต้นไม้ ด้วย BFS สองรอบ
// รอบแรกจากปมใดก็ได้ หาปมที่ไกลสุด รอบสองเริ่มจากปมนั้น ระยะที่ไกลสุดคือคำตอบ
// อินพุต: n แล้ว n-1 บรรทัด u v   เอาต์พุต: ความยาวเส้นผ่าศูนย์กลาง (นับเป็นจำนวนเส้น)
#include <bits/stdc++.h>
using namespace std;

/** คืนคู่ (ปมที่ไกลสุด, ระยะถึงปมนั้น) เมื่อเริ่มจาก s */
pair<int, int> far(int s, const vector<vector<int>>& g) {
    int n = (int)g.size();
    vector<int> dist(n, -1);
    dist[s] = 0;
    vector<int> q{s};
    for (int h = 0; h < (int)q.size(); h++)
        for (int v : g[q[h]])
            if (dist[v] == -1) {
                dist[v] = dist[q[h]] + 1;
                q.push_back(v);
            }
    int best = s;
    for (int i = 0; i < n; i++) if (dist[i] > dist[best]) best = i;
    return {best, dist[best]};
}

int main() {
    int n;
    if (scanf("%d", &n) != 1) return 0;
    vector<vector<int>> g(n);
    for (int i = 0; i + 1 < n; i++) {
        int u, v;
        scanf("%d %d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    if (n == 1) { printf("0\n"); return 0; }
    int a = far(0, g).first;      // ปมปลายทางหนึ่งของเส้นผ่าศูนย์กลาง
    printf("%d\n", far(a, g).second);
    return 0;
}
ตัวตรวจที่ทำให้กล้าเชื่อโค้ดข้างบน

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

brute_diameter.cpp
// ตัวตรวจอิสระของเส้นผ่าศูนย์กลาง: BFS จากทุกปม เอาระยะที่มากที่สุดของทั้งหมด
// ไม่ใช้ข้ออ้างว่า "ปมที่ไกลสุดจากปมใดก็ได้ เป็นปลายของเส้นผ่าศูนย์กลาง" เลย
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    if (scanf("%d", &n) != 1) return 0;
    vector<vector<int>> g(n);
    for (int i = 0; i + 1 < n; i++) {
        int u, v;
        scanf("%d %d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    int best = 0;
    for (int s = 0; s < n; s++) {
        vector<int> dist(n, -1);
        dist[s] = 0;
        vector<int> q{s};
        for (int h = 0; h < (int)q.size(); h++)
            for (int v : g[q[h]])
                if (dist[v] == -1) {
                    dist[v] = dist[q[h]] + 1;
                    q.push_back(v);
                }
        for (int i = 0; i < n; i++) best = max(best, dist[i]);
    }
    printf("%d\n", best);
    return 0;
}

สุ่มต้นไม้เล็ก ๆ รวมกรณีเกาะเดียวและกรณีเป็นเส้นตรง แล้วเทียบ 700 รอบ ตรงกันทุกรอบ ที่ N = 200,000 โค้ดนี้ใช้เวลา 0.11 วินาที

ทดสอบตัวเอง

ถ้าหน้าต่างเลื่อนบนวงที่คลี่ยาว c แทน c − 1 จะเกิดอะไรกับกลุ่มที่ 2 ของตัวอย่าง

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

ทำไมการปอกใบรับประกันว่า t1 ของเกาะหนึ่งไม่เปลี่ยนอีกหลังมันเข้าคิว

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

ทำไมวิธีตัดสะพานบนวงต้องตัดตามหมายเลขสะพาน ไม่ใช่ตามคู่เกาะ

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

สรุปและท่าที่ติดมือกลับไป

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

กราฟที่ทุกปมมีลูกศรออกเส้นเดียวพอดีเรียกว่ากราฟฟังก์ชัน (functional graph) มีบทปูพื้นเรื่องกราฟฟังก์ชันและบทเรื่อง DP บนต้นไม้อยู่ในคลังนี้แล้ว

คำถามที่ยกไปใช้กับข้ออื่นได้คือ ลูกศรของแต่ละก้าวถูกกำหนดตายตัวหรือไม่ ถ้าตายตัว รูปของกราฟก็ถูกล็อกทันที และเราแยกกรณีได้โดยไม่ต้องค้นหา

แหล่งที่มา

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