programming.in.th · ข้อ 2023

เม่นเดินซิกแซก: ช่องสีเทาคือคู่ที่บิตไม่ชนกัน จึงนับได้โดยไม่ต้องเดิน

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

★★★★☆ countingdpad hoc อ่าน 12 นาที 9 กันยายน 2026

โจทย์ · เม่นเดินซิกแซกบนกระดานที่ระบายสีด้วยเลขฐานสอง

ลูก้าเจอกระดานเกมแปลก ๆ ในห้องใต้หลังคา ขนาด R แถว C คอลัมน์ แถวนับ 0 ถึง R ลบหนึ่งจากบนลงล่าง คอลัมน์นับ 0 ถึง C ลบหนึ่งจากซ้ายไปขวา

ที่แปลกคือวิธีระบายสี ช่องหนึ่ง ๆ จะเป็นสีขาว ถ้าเลขแถวกับเลขคอลัมน์ เขียนเป็นเลขฐานสองแล้วมีเลข 1 ตรงตำแหน่งเดียวกันอย่างน้อยหนึ่งตำแหน่ง เช่นช่อง (4, 5) เพราะ 4 คือ 100 และ 5 คือ 101 ซึ่งชนกันที่ตำแหน่งซ้ายสุด นอกนั้นเป็นสีเทา เช่นช่อง (2, 5) เพราะ 2 คือ 010 ส่วน 5 คือ 101 ไม่ชนกันเลยสักตำแหน่ง

เม่นของลูก้าเริ่มเดินที่ช่อง (0, 0) แล้วเดินซิกแซกไปตามแนวทแยง ตามภาพข้างล่าง ลูก้านั่งนับว่าเม่นเหยียบช่องสีเทาไปกี่ช่อง พอเหยียบครบ K ช่องเม่นก็หลับ งานของเราคือบอกจำนวนช่องสีเทาที่เม่นเหยียบไป

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

EXAMPLE
InputOutput
10 10
6
5
3 5
11
8
10 10
100
51

ใบ้

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

ลองอ่านประโยคว่า "ไม่มีเลข 1 ตรงตำแหน่งเดียวกัน" ใหม่ ถ้าบิตของสองตัวนี้ไม่เคยชนกันเลย แล้ว r + c กับ r | c ต่างกันตรงไหน

0123456789 0123456789
กระดาน 10 x 10 จริง ๆ ช่องเขียวคือสีเทาตามนิยามของโจทย์ ช่องทึบคือสีขาว เส้นทองคือ 21 ก้าวแรกของเม่น สังเกตว่าแถว 0 กับคอลัมน์ 0 เป็นสีเทาทั้งแถบ เพราะเลข 0 ไม่มีบิต 1 ให้ชนกับใคร (วาดจากโมเดลในหน้านี้)

ลองเอง · กดช่องที่คิดว่าเป็นสีเทา

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

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

เลือกไว้ 0 ช่อง

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

ถ้าทายพลาดบ่อยที่ช่องแถวสูง ๆ ลองสังเกตว่าช่อง (r, c) กับช่อง (c, r) ให้ผลเหมือนกันเสมอ เพราะเงื่อนไขมันสมมาตร กระดานทั้งกระดานจึงพับทับกันได้ตามแนวทแยง

เฉลย ตอนที่ 1 · แปลคำว่าสีเทาเป็นภาษาที่นับได้

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

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

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

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

นิยามของโจทย์บอกว่าช่อง (r, c) เป็นสีเทาก็ต่อเมื่อ r กับ c ไม่มีบิต 1 ตรงตำแหน่งเดียวกันเลย ซึ่งเขียนสั้น ๆ ได้ว่า r & c = 0

หัวใจของทั้งข้อ
// ช่องสีเทาคือช่องที่บิตของแถวกับของคอลัมน์ไม่ชนกัน
// พอบิตไม่ชนกัน r + c จึงเท่ากับ r | c พอดี
// แปลว่าบนแนวทแยง r + c = d ช่องสีเทาคือคู่ที่ r เป็นสับเซตของบิต d เท่านั้น
bool grey(int r, int c){ return (r & c) == 0; }

