programming.in.th · ข้อ 2027

บ่อน้ำมันสามผืน: เลิกเลือกบล็อก แล้วหันไปเลือกเส้นที่แบ่งกระดาน

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

★★★★☆ prefix sumdpad hoc อ่าน 13 นาที 9 กันยายน 2026

โจทย์ · สามบริษัทฮั้วกัน ขอบล็อกละหนึ่งผืน

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

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

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

EXAMPLE
InputOutput
9 9 3
1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1
1 8 8 8 8 8 1 1 1
1 8 8 8 8 8 1 1 1
1 8 8 8 8 8 1 1 1
1 1 1 1 8 8 8 1 1
1 1 1 1 1 1 8 8 8
1 1 1 1 1 1 9 9 9
1 1 1 1 1 1 9 9 9
208

โจทย์เล่าไว้เองว่าตารางเดียวกันนี้ ถ้า K เท่ากับ 2 จะได้ 100 ส่วนที่ K เท่ากับ 3 จะได้ 208 ตัวเลขสองตัวนี้หน้าเว็บคำนวณเองตอนสร้างหน้า และตรงกับที่โจทย์บอกทั้งคู่

ใบ้

จำนวนตำแหน่งที่วางบล็อกได้มีราว M คูณ N ตำแหน่ง ที่ 1,500 คูณ 1,500 คือสองล้านกว่าตำแหน่ง ถ้าลองทุกสามตำแหน่ง มันคือเลขยกกำลังสาม ซึ่งไม่ต้องคิดต่อเลย

ลองวาดสี่เหลี่ยมสามอันที่ไม่ทับกันบนกระดาษดูหลาย ๆ แบบ แล้วถามว่า มีเส้นตรงเส้นหนึ่งที่ลากผ่านทั้งกระดานโดยไม่ตัดบล็อกไหนเลยเสมอไหม

ลองเอง · เลือกสามบล็อกที่ไม่ทับกัน

บล็อกขนาดหนึ่งช่องคือกรณีที่ง่ายที่สุด กดสามช่องที่คิดว่ารวมกันได้มากที่สุด

กดที่ช่องเพื่อวางมุมบนซ้ายของบล็อก วางได้สามบล็อก กดซ้ำที่เดิมเพื่อเอาออก

วางแล้ว 0 จาก 3 บล็อก

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

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

เฉลย ตอนที่ 1 · สามสี่เหลี่ยมที่ไม่ทับกัน มีได้แค่หกทรง

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

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

ซึ่งผิด เพราะสองบล็อกที่วางเรียงกันในแนวนอน ต้องการความสูงแค่ K ส่วนที่ต้องการ 2K คือความกว้าง ผมสลับสองแกนนี้กันในหัว

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

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

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

พอไล่ให้ครบว่าเส้นแรกเป็นแนวนอนหรือแนวตั้ง และเส้นที่สองอยู่ฝั่งไหนและเป็นแนวไหน ก็ได้ทั้งหมดหกทรง ตามภาพ

นอน 3 แถบ 1 2 3 ตั้ง 3 แถบ 1 2 3 บน 1 ล่าง 2 1 2 3 ล่าง 1 บน 2 1 2 3 ซ้าย 1 ขวา 2 1 2 3 ขวา 1 ซ้าย 2 1 2 3
หกทรงของการแบ่งกระดานให้แต่ละส่วนมีบล็อกเดียว สองทรงแรกคือแถบสามแถบซึ่งใช้เส้นตัดสองเส้นในแนวเดียวกัน ส่วนอีกสี่ทรงใช้เส้นตัดหนึ่งเส้นแล้วแบ่งฝั่งที่มีสองบล็อกอีกที เลขในกล่องคือหมายเลขบล็อก ไม่ได้บอกลำดับ

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

เฉลย ตอนที่ 2 · ถามหาบล็อกที่ดีที่สุดของสี่เหลี่ยมใด ๆ ในเวลาคงที่

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

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

