programming.in.th · ข้อ 2010

อัญมณีในท้องปลา: อย่าหักลบทีหลัง ให้ตั้งกฎว่าคำตอบเป็นของใครตั้งแต่แรก

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

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

โจทย์ · จับปลาหนึ่งตัว ได้อัญมณีในท้องมันทั้งหมด

ในทะเลสาบมีปลา F ตัว แต่ละตัวกลืนอัญมณีไว้หนึ่งชิ้น อัญมณีมี K ประเภท ปลาหลายตัวกินประเภทเดียวกันได้ ปลาตัวหนึ่งกินปลาอีกตัวได้ก็ต่อเมื่อมันยาวอย่างน้อยสองเท่า ของตัวที่ถูกกิน

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

คำถามคือชุดของอัญมณีที่เป็นไปได้มีกี่แบบ ชุดนับจากจำนวนของแต่ละประเภท ไม่สนใจลำดับ ตอบเป็นเศษที่เหลือจากการหารด้วย M (การหารเอาเศษนี้ในโค้ดเรียกว่า mod ย่อจาก modulo)

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

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

ชุดที่เป็นไปได้มี 11 แบบ คือ [1] [2] [3] [1,2] [1,3] [2,3] [3,3] [1,2,3] [1,3,3] [2,3,3] [1,2,3,3] และ 11 หารเอาเศษด้วย 7 ได้ 4 ตรงกับรายการที่โจทย์ยกมาให้ดูทั้งหมด

ใบ้

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

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

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

ลองเอง · เก็บชุดอัญมณีให้ครบทุกแบบ

ปลาสามตัวสองชนิด พอให้เห็นว่าตัวจับกลืนใครได้บ้าง

เลือกชนิดที่จะจับ ปลาตัวที่ยาวที่สุดของชนิดนั้นจะเป็นตัวจับ

เลือกชนิดที่จะจับก่อน แล้วค่อยปรับจำนวนอัญมณีแต่ละชนิดที่จะเก็บ

เก็บได้ 0 จาก 0 ชุดที่ต่างกัน

ยังไม่ได้เก็บชุดไหนเลย

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

เฉลย ตอนที่ 1 · จับปลาหนึ่งตัว ได้ชุดที่เป็นกล่องหนึ่งใบ

เริ่มจากคำถามในใบ้ ถ้าจะจับปลา X ที่ยาว Lx อัญมณีที่ไปอยู่ในท้องมันได้มาจากใครบ้าง

  • ปลาที่ X กินเองได้ ต้องยาวไม่เกิน Lx / 2
  • ปลาที่ถูกกินทางอ้อม ต้องถูกปลากลางทางที่ยาวไม่เกิน Lx / 2 กินก่อน ตัวมันเองยาวไม่เกิน Lx / 4 ซึ่งอยู่ในกลุ่มแรกอยู่แล้ว
  • ในกลุ่มนั้น X เลือกกินตัวไหนก็ได้ ทีละตัวตามใจ ชุดย่อยแบบไหนของกลุ่มก็ได้ทั้งนั้น

สิ่งที่ได้จากปลาหนึ่งตัว

อัญมณีของตัว X เอง บวกกับชุดย่อยใด ๆ ของปลาที่ยาวไม่เกินครึ่งหนึ่งของ X

ดูกับตัวอย่างในโจทย์ ถ้าจับปลายาว 8 (ชนิด 3) ปลาที่ยาวไม่เกิน 4 มี 3 ตัว แต่ละชนิดหยิบได้ดังนี้

  • ชนิด 1: 0 ถึง 1 ชิ้น
  • ชนิด 2: 0 ถึง 1 ชิ้น
  • ชนิด 3: 1 ถึง 2 ชิ้น (รวมชิ้นของตัวจับ)

ทุกชนิดเลือกจำนวนได้อิสระต่อกัน มีทั้งหมด 2 × 2 × 2 = 8 ชุด

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

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

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

ปัญหาที่เหลือ

มีกล่อง K ใบ ใบละชนิด คำตอบคือจำนวนชุดที่อยู่ในกล่องอย่างน้อยหนึ่งใบ

เฉลย ตอนที่ 2 · วิธีช้าแต่ถูก และระยะห่างถึงเฉลยเต็ม

รู้แล้วว่าต้องนับอะไร ลองไล่จากวิธีที่ตรงที่สุดขึ้นไปทีละขั้น แล้วดูว่าแต่ละวิธีไปได้ถึงชุดทดสอบไหน

ชุดทดสอบย่อยของโจทย์
ขอบเขตคะแนนสะสม
K ไม่เกิน 2025
K ไม่เกิน 7,00070
ขอบเขตเต็ม K ไม่เกิน 500,000100

วิธีที่ 1

ไล่ทุกชุดแล้วโยนลงเซต

เท่าจำนวนชุด ซึ่งโตแบบทวีคูณ

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

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

วิธีที่ 2

เพิ่มเข้าตัดออก

ราว 2K × K ก้าว

