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