พอบิตไม่ชนกัน การบวกก็ไม่มีตัวทด เลยสักตำแหน่ง ผลลัพธ์ของ r + c จึงเท่ากับ r | c พอดี แปลว่าบนแนวทแยงเส้นที่ r + c = d ช่องสีเทาคือคู่ที่ บิตของ r เป็นสับเซตของบิตของ d และ c คือบิตที่เหลือของ d พอดี

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

แนวทแยงเส้นที่ d = 6 ฐานสองคือ 110 แถว r คอลัมน์ c บิตชนกันไหม 0 = 000 6 = 110 ไม่ชน จึงเป็นสีเทา 2 = 010 4 = 100 ไม่ชน จึงเป็นสีเทา 4 = 100 2 = 010 ไม่ชน จึงเป็นสีเทา 6 = 110 0 = 000 ไม่ชน จึงเป็นสีเทา
แนวทแยงเส้นที่ d = 6 ซึ่งเขียนเป็นฐานสองได้ 110 มีบิต 1 อยู่สองตำแหน่ง ช่องสีเทาบนเส้นนี้จึงมีสี่ช่อง คือจำนวนวิธีแจกบิตสองตัวนั้นให้แถวหรือให้คอลัมน์ ส่วนช่องอื่นบนเส้นเดียวกันต้องยืมตัวทด บิตจึงชนกันเสมอ

เฉลย ตอนที่ 2 · ตัดคำถามเป็นสองท่อน แล้วท่อนแรกไม่สนใจการเดินเลย

เม่นเดินตามแนวทแยงทีละเส้น เส้นที่ d มีช่องอยู่ตายตัว ดังนั้นถ้าเดินไป K ช่อง เม่นจะเดินจบไปเต็ม ๆ หลายเส้น แล้วหลับกลางเส้นสุดท้ายเส้นเดียว

  • ท่อนแรกคือทุกแนวทแยงที่เดินจบแล้ว ซึ่งคือทุกช่องที่ r + c < D ทั้งหมด
  • ท่อนที่สองคือช่วงต่อเนื่องช่วงเดียวบนเส้น D

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

โน้ต · ทิศของแต่ละแนวทแยง

จากภาพกระดานข้างบน เส้นที่ d เป็นเลขคี่เดินลงล่าง คือ r เพิ่มขึ้น ส่วนเส้นที่ d เป็นเลขคู่เดินขึ้นบน คือ r ลดลง ตรวจได้จากสามก้าวแรกคือ (0,0) แล้ว (0,1) แล้ว (1,0)

เฉลย ตอนที่ 3 · นับทั้งท่อนแรกด้วยการเดินทีละบิต

ท่อนแรกคือการนับคู่ (r, c) ที่ผ่านเงื่อนไขสามข้อพร้อมกัน คือ r < R, c < C และ r | c < D โดยที่บิตของสองตัวห้ามชนกัน

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

สิ่งที่ต้องจำระหว่างทางมีแค่สามอย่าง คือตอนนี้ r ยังเท่ากับ R ในบิตที่ผ่านมาทั้งหมดหรือยัง c เท่ากับ C หรือยัง และผลรวมเท่ากับ D หรือยัง ถ้าตัวไหนเล็กกว่าขอบไปแล้ว ตัวนั้นจะอิสระตลอดที่เหลือ เพราะบิตที่ต่ำลงไปเปลี่ยนผลไม่ได้อีก สถานะทั้งหมดจึงมีแค่ 2 คูณ 2 คูณ 2 เท่ากับ 8 สถานะ

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

เดินบิตของ R = 10, C = 10, D = 7

ตารางน้ำหนักของทั้งแปดสถานะ
สถานะ เริ่มบิต 3บิต 2บิต 1บิต 0จบ
ติด R · ติด C · ติด D ······
ติด R · ติด C · หลุด D ······
ติด R · หลุด C · ติด D ······
ติด R · หลุด C · หลุด D ······
หลุด R · ติด C · ติด D ······
หลุด R · ติด C · หลุด D ······
หลุด R · หลุด C · ติด D ······
หลุด R · หลุด C · หลุด D ······