หัวใจของทั้งข้อ
// ตารางมุม ค่าที่ดีที่สุดของบล็อกที่อยู่ในสี่เหลี่ยมมุมบนซ้ายทั้งก้อน
// สร้างด้วยการไล่ครั้งเดียว โดยรับค่ามาจากช่องบนกับช่องซ้าย
F1(i, j) = max(BK(i, j), max(F1(i-1, j), F1(i, j-1)));

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

โน้ต · ทรงแถบสามแถบไม่ต้องใช้ตารางมุม

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

หกทรงบนตัวอย่างในโจทย์

เดินทีละทรง บนตาราง 9 x 9 ที่ K เท่ากับ 3

ค่าที่ดีที่สุดของแต่ละทรง
ทรงที่รูปแบบการแบ่งค่าที่ได้
1 แถบแนวนอนสามแถบ 173
2 แถบแนวตั้งสามแถบ 201
3 ตัดขวางหนึ่งเส้น ครึ่งล่างตัดตั้ง 173
4 ตัดขวางหนึ่งเส้น ครึ่งบนตัดตั้ง 208
5 ตัดตั้งหนึ่งเส้น ครึ่งขวาตัดขวาง 208
6 ตัดตั้งหนึ่งเส้น ครึ่งซ้ายตัดขวาง 173

คำตอบคือ 208 ซึ่งมาจากสามบล็อกที่มุมบนซ้ายอยู่ที่แถวคอลัมน์ (3, 2), (4, 5), (7, 7) นับจากหนึ่ง ตำแหน่งชุดนี้มาจากตัวตรวจที่ลองทุกสามบล็อก ไม่ได้มาจากท่าหกทรง

โค้ด

ดูโค้ดเต็ม
oil.cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int M, N, K, MB, NB;
vector<int> pre, blk, f1, f2, f3, f4;
inline int P(int i, int j){ return pre[(size_t)i * (N + 1) + j]; }
inline int&BK(int i, int j){ return blk[(size_t)i * (NB + 2) + j]; }   // มุมบนซ้ายที่ (i, j) ฐานหนึ่ง
inline int&F1(int i, int j){ return f1[(size_t)i * (NB + 2) + j]; }
inline int&F2(int i, int j){ return f2[(size_t)i * (NB + 2) + j]; }
inline int&F3(int i, int j){ return f3[(size_t)i * (NB + 2) + j]; }
inline int&F4(int i, int j){ return f4[(size_t)i * (NB + 2) + j]; }