หาขนาดของกล่องทุกใบรวมกันจากขนาดของส่วนที่ทับกัน ไม่ต้องไล่ทีละชุด

ได้ 25 คะแนน

วิธีที่ 3

ให้กล่องหลังสุดเป็นเจ้าของ แล้วคูณไล่

ราว K² ก้าว

ตั้งกฎให้ทุกชุดมีกล่องเจ้าของใบเดียว แล้วนับแต่ละชุดที่เจ้าของของมัน (ตอนที่ 3 และ 4)

ได้ 70 คะแนน

วิธีเต็ม

เจ้าของเดิม + ต้นไม้ช่วงเก็บผลคูณ

ราว (F + 4K) × log K บวกการเรียง

ต้นไม้ช่วง (segment tree อ่านว่า "เซกเมนต์ทรี") ตอบผลคูณของช่วงได้เร็ว log K คือจำนวนครั้งที่ผ่าครึ่ง K จนเหลือ 1 ได้ราว 19 ที่ K ห้าแสน (ตอนที่ 5)

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

วิธีที่ 2 ใช้หลักการเพิ่มเข้าตัดออก (inclusion–exclusion อ่านว่า "อินคลูชัน เอ็กซ์คลูชัน") กับกล่องสองใบ ให้บวกขนาดของทั้งสองใบ แล้วลบส่วนที่ทับกันออกหนึ่งรอบ เพราะชุดในส่วนนั้นถูกบวกไปสองรอบ

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

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

ตัวอย่างเล็กที่ใช้ตลอดเฉลย

ตัวอย่างในโจทย์เล็กเกินจะเห็นทุกส่วนของสูตร จากนี้ไปใช้ทะเลสาบนี้แทน มีปลา 7 ตัว อัญมณี 4 ชนิด

  • ชนิด 1: ปลายาว 2
  • ชนิด 2: ปลายาว 5, 8, 11
  • ชนิด 3: ปลายาว 4, 12
  • ชนิด 4: ปลายาว 16

ตัวแทนของชนิด 1 ถึง 4 ยาว 2, 11, 12, 16 ตามลำดับ ไล่นับตรง ๆ แล้วมีทั้งหมด 27 ชุด

กล่องของตัวอย่างเล็ก · ชนิดละหนึ่งใบ
หยิบชนิดกล่อง 1กล่อง 2กล่อง 3กล่อง 4
ชนิด 110 ถึง 10 ถึง 10 ถึง 1
ชนิด 201 ถึง 20 ถึง 10 ถึง 2
ชนิด 300 ถึง 11 ถึง 20 ถึง 1
ชนิด 40001
จำนวนชุด18812
ขนาดรวมกันตรง ๆ ได้ 1 + 8 + 8 + 12 = 29 มากกว่าคำตอบจริง 2 เพราะมีชุดที่อยู่ในหลายกล่อง

แกะสูตรกับกล่อง 2 และกล่อง 3

  • คำถาม: มีกี่ชุดที่อยู่ในกล่อง 2 หรือกล่อง 3
  • กล่อง 2 มี 8 ชุด กล่อง 3 มี 8 ชุด
  • ส่วนที่ทับกันคือ ชนิด 1: 0 ถึง 1 · ชนิด 2: 1 · ชนิด 3: 1 · ชนิด 4: 0 ได้ 2 ชุด
  • บวกสองใบแล้วลบส่วนที่ทับ: 8 + 8 − 2 = 14
  • นับตรง ๆ จากรายการทุกชุด ได้ 14 ชุด ตรงกัน

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

ดูโค้ดของชุด 25 คะแนน
fish_ie.cpp
// เฉลยชุดทดสอบย่อย K ไม่เกิน 20 (25 คะแนน)
// แต่ละชนิดให้กล่องหนึ่งใบ คำตอบคือขนาดของกล่องทุกใบรวมกัน หาด้วยการเพิ่มเข้าตัดออก
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int K; ll MOD, ans = 0;
vector<vector<ll>> hi;      // hi[t][s] = หยิบชนิด s ได้มากสุดกี่ชิ้น เมื่อจับชนิด t (ชนิด t นับตัวจับด้วย)

// เลือกทีละชนิดว่าจะเอาเข้าเซต S หรือไม่ up[s] กับ lo[s] คือขอบของกล่องที่ตัดกันมาถึงตอนนี้
void dfs(int t, int cnt, const vector<ll>& up, const vector<ll>& lo) {
    if (t == K) {
        if (cnt == 0) return;                       // เซตว่างไม่นับ
        ll sz = 1 % MOD;
        for (int s = 0; s < K; s++) {
            ll w = up[s] - lo[s] + 1;               // ชนิด s เลือกได้กี่ค่า
            if (w <= 0) return;                     // กล่องที่ตัดกันว่างเปล่า ไม่มีผล
            sz = sz * (w % MOD) % MOD;
        }
        if (cnt % 2 == 1) ans = (ans + sz) % MOD;   // เซตขนาดคี่บวก
        else ans = (ans - sz + MOD) % MOD;          // เซตขนาดคู่ลบ
        return;
    }
    dfs(t + 1, cnt, up, lo);                        // ไม่เอาชนิด t เข้าเซต
    vector<ll> up2(up), lo2(lo);
    for (int s = 0; s < K; s++) up2[s] = min(up2[s], hi[t][s]);
    lo2[t] = 1;                                     // ชุดของชนิด t ต้องมีอัญมณีของตัวจับอย่างน้อยหนึ่งชิ้น
    dfs(t + 1, cnt + 1, up2, lo2);
}