แถวล่างสุดคือแถวที่หลุดขอบครบทั้งสามตัว ซึ่งเป็นแถวเดียวที่นับเป็นคำตอบ ที่ R = 10, C = 10 และ D = 7 ได้ 19 ช่อง ตัวเลขทุกช่องในตารางนี้คำนวณตอนสร้างหน้า ไม่ได้พิมพ์มือ

ส่วนท่อนที่สองบนเส้นสุดท้าย เราต้องนับ r ที่บิตเป็นสับเซตของ D และอยู่ในช่วง [a, b] ซึ่งทำได้ด้วยการนับ r ที่ไม่เกิน b แล้วลบด้วยที่ไม่เกิน a ลบหนึ่ง การนับสับเซตที่ไม่เกินค่าหนึ่ง ก็เดินทีละบิตเหมือนกัน ต่างแค่คราวนี้มีขอบตัวเดียว

โค้ด

ทั้งโปรแกรมไม่มีลูปไหนยาวเกิน R + C คือใช้หาว่าเม่นหลับที่แนวทแยงเส้นไหน ส่วนการนับใช้ 21 บิตคูณ 8 สถานะ ซึ่งคงที่ ไม่โตตามอินพุตเลย

ดูโค้ดเต็ม
hedgehog.cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int B = 21;                       // 10^6 < 2^20 ส่วนผลรวมสองตัวใช้ 21 บิตพอ
ll R, C, K;

// จำนวนคู่ (r, c) ที่ r < R, c < C, บิตไม่ชนกัน และ r + c < D
ll countBelow(ll D){
    if(D <= 0) return 0;
    ll dp[2][2][2] = {};                // ยังติดขอบของ R, ของ C, ของ D อยู่ไหม
    dp[1][1][1] = 1;
    for(int i = B - 1; i >= 0; i--){
        ll nx[2][2][2] = {};
        int rb = (R >> i) & 1, cb = (C >> i) & 1, db = (D >> i) & 1;
        for(int a = 0; a < 2; a++) for(int b = 0; b < 2; b++) for(int c = 0; c < 2; c++){
            ll v = dp[a][b][c]; if(!v) continue;
            for(int x = 0; x < 2; x++) for(int y = 0; y < 2; y++){
                if(x && y) continue;                      // r กับ c ห้ามมีบิต 1 ตรงกัน
                int s = x | y;                            // บิตไม่ชนกัน ผลบวกจึงเท่ากับผลหรือ
                if(a && x > rb) continue; int na = (a && x == rb) ? 1 : 0;
                if(b && y > cb) continue; int nb = (b && y == cb) ? 1 : 0;
                if(c && s > db) continue; int nc = (c && s == db) ? 1 : 0;
                nx[na][nb][nc] += v;
            }
        }
        memcpy(dp, nx, sizeof dp);
    }
    // เอาเฉพาะสถานะที่หลุดขอบครบทั้งสามตัว ที่ยังติดขอบคือค่าเท่าขอบพอดี ซึ่งต้องไม่นับ
    return dp[0][0][0];
}

// จำนวน r ที่บิตเป็นสับเซตของ D และ r <= x
ll subLE(ll D, ll x){
    if(x < 0) return 0;
    ll res = 0, below = 0; bool tight = true;
    for(int i = B; i >= 0; i--) if((D >> i) & 1) below++;
    for(int i = B; i >= 0; i--){
        int di = (D >> i) & 1, xi = (x >> i) & 1;
        if(di) below--;
        if(xi){
            res += 1LL << below;                          // บิตนี้เลือกเป็น 0 ที่เหลืออิสระหมด
            if(!di){ tight = false; break; }              // บิตนี้บังคับเป็น 0 อยู่แล้ว เลิกติดขอบ
        }
    }
    return res + (tight ? 1 : 0);
}