int main(){
    if(scanf("%d %d %d", &M, &N, &K) != 3) return 0;
    pre.assign((size_t)(M + 1) * (N + 1), 0);
    for(int i = 1; i <= M; i++) for(int j = 1; j <= N; j++){ int x; scanf("%d", &x);
        pre[(size_t)i * (N + 1) + j] = x + P(i-1, j) + P(i, j-1) - P(i-1, j-1); }

    MB = M - K + 1; NB = N - K + 1;
    blk.assign((size_t)(MB + 2) * (NB + 2), 0);
    for(int i = 1; i <= MB; i++) for(int j = 1; j <= NB; j++)
        BK(i, j) = P(i+K-1, j+K-1) - P(i-1, j+K-1) - P(i+K-1, j-1) + P(i-1, j-1);

    // ตารางมุมสี่ทิศ ค่าที่ดีที่สุดของบล็อกที่อยู่ในสี่เหลี่ยมมุมนั้นทั้งก้อน
    f1.assign(blk.size(), 0); f2.assign(blk.size(), 0); f3.assign(blk.size(), 0); f4.assign(blk.size(), 0);
    for(int i = 1; i <= MB; i++) for(int j = 1; j <= NB; j++) F1(i,j) = max(BK(i,j), max(F1(i-1,j), F1(i,j-1)));
    for(int i = 1; i <= MB; i++) for(int j = NB; j >= 1; j--) F2(i,j) = max(BK(i,j), max(F2(i-1,j), F2(i,j+1)));
    for(int i = MB; i >= 1; i--) for(int j = 1; j <= NB; j++) F3(i,j) = max(BK(i,j), max(F3(i+1,j), F3(i,j-1)));
    for(int i = MB; i >= 1; i--) for(int j = NB; j >= 1; j--) F4(i,j) = max(BK(i,j), max(F4(i+1,j), F4(i,j+1)));

    // ค่าที่ดีที่สุดของบล็อกที่แถวบนเป็น i พอดี และที่คอลัมน์ซ้ายเป็น j พอดี
    vector<int> rb(MB + 2, 0), cb(NB + 2, 0);
    for(int i = 1; i <= MB; i++) for(int j = 1; j <= NB; j++){ rb[i] = max(rb[i], BK(i,j)); cb[j] = max(cb[j], BK(i,j)); }
    vector<int> rp(MB + 2, 0), rs(MB + 3, 0), cp(NB + 2, 0), cs(NB + 3, 0);
    for(int i = 1; i <= MB; i++) rp[i] = max(rp[i-1], rb[i]);
    for(int i = MB; i >= 1; i--) rs[i] = max(rs[i+1], rb[i]);
    for(int j = 1; j <= NB; j++) cp[j] = max(cp[j-1], cb[j]);
    for(int j = NB; j >= 1; j--) cs[j] = max(cs[j+1], cb[j]);
    auto A  = [&](int t){ int k = t - K + 1; return (k >= 1) ? rp[min(k, MB)] : 0; };   // ดีที่สุดในแถว 1..t
    auto Bv = [&](int t){ return (t <= MB) ? rs[max(t, 1)] : 0; };                      // ดีที่สุดในแถว t..M
    auto L  = [&](int v){ int k = v - K + 1; return (k >= 1) ? cp[min(k, NB)] : 0; };
    auto Rv = [&](int v){ return (v <= NB) ? cs[max(v, 1)] : 0; };

    ll best = 0;
    // 1. แถบแนวนอนสามแถบ เดินตามแถบกลางที่มีแถวบนเป็น m
    for(int m = 1; m <= MB; m++) best = max(best, (ll)A(m-1) + rb[m] + Bv(m + K));
    // 2. แถบแนวตั้งสามแถบ
    for(int m = 1; m <= NB; m++) best = max(best, (ll)L(m-1) + cb[m] + Rv(m + K));
    // 3. ตัดขวางที่ t ครึ่งบนหนึ่งบล็อก ครึ่งล่างตัดตั้ง
    for(int t = K; t + K <= M; t++){ int bi = t + 1, inner = 0;
        for(int v = K; v + K <= N; v++) inner = max(inner, F3(bi, v-K+1) + F4(bi, v+1));
        best = max(best, (ll)A(t) + inner); }
    // 4. ตัดขวางที่ t ครึ่งล่างหนึ่งบล็อก ครึ่งบนตัดตั้ง  (t เริ่มที่ K ไม่ใช่ 2K)
    for(int t = K; t <= M - K; t++){ int inner = 0;
        for(int v = K; v + K <= N; v++) inner = max(inner, F1(t-K+1, v-K+1) + F2(t-K+1, v+1));
        best = max(best, (ll)Bv(t+1) + inner); }
    // 5. ตัดตั้งที่ v ครึ่งซ้ายหนึ่งบล็อก ครึ่งขวาตัดขวาง
    for(int v = K; v + K <= N; v++){ int bj = v + 1, inner = 0;
        for(int t = K; t + K <= M; t++) inner = max(inner, F2(t-K+1, bj) + F4(t+1, bj));
        best = max(best, (ll)L(v) + inner); }
    // 6. ตัดตั้งที่ v ครึ่งขวาหนึ่งบล็อก ครึ่งซ้ายตัดขวาง
    for(int v = K; v <= N - K; v++){ int inner = 0;
        for(int t = K; t + K <= M; t++) inner = max(inner, F1(t-K+1, v-K+1) + F3(t+1, v-K+1));
        best = max(best, (ll)Rv(v+1) + inner); }
    printf("%lld\n", best);
}
ดูตัวตรวจที่ใช้เทียบ
brute_oil.cpp
// ตัวตรวจ ลองทุกสามบล็อกที่ไม่ทับกัน ไม่มีทฤษฎีการแบ่งแถบอยู่ในนี้เลย
#include <bits/stdc++.h>
using namespace std;
int M, N, K; int g[20][20];
int blockSum(int r, int c){ int s = 0;
    for(int i = 0; i < K; i++) for(int j = 0; j < K; j++) s += g[r+i][c+j]; return s; }