int main() {
    int F;
    if (scanf("%d %d %lld", &F, &K, &MOD) != 3) return 0;
    vector<int> L(F), T(F);
    vector<ll> B(K, 0);                             // B[t] = ปลาตัวยาวที่สุดของชนิด t
    for (int i = 0; i < F; i++) {
        scanf("%d %d", &L[i], &T[i]);
        T[i]--;
        B[T[i]] = max(B[T[i]], (ll)L[i]);
    }
    hi.assign(K, vector<ll>(K, 0));
    for (int t = 0; t < K; t++) {
        for (int i = 0; i < F; i++)
            if (2LL * L[i] <= B[t]) hi[t][T[i]]++;  // ปลาที่ยาวไม่เกินครึ่งของตัวจับ ถูกกลืนได้
        hi[t][t]++;                                 // บวกตัวจับเอง
    }
    dfs(0, 0, vector<ll>(K, LLONG_MAX), vector<ll>(K, 0));
    printf("%lld\n", ans % MOD);
}

ปัญหาคือจำนวนกลุ่มเท่ากับ 2K ลองเทียบงานของแต่ละวิธีกับงบเวลา คือจำนวนก้าวที่เครื่องทำทันในลิมิต 3 วินาที

106107108109101010111012 เพิ่มเข้าตัดออก · K = 20คูณไล่ · K = 7,000คูณไล่ · K = 500,000ต้นไม้ช่วง · F = K = 500,000 2.1×107 ทัน4.9×107 ทัน2.5×1011 เกินงบราว 830 เท่า9.5×107 ทัน งบ 3 วินาที
จำนวนก้าวโดยประมาณ สเกลแบบ log (ขีดละสิบเท่า) เส้นทองคืองบเวลา 3 วินาที โดยคิดหยาบ ๆ ว่าเครื่องทำได้ราว 10⁸ ก้าวต่อวินาที

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

  • งบเวลา: 3 วินาที × 10⁸ ก้าว ≈ 300,000,000 ก้าว
  • เพิ่มเข้าตัดออก K = 20: 220 × 20 = 20,971,520 ก้าว ทัน
  • เพิ่มเข้าตัดออก K = 7,000: 27,000 เป็นเลขยาว 2,108 หลัก วาดลงกราฟไม่ได้
  • คูณไล่ K = 7,000: 7,000² = 49,000,000 ก้าว ทัน
  • คูณไล่ K = 500,000: 500,000² = 2.5 × 10¹¹ ก้าว เกินงบราว 830 เท่า
  • ต้นไม้ช่วงที่ F = K = 500,000 (log ≈ 19): แก้ช่อง 9,500,000 + ถามช่วง 38,000,000 + ค้นหาแบบแบ่งครึ่ง 19,000,000 + เรียง 28,500,000 ≈ 95,000,000 ก้าว

เป้ามีสองขั้น ขั้นแรกต้องเลิกจ่ายเป็นทวีคูณของ K ด้วยการนับแต่ละชุดครั้งเดียวตั้งแต่ต้น (ตอนที่ 3 และ 4) ขั้นที่สองต้องลดงานต่อชนิดจากราว K ลงเหลือราว log K (ตอนที่ 5)

เฉลย ตอนที่ 3 · ชุดเดียวกันอยู่ได้หลายกล่อง ให้กล่องหลังสุดเป็นเจ้าของ

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

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

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

ในเกม ถ้ากดเก็บชุดที่เคยเก็บแล้ว เกมจะบอกว่าชุดนั้นไปถึงได้จากหลายทาง ตัวอย่างเล็กก็มีแบบนี้ ชุด [2,3] อยู่ทั้งในกล่อง 2 และกล่อง 3

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

ปลาทั้ง 7 ตัว 2512164811 ชนิด 1 · กลืนได้ถึง 1ชนิด 2 · กลืนได้ถึง 5.5ชนิด 3 · กลืนได้ถึง 6ชนิด 4 · กลืนได้ถึง 8 0246810121416
ตัวอย่างเล็กเรียงชนิดตามตัวแทน แถบของแต่ละแถวคือช่วงความยาวที่ตัวแทนกลืนได้ (ไม่เกินครึ่งของตัวแทน) เพชรคือตัวแทน วงกลมกลวงคือปลา x ของชนิดนั้น ซึ่งตอนที่ 4 จะใช้ แถบของแถวล่างยาวกว่าแถวบนเสมอ