int main(){
    if(scanf("%lld %lld %lld", &R, &C, &K) != 3) return 0;
    ll seen = 0, D = 0;                                   // หาว่าเม่นหลับกลางแนวทแยงเส้นไหน
    for(ll d = 0; d < R + C - 1; d++){
        ll len = min(R - 1, d) - max(0LL, d - C + 1) + 1;
        if(seen + len >= K){ D = d; break; }
        seen += len;
    }
    ll ans = countBelow(D);                               // ท่อนแรก แนวทแยงที่เดินจบแล้ว
    ll t = K - seen;                                      // ท่อนสอง เหลืออีก t ช่องบนเส้น D
    ll lo = max(0LL, D - C + 1), hi = min(R - 1, D), a, b;
    if(D & 1){ a = lo;         b = lo + t - 1; }          // แนวทแยงคี่เดินจากบนลงล่าง
    else     { a = hi - t + 1; b = hi; }                  // แนวทแยงคู่เดินจากล่างขึ้นบน
    ans += subLE(D, b) - subLE(D, a - 1);
    printf("%lld\n", ans);
}
ดูตัวตรวจที่ใช้เทียบ
brute_hedgehog.cpp
// ตัวตรวจ เดินจริงทีละช่องตามที่โจทย์เล่า ใช้ได้เฉพาะกระดานเล็ก
#include <bits/stdc++.h>
using namespace std;
int main(){
    long long R, C, K; cin >> R >> C >> K;
    long long cnt = 0, seen = 0;
    for(long long d = 0; d < R + C - 1 && seen < K; d++){
        long long lo = max(0LL, d - C + 1), hi = min(R - 1, d);
        if(d % 2 == 1) for(long long r = lo; r <= hi && seen < K; r++){
            long long c = d - r; seen++; if((r & c) == 0) cnt++; }
        else for(long long r = hi; r >= lo && seen < K; r--){
            long long c = d - r; seen++; if((r & c) == 0) cnt++; }
    }
    cout << cnt << "\n";
}

ตัวตรวจนี้เดินจริงตามที่โจทย์เล่า ไม่มีทฤษฎีเรื่องบิตอยู่ในนั้นเลยนอกจากนิยามของสี ผมกวาดเทียบสองโปรแกรมทุกกระดานที่ R และ C ไม่เกิน 40 และทุกค่าของ K รวม 672,400 เคส บวกกระดานสุ่มขนาดถึง 300 อีก 3,000 เคส ตรงกันทั้งหมด หน้าเว็บนี้ก็รันด่านตรวจเดียวกันตอนบิลด์ ที่ R และ C ไม่เกิน 12 รวม 6,084 เคส ถ้าไม่ตรงเมื่อไรหน้านี้จะไม่ขึ้นเลย

เวลาที่วัดได้บนเครื่องผม กระดาน 1,000,000 คูณ 1,000,000 และ K เท่ากับหนึ่งล้านล้าน ใช้เวลาไม่ถึง 30 มิลลิวินาที จากลิมิตหนึ่งวินาที คำตอบของเคสนั้นคือ 3,441,847,383 ช่อง ซึ่งเป็นจำนวนช่องสีเทาทั้งกระดาน

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

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

ทบทวนพื้นฐาน · ทำไมต้องมีธงว่าติดขอบ

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

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

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

บิลค่าใช้จ่าย
ทางขอบเขตที่มันไหวงานคร่าว ๆ ต่อหนึ่งเคส
เดินทีละช่องตามที่โจทย์เล่าK ไม่เกินหนึ่งล้าน1,000,000 ก้าว
นับด้วยการเดินบิตK ถึงหนึ่งล้านล้าน21 บิต คูณ 8 สถานะ
สองแถวนี้นับคนละหน่วยโดยตั้งใจ แถวบนนับก้าวของเม่น ซึ่งโตตาม K ตรง ๆ แถวล่างนับบิต ซึ่งเป็นลอการิทึมของ K จึงหยุดโตไปแล้วตั้งแต่ K หลักล้าน นี่คือเหตุผลเดียวที่ชุดทดสอบเต็มผ่านได้

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

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

แหล่งที่มา

  1. โจทย์ Hedgehog บน programming.in.th ข้อ 2023 programming.in.th/tasks/2023 (สืบค้น 9 กันยายน 2026)
  2. ต้นทางของโจทย์คือ COCI 2008/2009 Contest #1 วันที่ 18 ตุลาคม 2008