Hướng dẫn giải của Tô bảng (Đề HSG 12 Phú Thọ 2021-2022)


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.

Hướng dẫn giải

Mỗi lần tô một ô, ta được 1 điểm nếu tập các ô đã tô (kể cả ô vừa tô) tạo thành đúng một hình chữ nhật. Cần tìm điểm lớn nhất và đếm số thứ tự tô các ô còn lại đạt điểm đó (lấy dư cho ~10^9+7~).

Sub 1: Vét cạn — dùng khi số ô chưa tô ~\le 8~

Thử mọi hoán vị thứ tự tô các ô còn lại, mô phỏng và đếm điểm (các ô đã tô là hình chữ nhật khi số ô bằng diện tích hình bao). Nếu chỉ nộp sub này: đúng 1/40 test.

Sub 2: Công thức và quy hoạch động — ~O(m \cdot n)~

Gọi ~B_0~ là hình chữ nhật nhỏ nhất chứa các ô đã tô, kích thước ~H_0 \times W_0~. Mọi hình chữ nhật được tính điểm đều chứa ~B_0~. Dãy các hình chữ nhật lồng nhau dài nhất là dãy mở rộng từng bước thêm đúng 1 hàng hoặc 1 cột cho đến khi kín bảng, nên có ~V~ bước theo hàng (~V~ là số hàng thừa phía trên và dưới ~B_0~) và ~H~ bước theo cột. Điểm lớn nhất là ~V + H~, cộng thêm 1 nếu ~B_0~ chưa kín (lần tô kín ~B_0~ cũng được điểm).

Đếm cách: tô kín ~B_0~ theo thứ tự tùy ý (~(H_0 W_0 - k)!~ cách), rồi mỗi bước mở rộng thêm một hàng dài ~w~ có ~w!~ cách tô, thêm một cột dài ~h~ có ~h!~ cách (trong lúc tô dải chưa kín thì không được điểm). Độ dài ~w, h~ phụ thuộc thứ tự xen kẽ các bước nên dùng quy hoạch động: ~dp[i][j]~ là tổng số cách sau ~i~ bước theo hàng và ~j~ bước theo cột, ~dp[i][j] = dp[i-1][j] \cdot (W_0+j)! + dp[i][j-1] \cdot (H_0+i)!~. Cuối cùng nhân với ~\binom{V}{a} \binom{H}{c}~ (chọn bước nào mở lên trên, sang trái) và ~(H_0 W_0 - k)!~. Kiểm tra ví dụ: ~dp = 2 \cdot 2 + 1 \cdot 6 = 10~. Nếu chỉ nộp sub này: đúng 40/40 test.

Chương trình hoàn chỉnh

Hàm main đọc dữ liệu (loại các ô tô trùng); nếu còn rất ít ô chưa tô thì gọi sub1, ngược lại gọi sub2. Khi đi thi nên viết như vậy: sub sau có sai hoặc chưa kịp làm thì vẫn ăn điểm sub trước.

#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;
int m, n, k;
vector<pair<int,int>> cells;   // các ô đã tô (đã loại trùng)

// Sub 1: vét cạn mọi thứ tự tô các ô còn lại, dùng khi số ô chưa tô <= 8
// Sau mỗi lần tô: các ô đã tô tạo thành hình chữ nhật <=> số ô đã tô = diện tích hình bao
void sub1() {
    vector<vector<char>> g(m + 2, vector<char>(n + 2, 0));
    for (auto& c : cells) g[c.first][c.second] = 1;
    vector<pair<int,int>> rest;
    for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++) if (!g[i][j]) rest.push_back({i, j});
    int t = rest.size(), cnt0 = cells.size();
    vector<int> idx(t);
    iota(idx.begin(), idx.end(), 0);
    int best = -1; long long ways = 0;
    do {
        auto h = g;
        int r1 = m + 1, r2 = 0, c1 = n + 1, c2 = 0, cnt = cnt0, score = 0;
        for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++) if (h[i][j]) {
            r1 = min(r1, i); r2 = max(r2, i); c1 = min(c1, j); c2 = max(c2, j);
        }
        for (int x : idx) {
            int i = rest[x].first, j = rest[x].second;
            h[i][j] = 1; cnt++;
            r1 = min(r1, i); r2 = max(r2, i); c1 = min(c1, j); c2 = max(c2, j);
            if (cnt == (r2 - r1 + 1) * (c2 - c1 + 1)) score++;
        }
        if (score > best) { best = score; ways = 0; }
        if (score == best) ways++;
    } while (next_permutation(idx.begin(), idx.end()));
    printf("%d\n%lld\n", best, ways % MOD);
}