นึกภาพ

แถบในภาพซ้อนกันแบบตุ๊กตาแม่ลูกดก (matryoshka) แถบของแถวล่างครอบแถบของแถวบนไว้ทั้งแถบ ตัวแทนที่ยาวกว่ากลืนได้ทุกตัวที่ตัวแทนที่สั้นกว่ากลืนได้ และอาจกลืนได้เพิ่มอีก

ที่ซ้อนกันคือแถบ ตัวกล่องไม่ได้ซ้อนกัน เพราะกล่องของชนิด j ทุกชุดต้องมีชนิด j อย่างน้อยหนึ่งชิ้น เช่นกล่อง 1 มีแค่ชุด [1] ซึ่งไม่อยู่ในกล่อง 2 เลย

กฎเจ้าของ

ชุดหนึ่งเป็นของกล่องที่อยู่หลังสุดในลำดับ ในบรรดากล่องที่มีชุดนั้น

แถบที่ซ้อนกันทำให้กฎนี้เช็กง่าย กล่องที่มาหลังกลืนได้ทุกอย่างที่กล่องก่อนกลืนได้ สิ่งที่ต้องดูจึงเหลือสองเรื่อง ซึ่งตอนที่ 4 จะไล่ให้ดู ชุด [2,3] เมื่อกี้เป็นของกล่อง 3

เฉลย ตอนที่ 4 · กล่องหลังแย่งชุดไปได้เมื่อไหร่

เลือกกล่องของชนิด i มาหนึ่งใบ ชุดในกล่องนี้เป็นของ i เมื่อไม่มีกล่อง j ที่มาหลัง i มีชุดนั้นด้วย ตลอดตอนนี้จะดูไปพร้อมกับแถวของชนิด 2 ในตัวอย่างเล็ก ซึ่งตัวแทนยาว 11 กลืนปลาได้ถึงยาว 5.5 ได้ m = [1, 1, 1, 0] ตามชนิด 1 ถึง 4

กล่อง j (หลัง i) มีชุดนี้ได้ต้องผ่านอะไรบ้าง
เงื่อนไขผ่านเองเสมอไหมเหตุผล
ข้อ 1: ชนิดอื่นที่ไม่ใช่ i และ j ไม่เกินช่วงของกล่อง j ผ่านเสมอ ตัวแทนของ j ยาวไม่น้อยกว่าของ i กลืนได้ทุกตัวที่ i กลืนได้
ข้อ 2: ชนิด j ไม่เกินช่วงของกล่อง j ผ่านเสมอ ในกล่อง i ชนิด j มีได้ไม่เกินจำนวนที่ i กลืน ซึ่งไม่เกินที่ j กลืน
ข้อ 3: ชนิด j มีอย่างน้อยหนึ่งชิ้น ไม่เสมอ ทุกชุดในกล่อง j มีชิ้นของตัวจับ ชุดที่มีชนิด j เป็น 0 อยู่ในกล่อง j ไม่ได้
ข้อ 4: ชนิด i ไม่เกินช่วงของกล่อง j ไม่เสมอ ในกล่อง i ชนิด i มีชิ้นพิเศษของตัวจับ ถ้าชุดใช้ถึงเพดาน กล่อง j ต้องกลืนปลาชนิด i ได้มากกว่าที่ i กลืนหนึ่งตัว
ข้อ 1 และข้อ 2 ผ่านฟรี เหลือข้อ 3 และข้อ 4 ข้อ 4 ขึ้นกับว่าชุดใช้ชนิด i ถึงเพดานหรือไม่ การนับเลยแยกเป็นสองกรณี

กรณี (ข) ชนิด i ยังไม่ถึงเพดาน

ชุดมีชนิด i อยู่ 1 ถึง m ชิ้น กล่องทุกใบที่มาหลังกลืนปลาชนิด i ได้อย่างน้อย m ตัวอยู่แล้ว ข้อ 4 ผ่านกับทุก j ตัวกันที่เหลือคือข้อ 3 ชุดเป็นของ i ได้เมื่อทุกชนิดที่มาหลังมี 0 ชิ้น

ชนิด i เลือกได้ m แบบ ชนิดที่มาก่อนแต่ละชนิดเลือกได้ 0 ถึง m ของมัน คือ m + 1 แบบ ผลคูณของ m + 1 ของทุกชนิดที่มาก่อน ต่อจากนี้เรียกว่าก่อนหน้า

สูตรกรณี (ข)

m ของชนิด i × ก่อนหน้า

แถวชนิด 2: 1 × 2 = 2 ชุด คือ [1,2] [2]

กรณี (ก) ชนิด i ถึงเพดาน

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

กล่อง j กลืน x ได้เมื่อตัวแทนของ j ยาวอย่างน้อย 2x ชนิดแรกหลัง i ที่ทำได้เรียกว่าJ ตัวแทนเรียงจากสั้นไปยาวแล้ว ทุกชนิดตั้งแต่ J เป็นต้นไปก็กลืน x ได้หมด

