Tạo xâu đối xứng
Đề bài
Cho xâu $S$ độ dài $M$ chỉ gồm các chữ cái trong tập hợp $N$ chữ cái latin in thường cho trước. Mỗi chữ cái có chi phí chèn thêm và chi phí loại bỏ.
Hãy xác định tổng chi phí ít nhất để biến $S$ thành một xâu đối xứng.
Dữ liệu:
- Dòng 1: hai số nguyên $N, M$ $(1 \le N \le 26;\ 1 \le M \le 2000)$.
- Dòng 2: xâu $S$ $(|S| = M)$.
- Dòng $3 \ldots N + 2$: dòng $2 + i$ chứa một kí tự chữ cái $C$ và hai số nguyên $A, B$ là chi phí chèn / loại của kí tự $C$ $(0 \le A, B \le 10000)$. Không có hai dòng nào chứa cùng kí tự $C$. Các kí tự có trong $S$ đều có trong $N$ dòng này.
Kết quả: một số nguyên là tổng chi phí nhỏ nhất.
3 4
abcb
a 1000 1100
b 350 700
c 200 800900Giải thích ví dụ: từ abcb chèn thêm bcb vào đầu (chi phí $350 + 200 + 350 = 900$) được bcbabcb – đối xứng. Nếu xoá a (chi phí 1100) hay chèn thêm a vào cuối (chi phí 1000) đều đắt hơn.
Lời mở đầu
Bài này là bản tiếng Việt của bài Cheapest Palindrome (USACO 2007). Đây là bài QHĐ trên đoạn rất kinh điển, giống bài "thêm ít ký tự nhất để được xâu đối xứng" nhưng có thêm chi phí. Mình sẽ đi từ đệ quy thử mọi cách, sang đệ quy có nhớ, rồi QHĐ bảng – và giải thích kỹ mẹo "chèn hay xoá đều như nhau, chỉ cần lấy cái rẻ hơn".
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 OJBạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.
Nộp bài trên CodeforcesLộ 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ì?
| Test | Số test | $N$ | $M$ | Chi phí $A, B$ | Đáp án |
|---|---|---|---|---|---|
| 1–30 | 30 | 4 | 4 – 10 | 1 – 99 | 0 – 67 |
| 31–100 | 70 | 26 | 2.000 | 1 – 9.990 | 65.322 – 1.139.795 |
Đề không ghi subtask, nhưng bộ test chia rất rõ thành 2 mức: 30 test đầu xâu rất ngắn ($M \le 10$, chỉ dùng 4 chữ cái a–d; có 1 test đáp án 0 vì xâu đã đối xứng sẵn), 70 test sau đều có $M = 2000$ – lớn nhất có thể – với đủ 26 chữ cái. Đáp án lớn nhất cỡ $1{,}1 \cdot 10^6$, kiểu int là đủ (tối đa $2000 \cdot 10^4 = 2 \cdot 10^7$).
| Mức | Điều kiện | Test | Cách 1 đệ quy | Cách 2 đệ quy có nhớ | Cách 3 QHĐ bảng |
|---|---|---|---|---|---|
| 1 | $M \le 10$ | 1–30 | qua | qua | qua |
| 2 | $M = 2000$ | 31–100 | quá thời gian | qua | qua |
Ý tưởngXử lý 2 đầu xâu, chèn hay xoá chỉ cần chọn cái rẻ hơn
Xét đoạn $S[l..r]$. Để nó đối xứng, ký tự đầu và ký tự cuối phải "ghép cặp" được với nhau:
- Nếu $S[l] = S[r]$: 2 đầu đã khớp, chỉ cần làm cho phần giữa $S[l+1..r-1]$ đối xứng.
- Nếu $S[l] \ne S[r]$: ít nhất một đầu phải được xử lý. Xử lý đầu trái $S[l] = c$ có 2 cách: xoá nó (tốn $B_c$), hoặc chèn thêm một chữ $c$ vào ngay sau $S[r]$ để ghép cặp với nó (tốn $A_c$). Cả 2 cách đều đưa về cùng một bài toán con $S[l+1..r]$! Vậy chi phí xử lý ký tự $c$ chỉ cần là $$cost[c] = \min(A_c, B_c)$$ Tương tự với đầu phải $S[r]$ → bài toán con $S[l..r-1]$.
Ô $f[l][r]$ cần các ô ở hàng $l + 1$ (bên dưới) và ô bên trái cùng hàng, nên ta điền hàng $l$ từ dưới lên, trong mỗi hàng điền $r$ từ trái sang phải. Em sửa xâu và chi phí rồi bấm Tiếp (ô vàng đang tính, ô tím là $f[l+1][r]$ hoặc $f[l+1][r-1]$, ô cam là $f[l][r-1]$):
Cách 1Đệ quy thử 2 đầu – O(2ᴹ)
Viết thẳng công thức trên thành hàm đệ quy Try(l, r). Mảng cost đánh chỉ số bằng chính ký tự (kiểu char đổi ra mã ASCII) nên tra rất tiện.
Kết quả chạy thật: đúng 30/100 test – qua trọn mức 1 ($M \le 10$). Khi 2 đầu khác nhau, hàm gọi 2 nhánh, mỗi nhánh chỉ bớt 1 ký tự, nên số lời gọi tới cỡ $2^M$: với $M = 2000$ thì không bao giờ xong → quá thời gian cả 70 test còn lại.
Cách 2Đệ quy có nhớ – O(M²)
Hàm Try(l, r) chỉ phụ thuộc 2 tham số, nên chỉ có khoảng $\frac{M^2}{2} = 2 \cdot 10^6$ trạng thái khác nhau – thế mà cách 1 gọi tới $2^M$ lần, tức là mỗi trạng thái bị tính lặp lại vô số lần. Ta lưu kết quả vào memo[l][r]:
memo nháy lên khi một lời gọi lấy lại kết quả đã lưu.memset(memo, -1, sizeof memo)đánh dấu "chưa tính" – chi phí thật luôn $\ge 0$ nên không nhầm.- Độ sâu đệ quy tối đa khoảng $M = 2000$ tầng, an toàn.
- Mảng
memo$2005 \times 2005$ sốinttốn 16 MB.
Kết quả chạy thật: đúng 100/100 test, chậm nhất 0,04 giây. Full điểm!
Cách 3QHĐ bảng – O(M²)
Chủ động điền bảng thay vì đệ quy: $l$ đi từ $M - 1$ về 0, $r$ đi từ $l + 1$ lên $M - 1$. Khi tính $f[l][r]$ thì hàng $l + 1$ đã xong và $f[l][r-1]$ vừa tính ngay trước.
- Các ô $f[l][l] = 0$ (1 ký tự) và $f[l+1][l] = 0$ (đoạn rỗng, dùng khi $r = l + 1$ và $S[l] = S[r]$) có sẵn vì mảng toàn cục mặc định bằng 0.
- Bản Python chỉ giữ 2 hàng
nxt(hàng $l + 1$) vàcur(hàng $l$) để đỡ tốn bộ nhớ.
Kết quả chạy thật: bản C++ đúng 100/100 test, chậm nhất 0,02 giây. Bản Python đúng 100/100 test – chậm nhất 0,72 giây (khoảng $2 \cdot 10^6$ phép so sánh, Python vẫn kịp nhờ chỉ dùng list và biến cục bộ).
Nhận xét
Đăng nhận xét