Chọn quà
Đề bài
Để khích lệ học sinh tích cực học tập, cô giáo lớp An dùng các nhãn dán khen thưởng khi tổ chức các hoạt động trên lớp. Cuối năm, cô khen thưởng cho các bạn tích lũy được nhiều nhãn. Với sự nỗ lực, An đã tích lũy được $K$ nhãn.
Cô chuẩn bị một khung lưới hình chữ nhật có kích thước $D \times R$. Khung lưới này có $D$ đường ngang và $R$ đường dọc tạo thành $(D-1) \times (R-1)$ hình vuông nhỏ có chiều dài cạnh là 1 đơn vị. Các món quà được treo tại các mối nối giao nhau của những đường ngang và đường dọc trên lưới. Tại một mối nối có thể không có quà hoặc có nhiều nhất một món quà.
Để nhận quà, An được phát cho một khung vợt hình vuông có kích thước $K \times K$ (cách tính đơn vị như khung lưới). An đặt vợt trên khung lưới sao cho 4 cạnh của vợt phải nằm hoàn toàn trên các đường ngang, đường dọc của khung lưới. Khi đó An sẽ nhận các món quà nằm hoàn toàn bên trong 4 cạnh của vợt (các món quà nằm trên cạnh của vợt sẽ không được tính). Bạn hãy giúp An xác định số lượng lớn nhất các món quà mà An có thể nhận.
Yêu cầu: viết chương trình cho biết số lượng món quà lớn nhất mà An có thể nhận khi đặt vợt trên khung lưới.
Dữ liệu: vào từ tập tin văn bản CHONQUA.INP, gồm:
- Dòng đầu tiên chứa 3 số nguyên $D, R, K$ $(3 \le K \le D, R \le 1000)$ cách nhau một khoảng trắng.
- Trên $D$ dòng tiếp theo, mỗi dòng chứa $R$ kí tự mô tả hình ảnh khung lưới với các món quà treo trên đó. Kí tự
'*'cho biết vị trí một món quà, kí tự'.'cho biết vị trí một mối nối trên khung lưới.
Kết quả: ghi ra tập tin văn bản CHONQUA.OUT một số nguyên cho biết số lượng món quà lớn nhất mà An có thể nhận khi đặt vợt trên khung lưới để nhận quà.
Ràng buộc: 50% test ứng với 50% số điểm của bài có $3 \le D, R \le 100$.
7 6 4
......
.*.*.*
......
.*.*..
..*...
..*...
*....*2Chú thích ví dụ (hình trong đề): một cách đặt vợt giúp An lấy tối đa 2 món quà. Lưu ý không tính những món quà nằm trên cạnh của vợt.
...... .*.*.* +--+.. |*.|.. |.*|.. +--+.. *....*
Lưu ý trên CLUE OJ: bài này đọc/ghi file CHONQUA.INP / CHONQUA.OUT đúng như đề thi. Code bên dưới dùng mẹo if (fopen(...)): có file thì đọc/ghi file, không có thì đọc bàn phím – in màn hình, nên chạy thử trên máy hay nộp bài đều được.
Lời mở đầu
Đây là bài 3 trong đề thi học sinh giỏi Tin học lớp 9 TP.HCM năm 2022. Đọc đề thì hơi rối vì có "đường ngang", "đường dọc", "mối nối", "khung vợt"... nhưng gỡ ra thì bài chỉ là: tìm hình vuông con có nhiều dấu * nhất trong một bảng ký tự. Bài này là ví dụ kinh điển để các bạn làm quen với tổng tiền tố 2 chiều (prefix sum 2D) – một kỹ thuật cực kỳ hay gặp trong các đề thi HSG. Mình sẽ đi từ cách vét cạn (lấy 50% điểm) rồi tối ưu dần lên cách đạt điểm tuyệt đối nha.
Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.
Nộp bài trên CLUE OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Bộ testBộ test chính thức có gì?
Bộ test có 100 test, mình kiểm tra trực tiếp thì đúng như ràng buộc của đề: 50 test đầu có $D, R \le 100$, 50 test sau có $D, R$ lên tới 1000 (trong đó chỉ test 58 là còn nhỏ). Mỗi ký tự trong lưới chỉ là . hoặc *, quà chiếm khoảng một nửa số mối nối.
| Test | D | R | K | Số quà | Đáp án | Ghi chú |
|---|---|---|---|---|---|---|
| 1 – 10 | 3 – 9 | 4 – 10 | 3 – 5 | 11 – 30 | 1 – 8 | Nhỏ, kiểm tra tay được |
| 11 – 50 | 4 – 97 | 4 – 99 | 3 – 72 | 175 – 3848 | 1 – 2475 | Ngẫu nhiên vừa |
| 51 – 90 | 9 – 964 | 17 – 998 | 5 – 671 | 1071 – 434254 | 9 – 224269 | Ngẫu nhiên lớn |
| 91 – 100 | 1000 – 1000 | 1000 – 1000 | 100 – 1000 | 0 – 1000000 | 0 – 996004 | Lớn nhất: 1000 × 1000 |
| Subtask | Điểm | Điều kiện | Test | Cách đạt |
|---|---|---|---|---|
| 1 | 50% | $D, R \le 100$ | 1 – 50 | Cách 1, Cách 2 |
| 2 | 50% | $D, R \le 1000$ | 51 – 100 | Cách 2 |
Vài test đặc biệt đáng chú ý: test 99 ($K = 900$) không có món quà nào nên đáp án là 0; test 100 có $D = R = K = 1000$ và toàn bộ là quà, vợt chỉ có đúng 1 vị trí đặt, đáp án là $998^2 = 996004$. Test 91 – 100 đều là lưới $1000 \times 1000$ với $K = 100, 200, \dots, 1000$ – đây là các test "giết" cách vét cạn: số phép tính của vét cạn lên tới khoảng $6.2 \cdot 10^{10}$.
Ý tưởngGỡ rối đề: vợt K × K "ôm" được bao nhiêu mối nối?
Mỗi ký tự trong input là một mối nối (giao điểm của một đường ngang và một đường dọc), chứ không phải một ô vuông. Khung vợt $K \times K$ có 4 cạnh nằm trên đường lưới, nên mỗi cạnh của vợt đi qua đúng $K$ mối nối (giống khung lưới $D \times R$ có $D$ đường ngang). Ví dụ trong đề có $K = 4$: cạnh vợt là +--+, đi qua 4 mối nối.
- Nếu góc trên trái của vợt nằm ở mối nối hàng $i$, cột $j$ thì vợt phủ các hàng $i \dots i + K - 1$ và các cột $j \dots j + K - 1$.
- Bỏ đi 4 cạnh (hàng đầu, hàng cuối, cột đầu, cột cuối) thì phần bên trong vợt là hình vuông các hàng $i + 1 \dots i + K - 2$, các cột $j + 1 \dots j + K - 2$, tức là hình vuông $(K - 2) \times (K - 2)$ mối nối.
- Vợt phải nằm trọn trong khung lưới nên $1 \le i \le D - K + 1$ và $1 \le j \le R - K + 1$. Hệ quả: phần bên trong vợt không bao giờ chạm được hàng 1, hàng $D$, cột 1, cột $R$ (quà ở viền khung lưới không bao giờ lấy được).
* trong hình vuông bên trong $(i+1, j+1) \to (i+K-2, j+K-2)$, lấy giá trị lớn nhất.Em sửa khung lưới, chọn $K$ rồi bấm Tiếp để xem vợt (khung cam) trượt qua mọi vị trí. Quà xanh là quà được nhận, quà đỏ nằm trên cạnh vợt nên không được tính, khung xanh nét đứt là vị trí tốt nhất tới lúc đó:
Câu hỏi còn lại chỉ là: đếm số dấu * trong một hình vuông cho nhanh. Đếm thủ công từng mối nối thì được 50% điểm, dùng tổng tiền tố 2 chiều thì đạt điểm tối đa.
Cách 1Vét cạn: đếm trực tiếp từng mối nối – O(D·R·K²)
Làm đúng như ý tưởng: 2 vòng lặp chọn góc trên trái $(i, j)$ của vợt, 2 vòng lặp nữa duyệt từng mối nối bên trong để đếm. Code dùng chỉ số từ 0 cho gọn với string.
- Điều kiện vòng lặp
i + K - 1 < Dnghĩa là cạnh dưới của vợt (hàng $i + K - 1$) vẫn còn nằm trong khung lưới. Tương tự choj. - Hai vòng trong chạy
xtừi + 1tớii + K - 2vàytừj + 1tớij + K - 2: chính là phần bên trong vợt, đã bỏ 4 cạnh. - Bản Python dùng
a[x].count('*', j + 1, j + K - 1)để đếm cả một đoạn của hàng trong một lần gọi (nhanh hơn vòng lặp tay), nhưng về bản chất vẫn là vét cạn.
Số phép tính là $(D - K + 1)(R - K + 1)(K - 2)^2$. Với subtask 1 ($D, R \le 100$) con số này lớn nhất chỉ khoảng $3{,}3 \cdot 10^6$ trên bộ test – chạy vèo vèo. Nhưng với $D = R = 1000$, $K = 500$ thì là $501^2 \cdot 498^2 \approx 6{,}2 \cdot 10^{10}$ – quá chậm.
Cách 2Tổng tiền tố 2 chiều – O(D·R)
Gọi $s[i][j]$ là số quà trong hình chữ nhật từ mối nối $(1, 1)$ đến mối nối $(i, j)$ (đánh số từ 1, hàng 0 và cột 0 của mảng $s$ toàn số 0). Bảng $s$ tính được trong một lượt duyệt nhờ công thức "bao hàm – loại trừ":
Có bảng $s$ rồi, số quà trong hình chữ nhật bất kỳ $(x_1, y_1) \to (x_2, y_2)$ tính bằng 4 phép cộng trừ:
Với vợt có góc trên trái $(i, j)$ thì $x_1 = i + 1$, $y_1 = j + 1$, $x_2 = i + K - 2$, $y_2 = j + K - 2$. Em bấm Tiếp để xem 4 ô của bảng $s$ được dùng cho từng vị trí vợt (ô xanh mang dấu +, ô đỏ mang dấu −):
- Mảng
skhai báo toàn cục nên tự động bằng 0 – hàng 0 và cột 0 chính là "lề" giúp công thức không bị truy cập chỉ số $-1$. - Ký tự thứ $j$ của hàng (đánh số từ 1) là
row[j - 1]vìstringđánh số từ 0. Biểu thức(row[j - 1] == '*')có giá trị 1 hoặc 0. - Kết quả lớn nhất là $(K - 2)^2 \le 998^2 < 10^6$ nên kiểu
intlà đủ. - Bản Python: mỗi hàng của $s$ là hàng trên cộng với tổng dồn của hàng hiện tại (
accumulate); khi truy vấn, với mỗi $i$ mình tính sẵn hiệu 2 hàngd = s[i + K - 2] - s[i]rồi lấymax(d[j + K - 2] - d[j])bằngmapđể chạy nhanh hơn vòng lặp tay.
Tổng kếtSo sánh 2 cách
| Cách | Độ phức tạp | C++ | Python | Subtask đạt |
|---|---|---|---|---|
| 1. Vét cạn | $O(D \cdot R \cdot K^2)$ | 75/100 | 65/100 | Subtask 1 (50%) |
| 2. Tổng tiền tố 2D | $O(D \cdot R)$ | 100/100 | 100/100 | Cả 2 subtask (100%) |
Nhận xét
Đăng nhận xét