หา J ของแถวชนิด 2

  • ปลาชนิด 2 ยาว 5, 8, 11 ตัวที่สั้นที่สุดที่ยาวเกิน 5.5 คือ 8 ได้ x = 8
  • ชนิด 3: ตัวแทนยาว 12 ยังไม่ถึง 16 กลืน x ไม่ได้
  • ชนิด 4: ตัวแทนยาว 16 ถึง 16 กลืน x ได้
  • ได้ J = ชนิด 4 (วงกลมกลวงในภาพของตอนที่ 3 คือ x ของแต่ละแถว)

ชนิดตั้งแต่ J ผ่านข้อ 4 ได้ ตัวกันของมันคือข้อ 3 ชนิดพวกนี้ต้องมี 0 ชิ้น ส่วนชนิดที่อยู่หลัง i แต่ก่อน J ตกข้อ 4 อยู่แล้ว จะมีกี่ชิ้นก็ได้ตามกล่องของ i ผลคูณของ m + 1 ของชนิดกลุ่มนี้เรียกว่าช่วงกลาง

สูตรกรณี (ก)

ก่อนหน้า × ช่วงกลาง

แถวชนิด 2: ก่อนหน้า 2 × ช่วงกลาง (ชนิด 3) 2 = 4 ชุด คือ [1,2,2,3] [1,2,2] [2,2,3] [2,2]

ชุดทั้ง 6 ชุดของสองกรณีตรงกับรายการชุดที่กล่อง 2 เป็นเจ้าของจากการไล่นับตรง ๆ ทีนี้ไล่ทุกแถวของตัวอย่างเล็ก กดถัดไปทีละชนิด

กดถัดไปเพื่อไล่ชนิดทีละชนิด

ตัวอย่างเล็ก เรียงชนิดตามตัวแทนจากสั้นไปยาว
ชนิดตัวแทนm ทุกชนิดx J(ข)(ก)
1 2 0 0 0 0 2 ชนิด 2 0 × 1 = 0 1 × 1 = 1
2 11 1 1 1 0 8 ชนิด 4 1 × 2 = 2 2 × 2 = 4
3 12 1 1 1 0 12 ไม่มี 1 × 4 = 4 4 × 1 = 4
4 16 1 2 1 0 16 ไม่มี 0 × 12 = 0 12 × 1 = 12

รวมสองคอลัมน์สุดท้ายทุกแถวได้ 27 ตรงกับการไล่นับ 27 ชุด ตัวอย่างในโจทย์ใช้สูตรเดียวกันได้ 1 + 2 + 4 + 4 = 11 และ 11 หารเอาเศษด้วย 7 ได้ 4

ถ้าคำนวณก่อนหน้ากับช่วงกลางด้วยการคูณไล่ตรง ๆ ทุกแถว ก็คือวิธีที่ 3 ในตอนที่ 2 แต่ละแถวคูณไม่เกิน K ครั้ง รวมราว K² ได้ 70 คะแนน

สองบรรทัดที่นับคำตอบ (ย่อจากโค้ดเต็ม)
ll own = m[i];                     // m ของชนิดตัวเอง ในกล่องของตัวเอง
ll left = prod(0, i - 1);          // ก่อนหน้า: ผลคูณของ (m + 1) ของชนิดที่มาก่อน

// กรณี (ข) ชนิดตัวเองไม่เต็มเพดาน  ชนิดที่มาทีหลังต้องเป็นศูนย์ทั้งหมด
ans = (ans + own % MOD * left) % MOD;

