Lưới tổ ong (Tin học trẻ toàn quốc 2025 – Vòng khu vực miền Nam – Bảng B)
Đề bài
Lưới tổ ong kích thước $N$ gồm $2N - 1$ hàng ngang, đánh số từ trên xuống. Hàng trên cùng có $N$ ô, mỗi hàng sau có nhiều hơn hàng trước 1 ô cho tới hàng giữa ($2N - 1$ ô), rồi mỗi hàng sau lại ít hơn hàng trước 1 ô cho tới hàng dưới cùng ($N$ ô). Mỗi ô ghi một số nguyên.
Lưới tổ ong kích thước 3 (hình mình vẽ lại).
Một đường đi xuất phát từ một ô ở hàng trên cùng, kết thúc ở một ô ở hàng dưới cùng; mỗi bước chỉ được đi chéo xuống sang ô kề ở hàng ngay dưới, phía trái hoặc phía phải (theo các mũi tên).
Trước khi chọn đường, bạn được phép đổi chỗ hai số nằm trên cùng một hàng ngang, nhiều nhất một lần (cho toàn bộ lưới).
Yêu cầu: chọn $K$ đường đi ($0 < K \le 2$) đôi một không giao nhau (không có ô chung) sao cho tổng các số trên các ô chúng đi qua là lớn nhất (có tính tới khả năng đổi chỗ một lần nói trên).
Dữ liệu vào:
- Dòng đầu: hai số nguyên $N, K$ ($1 < N < 100$; $0 < K \le 2$).
- $2N - 1$ dòng tiếp theo: mỗi dòng là các số trên một hàng ngang, từ trên xuống dưới, từ trái sang phải. Các số là số nguyên trong đoạn $[0, 10^9]$.
Kết quả: một số nguyên là tổng lớn nhất của $K$ đường đi.
Subtask: Subtask 1 (50%): $K = 1$; Subtask 2 (50%): $K = 2$.
Giới hạn thời gian: 1 giây. Dữ liệu nhập từ bàn phím, in ra màn hình.
Ghi chú: đề gốc ghi "tổng độ dài" nhưng nghĩa đúng là tổng các số trên đường đi (khớp ví dụ). Khi $K = 2$, phép đổi chỗ là một lần chung cho cả lưới, không phải mỗi đường một lần.
3 1
1 2 3
3 2 2 1
4 2 8 0 3
5 3 1 2
3 1 4223 2
1 2 3
3 2 2 1
4 2 8 0 3
5 3 1 2
3 1 436Giải thích: Ví dụ 1: đổi chỗ hai số $5$ và $1$ ở hàng thứ 4 (hàng 4 thành 1 3 5 2), rồi đi đường $3 \to 2 \to 8 \to 5 \to 4$ (các ô tô xám trong hình) được tổng $22$. Ví dụ 2 cùng lưới nhưng cần 2 đường không chung ô, tổng lớn nhất là $36$.
Lời mở đầu
Bài QHĐ trên lưới quen thuộc ("đi từ trên xuống, tổng lớn nhất") nhưng có hai "gia vị": một lần đổi chỗ và hai đường đi không giao nhau. Mỗi gia vị được xử lý bằng một kỹ thuật kinh điển: thêm một chiều trạng thái "đã dùng phép đổi chưa", và QHĐ trên cặp vị trí của hai đường đi cùng lúc. Mình đi từ cách dễ nghĩ nhất cho $K = 1$ tới lời giải đầy đủ.
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 có gì?
Bộ test trên CLUE do mình tự sinh theo đúng giới hạn và tỉ lệ subtask của đề: 22 test (test 1, 2 là hai ví dụ của đề), 11 test $K = 1$ và 11 test $K = 2$. Có các kiểu dữ liệu đặc biệt: toàn số 0, toàn $10^9$, mỗi hàng có một số rất lớn nằm ở mép (phép đổi chỗ rất có lợi)...
| Test | $N$ | $K$ | Số ô | Kiểu dữ liệu | Đáp án |
|---|---|---|---|---|---|
| 1 | 3 | 1 | 19 | ví dụ 1 | 22 |
| 3 | 2 | 1 | 7 | nhỏ (số 0..9) | 21 |
| 4 | 4 | 1 | 37 | nhỏ (số 0..9) | 56 |
| 5 | 10 | 1 | 271 | ngẫu nhiên | 14.793.514.278 |
| 6 | 50 | 1 | 7.351 | "gai": mỗi hàng có 1 số rất lớn ở mép | 31.563.831.221 |
| 7 | 99 | 1 | 29.107 | ngẫu nhiên | 148.601.495.531 |
| 8 | 99 | 1 | 29.107 | toàn $10^9$ | 197.000.000.000 |
| 9 | 99 | 1 | 29.107 | toàn 0 | 0 |
| 10 | 99 | 1 | 29.107 | "gai" ở mép | 68.318.734.054 |
| 11 | 99 | 1 | 29.107 | số lớn ở 2 mép | 104.605.553.040 |
| 12 | 98 | 1 | 28.519 | số $10^9$ ở giữa mỗi hàng | 195.000.000.000 |
| 2 | 3 | 2 | 19 | ví dụ 2 | 36 |
| 13 | 2 | 2 | 7 | nhỏ (số 0..9) | 22 |
| 14 | 2 | 2 | 7 | ngẫu nhiên | 2.186.146.183 |
| 15 | 5 | 2 | 61 | nhỏ (số 0..9) | 131 |
| 16 | 30 | 2 | 2.611 | ngẫu nhiên | 88.258.290.297 |
| 17 | 99 | 2 | 29.107 | ngẫu nhiên | 297.464.013.858 |
| 18 | 99 | 2 | 29.107 | toàn $10^9$ | 394.000.000.000 |
| 19 | 99 | 2 | 29.107 | toàn 0 | 0 |
| 20 | 99 | 2 | 29.107 | "gai" ở mép | 110.256.128.875 |
| 21 | 99 | 2 | 29.107 | số lớn ở 2 mép | 220.777.087.124 |
| 22 | 98 | 2 | 28.519 | số $10^9$ ở giữa mỗi hàng | 195.147.109.961 |
Đáp án lớn nhất cỡ $2 \cdot 197 \cdot 10^9 \approx 3{,}9 \cdot 10^{11}$ (hai đường, mỗi đường 197 ô) nên phải dùng long long.
Kết quả chạy thật các cách bên dưới theo từng subtask:
| Subtask | Điều kiện | Test | Cách 1 thử từng hàng | Cách 2 QHĐ 2 trạng thái | Cách 3 QHĐ cặp ô C++ | Cách 3 Python |
|---|---|---|---|---|---|---|
| Subtask 1 (50%) | $K = 1$ | 1, 3–12 | 11/11 | 11/11 | 11/11 | 11/11 |
| Subtask 2 (50%) | $K = 2$ | 2, 13–22 | 1/11 | 1/11 | 11/11 | 5/11 |
Nhận xétĐổi chỗ thế nào mới có lợi?
Đường đi (hoặc hai đường đi) chỉ dùng một ô (hoặc hai ô) trên mỗi hàng. Phép đổi chỗ trên hàng $r$ chỉ làm tổng thay đổi khi nó đưa một số từ ô ngoài đường vào ô trên đường. Vậy:
- $K = 1$: đổi ở hàng $r$ tốt nhất là mang số lớn nhất của hàng $mx_r$ vào ô đường đi đang dùng. Nói cách khác: khi đi qua hàng được chọn để đổi, ô đó được tính là $mx_r$ thay vì $a[r][j]$. (Nếu ô đó đã là số lớn nhất thì đổi không có lợi, cũng không có hại.)
- $K = 2$: hai đường đi qua ô $j_1, j_2$ của hàng $r$. Đổi chỗ tốt nhất là đưa số lớn nhất trong các ô còn lại (khác $j_1, j_2$) vào thay số nhỏ hơn trong hai ô $a[r][j_1], a[r][j_2]$. Muốn biết "số lớn nhất ngoài $j_1, j_2$" nhanh thì chỉ cần nhớ 3 ô lớn nhất của mỗi hàng – trong 3 ô đó chắc chắn có một ô khác $j_1$ và $j_2$.
Còn cách di chuyển: đánh số hàng $r = 0..2N-2$, ô trong hàng $j = 0, 1, \dots$ từ trái sang. Ở nửa trên ($r < N - 1$, hàng dưới dài hơn) ô $j$ đi xuống được ô $j$ và $j + 1$; ở nửa dưới (hàng dưới ngắn hơn) ô $j$ đi xuống được ô $j - 1$ và $j$ (bỏ các ô nằm ngoài hàng).
Cách 1Thử từng hàng để đổi – O(N³) (Subtask 1)
Với $K = 1$, theo nhận xét: thử lần lượt "không đổi" và "đổi ở hàng $sw$" với mọi $sw$. Đổi ở hàng $sw$ thì coi mọi ô của hàng $sw$ có giá trị $mx_{sw}$ (đường đi dùng ô nào ở hàng đó thì ô đó cũng nhận được $mx_{sw}$). Với mỗi lưới như vậy, chạy QHĐ đường đi tổng lớn nhất quen thuộc:
$$f[r+1][k] = \max\big(f[r][j]\big) + b[r+1][k] \quad \text{với mọi ô } j \text{ ở hàng } r \text{ đi xuống được ô } k.$$
- Hàm
bestPathlà QHĐ "đẩy xuống": từ mỗi ô $(r, j)$ cập nhật cho 2 ô kề ở hàng dưới. Biếnk1là ô kề bên trái ($j$ ở nửa trên, $j - 1$ ở nửa dưới), ô kề bên phải làk1 + 1. - Có $2N - 1 \approx 200$ hàng để thử, mỗi lần QHĐ trên khoảng $3N^2 \approx 30\,000$ ô → khoảng $6 \cdot 10^6$ phép tính, rất nhanh.
- Cách này chỉ đúng cho $K = 1$ (code vẫn chạy với $K = 2$ nhưng in sai).
Kết quả chạy thật: Bản C++ đúng 12/22 test, sai test 2, 13–18, 20–22 (các test đúng chạy lâu nhất 0.028s) → 50 điểm (đúng toàn bộ subtask 1; test 19 của subtask 2 đúng "ăn may" vì lưới toàn số 0).
Cách 2Thêm trạng thái "đã đổi chưa" – O(N²) (Subtask 1)
Thay vì chạy QHĐ $2N$ lần, ta cho QHĐ "nhớ" thêm việc đã dùng phép đổi hay chưa:
- $f[j][0]$ = tổng lớn nhất của đường đi tới ô $j$ của hàng hiện tại, chưa dùng phép đổi;
- $f[j][1]$ = tổng lớn nhất tới ô $j$, đã dùng phép đổi ở một hàng nào đó (kể cả hàng hiện tại).
Đi từ ô $j$ (hàng $r$) xuống ô $k$ (hàng $r + 1$):
$$g[k][0] = \max\big(f[j][0] + a[r+1][k]\big), \qquad g[k][1] = \max\big(f[j][1] + a[r+1][k],\ \ f[j][0] + mx_{r+1}\big).$$
Vế thứ hai của $g[k][1]$ chính là "dùng phép đổi ở hàng $r + 1$". Hàng đầu tiên: $f[j][0] = a[0][j]$, $f[j][1] = mx_0$. Em bấm Tiếp để xem bảng $f$ được tính dần từng hàng (mỗi ô hiện f0 | f1), cuối cùng là đường đi tốt nhất và chỗ đổi:
- Chỉ cần 2 mảng một chiều
f(hàng hiện tại) vàg(hàng dưới) vì mỗi hàng chỉ phụ thuộc hàng ngay trên. - Đáp án là giá trị lớn nhất của cả
f[j][0]lẫnf[j][1]ở hàng cuối (không bắt buộc phải đổi).
Kết quả chạy thật: Bản C++ đúng 12/22 test, sai test 2, 13–18, 20–22 (các test đúng chạy lâu nhất 0.005s). Bản Python đúng 12/22 test, sai test 2, 13–18, 20–22 (các test đúng chạy lâu nhất 0.040s). Vẫn 50 điểm – nhưng kỹ thuật "thêm chiều trạng thái" này chính là chìa khoá cho $K = 2$.
Cách 3QHĐ trên cặp ô của hai đường – O(N³) (full)
Với $K = 2$, hai đường đi đi xuống song song, mỗi hàng mỗi đường dùng một ô. Vì hai đường không chung ô và mỗi bước chỉ dịch tối đa một ô, đường bên trái luôn ở bên trái (không thể "vượt" qua nhau mà không chung ô). Vậy trạng thái QHĐ là:
$$f[j_1][j_2][u] \quad (j_1 < j_2):\ \text{tổng lớn nhất khi ở hàng hiện tại đường trái ở ô } j_1, \text{ đường phải ở ô } j_2, \ u = \text{đã đổi chưa}.$$
Từ $(j_1, j_2)$ mỗi đường có 2 lựa chọn ô ở hàng dưới → 4 cặp $(k_1, k_2)$, chỉ giữ cặp có $k_1 < k_2$. Gọi giá trị hàng $r+1$ khi đi qua $(k_1, k_2)$ là $u_0 = a[k_1] + a[k_2]$ (không đổi) và $u_1 = u_0 - \min(a[k_1], a[k_2])$ $+ (\text{số lớn nhất ngoài } k_1, k_2)$ (đổi ở hàng này, lấy max với $u_0$). Chuyển trạng thái:
$$g[k_1][k_2][0] \leftarrow f[j_1][j_2][0] + u_0, \qquad g[k_1][k_2][1] \leftarrow \max\big(f[j_1][j_2][0] + u_1,\ \ f[j_1][j_2][1] + u_0\big).$$
Code đầy đủ gồm solve1 (giống cách 2) cho $K = 1$ và solve2 cho $K = 2$; hàm rowVal tính $u_0, u_1$ nhờ 3 ô lớn nhất của hàng (top).
- Mỗi hàng có tối đa $2N - 1 \approx 197$ ô nên khoảng $197^2 / 2 \approx 19\,000$ cặp, nhân 4 cách đi, nhân 197 hàng ≈ $1{,}5 \cdot 10^7$ phép tính – chạy khoảng 0,1 giây.
- Bảng
flưu phẳng trongvectormột chiều cỡ $W \cdot W \cdot 2$ với $W = 2N$; giá trịNEG = -1đánh dấu trạng thái không đạt được (mọi tổng thật đều $\ge 0$). - Không cần xét đổi chỗ giữa chính hai ô $j_1, j_2$ (chỉ chuyển số giữa hai đường, tổng không đổi).
Kết quả chạy thật: Bản C++ đúng 22/22 test, chạy lâu nhất 0.096s. Full điểm. Bản Python đúng 16/22 test, quá thời gian test 17–22 (các test đúng chạy lâu nhất 0.270s) – bản Python dùng từ điển cho các cặp, với $N = 99$, $K = 2$ thì $1{,}5 \cdot 10^7$ bước là quá sức Python.
Tổng kếtKết quả chạy thật trên bộ test
| Cách | Ngôn ngữ | Đúng | Lâu nhất (test đúng) | Điểm theo subtask |
|---|---|---|---|---|
| 1. Thử từng hàng đổi + QHĐ (K = 1) | C++ | 12/22 | 0.028s | 50% |
| 2. QHĐ thêm trạng thái "đã đổi" (K = 1) | C++ | 12/22 | 0.005s | 50% |
| Python | 12/22 | 0.040s | 50% | |
| 3. QHĐ trên cặp ô (K = 1 và 2) | C++ | 22/22 | 0.096s | 100% |
| Python | 16/22 | 0.270s | 50% |
Nhận xét
Đăng nhận xét