long long pw(long long b, long long e) {
    long long r = 1; b %= MOD;
    for (; e; e >>= 1, b = b * b % MOD) if (e & 1) r = r * b % MOD;
    return r;
}

// Sub 2: công thức + quy hoạch động O(m * n)
// Các ô đã tô nằm trong hình bao B0 = [r1,r2] x [c1,c2]. Mỗi lần được điểm, các ô tô là một hình chữ nhật chứa B0.
// Điểm lớn nhất khi mở rộng từ B0 từng bước 1 hàng/cột (mỗi bước được 1 điểm) cho tới kín bảng,
// cộng thêm 1 điểm lúc tô kín B0 nếu B0 chưa kín.
void sub2() {
    int r1 = m + 1, r2 = 0, c1 = n + 1, c2 = 0;
    for (auto& c : cells) {
        r1 = min(r1, c.first); r2 = max(r2, c.first);
        c1 = min(c1, c.second); c2 = max(c2, c.second);
    }
    int H0 = r2 - r1 + 1, W0 = c2 - c1 + 1;
    int a = r1 - 1, b = m - r2, c = c1 - 1, d = n - c2;
    int V = a + b, Hh = c + d;                  // số bước mở rộng theo hàng, theo cột
    long long areaB = (long long)H0 * W0;
    int score = V + Hh + (areaB > (long long)cells.size() ? 1 : 0);
    // giai thừa tới max(m, n)
    int F = max(m, n) + 1;
    vector<long long> fact(F + 1, 1), inv(F + 1, 1);
    for (int i = 1; i <= F; i++) fact[i] = fact[i - 1] * i % MOD;
    inv[F] = pw(fact[F], MOD - 2);
    for (int i = F; i >= 1; i--) inv[i - 1] = inv[i] * i % MOD;
    auto C = [&](int x, int y) { return fact[x] * inv[y] % MOD * inv[x - y] % MOD; };
    // dp[i][j]: tổng số cách sau i bước dọc và j bước ngang
    // bước dọc thêm một hàng dài W0 + j: (W0+j)! cách tô; bước ngang thêm một cột dài H0 + i: (H0+i)! cách
    vector<long long> dp(Hh + 1, 0);
    dp[0] = 1;
    for (int i = 0; i <= V; i++)
        for (int j = 0; j <= Hh; j++) {
            if (i == 0 && j == 0) continue;
            long long v = 0;
            if (i > 0) v += dp[j] * fact[W0 + j] % MOD;       // dp[i-1][j]
            if (j > 0) v += dp[j - 1] * fact[H0 + i] % MOD;   // dp[i][j-1]
            dp[j] = v % MOD;
        }
    long long ways = dp[Hh] * C(V, a) % MOD * C(Hh, c) % MOD;
    for (long long x = 1; x <= areaB - (long long)cells.size(); x++) ways = ways * (x % MOD) % MOD;
    printf("%d\n%lld\n", score, ways % MOD);
}

int main() {
    freopen("TOBANG.INP", "r", stdin);
    freopen("TOBANG.OUT", "w", stdout);
    scanf("%d %d %d", &m, &n, &k);
    for (int i = 0; i < k; i++) {
        int x, y; scanf("%d %d", &x, &y);
        cells.push_back({x, y});
    }
    sort(cells.begin(), cells.end());
    cells.erase(unique(cells.begin(), cells.end()), cells.end());
    if ((long long)m * n - (long long)cells.size() <= 8) sub1();
    else sub2();
    return 0;
}

Bình luận

Hãy đọc nội quy trước khi bình luận.


Không có bình luận tại thời điểm này.