// กรณี (ก) ชนิดตัวเองเต็มเพดาน  ชนิดที่มาทีหลังใส่ได้จนกว่าจะถึงชนิด J ที่กลืน x ได้
int J = first_index_after_i_whose_rep_swallows(x);
ans = (ans + left * prod(i + 1, J - 1)) % MOD;
ดูโค้ดของชุด 70 คะแนน
fish_sub.cpp
// เฉลยชุดทดสอบย่อย K ไม่เกิน 7,000 (70 คะแนน)
// สูตรเดียวกับเฉลยเต็ม ต่างแค่ไม่ยกเซกเมนต์ทรีมาใช้ คูณไล่เอาตรง ๆ เป็น O(K^2)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
    int F, K; ll MOD;
    scanf("%d %d %lld", &F, &K, &MOD);
    vector<int> L(F), T(F);
    vector<vector<int>> byType(K + 1);
    for (int i = 0; i < F; i++) { scanf("%d %d", &L[i], &T[i]); byType[T[i]].push_back(L[i]); }
    for (int t = 1; t <= K; t++) sort(byType[t].begin(), byType[t].end());

    vector<int> ord(K); for (int t = 1; t <= K; t++) ord[t - 1] = t;
    sort(ord.begin(), ord.end(), [&](int a, int b) { return byType[a].back() < byType[b].back(); });
    vector<int> posOf(K + 1); vector<ll> B(K);
    for (int i = 0; i < K; i++) { posOf[ord[i]] = i; B[i] = byType[ord[i]].back(); }

    vector<pair<int,int>> fish(F);
    for (int i = 0; i < F; i++) fish[i] = {L[i], T[i]};
    sort(fish.begin(), fish.end());

    vector<ll> m(K, 0), val(K, 1 % MOD);        // val[p] = m[p] + 1 คือจำนวนทางเลือกของชนิดนั้น
    int ptr = 0; ll ans = 0;
    for (int i = 0; i < K; i++) {
        while (ptr < F && 2LL * fish[ptr].first <= B[i]) {
            int p = posOf[fish[ptr].second]; m[p]++; val[p] = (m[p] + 1) % MOD; ptr++;
        }
        ll left = 1 % MOD;
        for (int j = 0; j < i; j++) left = left * val[j] % MOD;     // คูณไล่แทนการถามเซกเมนต์ทรี
        ans = (ans + m[i] % MOD * left) % MOD;

        const vector<int>& v = byType[ord[i]];
        int xi = (int)(lower_bound(v.begin(), v.end(), B[i] / 2 + 1) - v.begin());
        int J = (int)(lower_bound(B.begin(), B.end(), 2LL * v[xi]) - B.begin());
        if (J < i + 1) J = i + 1;
        ll mid = 1 % MOD;
        for (int j = i + 1; j < J; j++) mid = mid * val[j] % MOD;
        ans = (ans + left * mid) % MOD;
    }
    printf("%lld\n", ans % MOD);
}

เฉลย ตอนที่ 5 · ผลคูณของช่วงที่หารกลับไม่ได้

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

ทุกทางที่ต้องหารใช้ไม่ได้ในข้อนี้ เหตุผลมีสองข้อ

  • M ไม่รับประกันว่าเป็นจำนวนเฉพาะ เลขที่มีตัวประกอบร่วมกับ M ไม่มีตัวผกผัน (inverse คือเลขที่คูณแล้วได้ 1 เมื่อหารเอาเศษด้วย M)
  • ต่อให้ M เป็นจำนวนเฉพาะ ค่า m + 1 ขึ้นได้ถึง 500,000 ซึ่งเกิน 30,000 อาจเป็นพหุคูณของ M พอหารเอาเศษแล้วกลายเป็น 0 ซึ่งหารไม่ได้

ต้นไม้เฟนวิก (Fenwick tree ตั้งชื่อตาม Peter Fenwick) เป็นตัวเลือกยอดนิยมสำหรับผลสะสมจากต้นถึงจุดหนึ่ง แต่ถ้าใช้เก็บผลคูณ มันต้องหารสองที่ ที่แรกคือตอนเอาช่วงกลาง ต้องหารผลคูณส่วนหน้าออก ที่สองคือตอนแก้ช่องจาก m + 1 เป็น m + 2 ต้องคูณทุกปมที่ครอบช่องนั้นด้วย (m + 2) / (m + 1) ต่อให้ถามแค่ผลคูณจากต้นก็ยังติดที่สอง

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

นับงานของเฉลยเต็มตามโค้ดจริง (ตัวเลขรวมอยู่ในตอนที่ 2)

  • ปลาแต่ละตัวเข้าเกณฑ์ครั้งเดียวตลอดโปรแกรม แก้ช่องในต้นไม้ F ครั้ง ครั้งละราว log K
  • แต่ละชนิดถามผลคูณสองช่วง (ก่อนหน้า กับช่วงกลาง) ช่วงละราว 2 log K รวม 4K log K
  • แต่ละชนิดหา x กับ J ด้วยการค้นหาแบบแบ่งครึ่ง (binary search ผ่าครึ่งช่วงที่เรียงไว้แล้วจนเจอ) สองครั้ง ซึ่งทำได้เพราะความยาวในชนิดเดียวกันและตัวแทนของทุกชนิดเรียงไว้แล้ว
  • บวกการเรียงปลาตามความยาว การเรียงความยาวในแต่ละชนิด และการเรียงชนิดตามตัวแทน

โค้ด C++ และการตรวจ

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