bool ov(int r1, int c1, int r2, int c2){
    return !(r1 + K <= r2 || r2 + K <= r1 || c1 + K <= c2 || c2 + K <= c1); }
int main(){
    scanf("%d %d %d", &M, &N, &K);
    for(int i = 0; i < M; i++) for(int j = 0; j < N; j++) scanf("%d", &g[i][j]);
    vector<array<int,3>> P;
    for(int r = 0; r + K <= M; r++) for(int c = 0; c + K <= N; c++) P.push_back({r, c, blockSum(r, c)});
    long long best = -1;
    for(size_t a = 0; a < P.size(); a++) for(size_t b = a + 1; b < P.size(); b++){
        if(ov(P[a][0], P[a][1], P[b][0], P[b][1])) continue;
        for(size_t c = b + 1; c < P.size(); c++){
            if(ov(P[a][0], P[a][1], P[c][0], P[c][1]) || ov(P[b][0], P[b][1], P[c][0], P[c][1])) continue;
            best = max(best, (long long)P[a][2] + P[b][2] + P[c][2]); } }
    printf("%lld\n", best);
}

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

ผมสุ่มตารางขนาดไม่เกิน 9 คูณ 9 ที่ K เป็น 1 ถึง 3 รวม 1,856 เคส แล้วเทียบกัน ตรงกันหมด หน้าเว็บนี้ก็รันด่านตรวจเดียวกันตอนบิลด์ 220 ชุด บวกกับตรวจว่าตัวอย่างในโจทย์ได้ 208 ที่ K เท่ากับ 3 และ 100 ที่ K เท่ากับ 2

ดูรุ่นที่ผิด กับเคสเล็กที่สุดที่ทำให้มันพัง
ผิดที่ขอบล่างของเส้นตัดสองทรง
// รุ่นที่ผิด ทรงที่ 4 กับ 6 เริ่มเส้นตัดที่ 2K ทั้งที่ควรเริ่มที่ K
for(int t = 2 * K; t <= M - K; t++){ ... }     // ครึ่งบนวางสองบล็อกเรียงกันแนวนอน
for(int v = 2 * K; v <= N - K; v++){ ... }     // ครึ่งซ้ายวางสองบล็อกเรียงกันแนวตั้ง

เคสที่เล็กที่สุดที่จับผิดได้คือตาราง 2 คูณ 2 ที่ K เท่ากับ 1 และค่าในตารางคือ 2 1 กับ 2 0 คำตอบจริงคือ 5 คือหยิบ 2, 1 และ 2 แต่รุ่นที่ผิดตอบ 4 เพราะทรงที่ต้องใช้ ถูกตัดทิ้งไปตั้งแต่เงื่อนไขของลูป

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

เวลาที่วัดได้บนเครื่องผม ตาราง 1,500 คูณ 1,500 ที่ K เท่ากับ 50 ใช้เวลา 0.53 วินาที จากลิมิต 1.5 วินาที

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

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

ทบทวนพื้นฐาน · ผลรวมสะสมสองมิติ

ตารางผลรวมสะสมสร้างด้วยสูตร P(i,j) = a(i,j) + P(i-1,j) + P(i,j-1) - P(i-1,j-1) ที่ต้องลบตัวสุดท้ายเพราะสี่เหลี่ยมสองผืนที่บวกเข้ามาซ้อนทับกันอยู่หนึ่งผืน พอมีตารางนี้แล้ว ผลรวมของสี่เหลี่ยมใด ๆ คิดได้ด้วยการบวกลบสี่ตัว

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

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

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

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

แหล่งที่มา

  1. โจทย์ Oil บน programming.in.th ข้อ 2027 programming.in.th/tasks/2027 (สืบค้น 9 กันยายน 2026)
  2. ต้นทางของโจทย์คือ Asia-Pacific Informatics Olympiad 2009