ดูโค้ดเต็ม
fish.cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int SZ; ll MOD; vector<ll> seg;                 // เซกเมนต์ทรีเก็บ "ผลคูณ" เพราะหารเอาเศษด้วย M แล้วหารกลับไม่ได้
void build(int k){ SZ=1; while(SZ<k) SZ<<=1; seg.assign(2*SZ,1%MOD); }
void setv(int p,ll v){ p+=SZ; seg[p]=v%MOD; for(p>>=1;p;p>>=1) seg[p]=seg[2*p]*seg[2*p+1]%MOD; }
ll prod(int l,int r){ if(l>r) return 1%MOD; ll a=1%MOD,b=1%MOD; for(l+=SZ,r+=SZ+1;l<r;l>>=1,r>>=1){ if(l&1) a=a*seg[l++]%MOD; if(r&1) b=seg[--r]*b%MOD; } return a*b%MOD; }
int main(){
    int F,K; if(scanf("%d %d %lld",&F,&K,&MOD)!=3) return 0;
    vector<int> L(F),T(F);
    vector<vector<int>> byType(K+1);
    for(int i=0;i<F;i++){ scanf("%d %d",&L[i],&T[i]); byType[T[i]].push_back(L[i]); }
    for(int t=1;t<=K;t++) sort(byType[t].begin(),byType[t].end());
    vector<int> ord; ord.reserve(K);
    for(int t=1;t<=K;t++) ord.push_back(t);
    sort(ord.begin(),ord.end(),[&](int a,int b){ return byType[a].back()<byType[b].back(); });
    vector<int> posOf(K+1); vector<ll> B(K);
    for(int i=0;i<K;i++){ posOf[ord[i]]=i; B[i]=byType[ord[i]].back(); }

    vector<pair<int,int>> fish(F);               // (ความยาว, ชนิด) เรียงตามความยาว
    for(int i=0;i<F;i++) fish[i]={L[i],T[i]};
    sort(fish.begin(),fish.end());

    build(K);
    vector<ll> m(K,0);                           // m[p] = จำนวนปลาชนิด ord[p] ที่ตัวแทนของแถวปัจจุบันกลืนได้
    int ptr=0; ll ans=0;
    for(int i=0;i<K;i++){
        while(ptr<F && 2LL*fish[ptr].first<=B[i]){ int p=posOf[fish[ptr].second]; m[p]++; setv(p,(m[p]+1)%MOD); ptr++; }
        ll own=m[i];
        ll left=prod(0,i-1);
        ans=(ans+own%MOD*left)%MOD;              // กรณี (ข) นับชนิดตัวเองไม่เต็มเพดาน
        // กรณี (ก) นับชนิดตัวเองเต็มเพดาน  ต้องหา J = ชนิดแรกที่ตัวแทนกลืนปลา x ได้
        const vector<int>& v=byType[ord[i]];
        ll bt=B[i];
        int xi=(int)(lower_bound(v.begin(),v.end(),bt/2+1)-v.begin());   // ตัวแรกที่ 2L > bt
        ll need=2LL*v[xi];
        int J=(int)(lower_bound(B.begin(),B.end(),need)-B.begin());
        if(J<i+1) J=i+1;
        ans=(ans+left*prod(i+1,J-1))%MOD;
    }
    printf("%lld\n",ans%MOD);
}
แต่ละส่วนของโค้ดเต็ม อธิบายไว้ตรงไหน
ส่วนในโค้ดทำอะไรตอนที่
build / setv / prodต้นไม้ช่วงที่เก็บผลคูณ แก้ช่อง และถามผลคูณของช่วง5
เรียง ord ด้วย byType[t].back()เรียงกล่องตามตัวแทนจากสั้นไปยาว3
ลูป while(ptr ...)ปลาที่เพิ่งเข้าเกณฑ์ของแถวนี้ เพิ่ม m แล้วแก้ช่อง4 (ตัวเดินทีละชนิด)
own, left, กรณี (ข)m ของตัวเองคูณก่อนหน้า4
xi, need, J, กรณี (ก)หา x กับ J แล้วคูณก่อนหน้ากับช่วงกลาง4
ตัวตรวจสองตัวที่ใช้เทียบ

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

brute.cpp
// ตัวตรวจ: ไล่ทุกชุดของทุกตัวจับแล้วโยนลง set ไม่มีทฤษฎีกันนับซ้ำเลย
// ใช้ได้แค่ทะเลสาบเล็ก ๆ เพราะจำนวนชุดโตแบบทวีคูณ
#include <bits/stdc++.h>
using namespace std;
int F, K; long long MOD;
set<vector<int>> seen;
vector<int> hi, cur;
int catcher;

void rec(int s) {
    if (s == K) {
        if (cur[catcher] >= 1) seen.insert(cur);   // ต้องมีอัญมณีของตัวจับเอง
        return;
    }
    for (int v = 0; v <= hi[s]; v++) { cur[s] = v; rec(s + 1); }
}

int main() {
    if (scanf("%d %d %lld", &F, &K, &MOD) != 3) return 0;
    vector<int> L(F), T(F);
    for (int i = 0; i < F; i++) { scanf("%d %d", &L[i], &T[i]); T[i]--; }
    for (int x = 0; x < F; x++) {                  // ลองให้ปลาทุกตัวเป็นตัวจับ ไม่ใช่แค่ตัวยาวสุดของชนิด
        hi.assign(K, 0);
        for (int i = 0; i < F; i++)
            if (2 * L[i] <= L[x]) hi[T[i]]++;
        hi[T[x]]++;
        catcher = T[x];
        cur.assign(K, 0);
        rec(0);
    }
    printf("%lld\n", (long long)(seen.size() % MOD));
}

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

simulate.cpp
// ตัวตรวจชั้นที่สอง: จำลองการกินกันจริงทุกลำดับ แล้วเก็บถุงอัญมณีของปลาทุกตัวที่ยังว่ายอยู่ในทุกสภาพ
#include <bits/stdc++.h>
using namespace std;
int F, K; long long MOD;
vector<int> L, T;
typedef vector<int> State;   // แต่ละตัว: -1 ถ้าถูกกินแล้ว ไม่งั้นคือรหัสถุง (จำนวนต่อชนิด เข้ารหัสฐาน 16)
int main() {
    if (scanf("%d %d %lld", &F, &K, &MOD) != 3) return 0;
    L.resize(F); T.resize(F);
    for (int i = 0; i < F; i++) { scanf("%d %d", &L[i], &T[i]); T[i]--; }
    vector<int> pw(K, 1);
    for (int s = 1; s < K; s++) pw[s] = pw[s - 1] * 16;
    State st(F);
    for (int i = 0; i < F; i++) st[i] = pw[T[i]];
    set<State> vis{st};
    vector<State> q{st};
    set<int> bags;
    while (!q.empty()) {
        State c = q.back(); q.pop_back();
        for (int i = 0; i < F; i++) if (c[i] >= 0) bags.insert(c[i]);
        for (int a = 0; a < F; a++) if (c[a] >= 0)
            for (int b = 0; b < F; b++) if (b != a && c[b] >= 0 && L[a] >= 2 * L[b]) {
                State n = c; n[a] += c[b]; n[b] = -1;
                if (vis.insert(n).second) q.push_back(n);
            }
    }
    printf("%lld\n", (long long)(bags.size() % MOD));
}

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

  • สุ่มทะเลสาบเล็ก (ปลาไม่เกิน 8 ตัว อัญมณีไม่เกิน 4 ชนิด ยาวไม่เกิน 16) 6,000 ชุด เทียบเฉลยเต็ม วิธีที่ 3 และวิธีที่ 2 กับ brute.cpp ตรงกันทุกชุด ครึ่งหนึ่งใช้ M เล็ก (2 ถึง 30) ให้การหารเอาเศษทำงานจริง
  • simulate.cpp เทียบกับ brute.cpp อีก 2,000 ชุด ตรงกันทุกชุด
  • ชุดกลาง (F = 200, K = 15) 300 ชุด สามวิธีตรงกัน และชุดใหญ่ (F = 20,000, K = 5,000) 100 ชุด เฉลยเต็มกับวิธีที่ 3 ตรงกัน
  • ตัวอย่างในโจทย์และตัวอย่างเล็ก ทั้งห้าโปรแกรม (เฉลยเต็ม วิธีที่ 3 วิธีที่ 2 brute.cpp และ simulate.cpp) ได้ 4 และ 27 ตามลำดับ

ทั้งหมดนี้คือการตรวจตามขนาดที่สุ่ม ยังไม่ใช่บทพิสูจน์ว่าถูกทุกกรณี

เวลาที่วัดได้ (ค่ากลางของสามรอบ รวมเวลาอ่านอินพุต)
โปรแกรมอินพุตวินาที
เฉลยเต็มF = 500,000, K = 200,0000.57
เฉลยเต็มF = 500,000, K = 500,0000.89
วิธีที่ 3 (คูณไล่)F = 500,000, K = 7,0000.48
วิธีที่ 2 (เพิ่มเข้าตัดออก)F = 500,000, K = 200.36
วัดบน WSL ด้วย g++ -O2 ลิมิตของโจทย์คือ 3 วินาที

ทดสอบตัวเอง

ชุด [2,3] ของตัวอย่างเล็กถูกนับที่แถวไหน กรณีไหน

มันอยู่ในกล่อง 2 และ 3 เป็นของกล่อง 3 ซึ่งอยู่หลังสุด ชนิด 3 มีอยู่ 1 ชิ้น เทียบกับ m ของตัวเองซึ่งเท่ากับ 1 เข้ากรณี (ข)

แถวของชนิด 1 ในตัวอย่างเล็กได้ (ก) = 1 ชุดนั้นคืออะไร และทำไม J เป็นชนิด 2 ทันที

ตัวแทนยาว 2 กลืนใครไม่ได้เลย m ทุกชนิดเป็น 0 ชุดเดียวที่ได้คือ [1] x คือตัวแทนเอง (2) ต้องการตัวแทนยาวอย่างน้อย 4 และชนิด 2 ซึ่งมาถัดไปยาว 11 ช่วงกลางเลยว่าง

ถ้าชนิดหนึ่งมีปลาตัวเดียว x คือตัวไหน

คือตัวแทนเอง เพราะปลายาว B กลืนตัวเองไม่ได้ (2B มากกว่า B) J คือชนิดแรกที่ตัวแทนยาวอย่างน้อย 2B ในตัวอย่างเล็ก ชนิด 1 กับชนิด 4 มีปลาตัวเดียว ได้ x = 2 และ 16 เท่ากับตัวแทนของมันพอดี

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

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

แหล่งที่มา

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