Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Kí tự

Kí tự
Nguồn: CLUE OJ – Chương 22: Quy hoạch động (Đề thi HSG Tin lớp 9 TP.HCM 2020) Độ khó: Trung bình Quy hoạch động Dãy con tăng dài nhất

Đề bài

Cho xâu kí tự $S$ có chiều dài tối đa 1000 kí tự gồm các kí tự chữ cái cả hoa và thường.

Yêu cầu: hãy tìm xâu kí tự $T$ theo thứ tự trong $S$ gồm các kí tự hoa tăng dần dài nhất.

Dữ liệu vào: cho trong tập tin văn bản KITU.INP có 1 dòng duy nhất gồm các kí tự.

Kết quả: ghi vào tập tin văn bản KITU.OUT kết quả là số kí tự trong xâu $T$.

Subtask:

  • 20% số test đầu tiên có $N \le 20$.
  • 20% số test tiếp theo có $N \le 50$.
  • 20% số test tiếp theo có $N \le 100$.
  • 40% số test còn lại không có điều kiện gì thêm.

($N$ là độ dài xâu $S$.)

Ví dụ – input
aADbCBcdDCEefDEgFh
Ví dụ – output
6

Giải thích: xâu $S$ = aADbCBcdDCEefDEgFh. Xâu $T$ tăng dần là ABCDEF (lấy trong xâu $S$: A vị trí 2, B vị trí 6, C vị trí 10, D vị trí 14, E vị trí 15, F vị trí 17).

Lưu ý khi nộp bài
Trên CLUE OJ bài này đọc/ghi bằng file KITU.INP / KITU.OUT. Code bên dưới dùng mẫu if(fopen("KITU.INP", "r")){ freopen(...); }: có file thì đọc file, không có file thì vẫn đọc bàn phím bình thường, nên các bạn chạy thử trên máy hay nộp bài đều được.

Lời mở đầu

Bỏ đi lớp áo "xâu kí tự", đây chính là bài toán kinh điển dãy con tăng dài nhất (Longest Increasing Subsequence – LIS): chỉ xét các chữ hoa, chọn ra nhiều chữ nhất (giữ nguyên thứ tự trong xâu) sao cho chữ sau lớn hơn hẳn chữ trước. Bài rất hợp để luyện quy trình của chương quy hoạch động: đi từ đệ quy vét cạn → QHĐ $O(N^2)$ → tận dụng chỉ có 26 chữ cái để còn $O(26N)$.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

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 OJ

Lộ 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Độ dài NSố chữ hoa nhiều nhấtĐáp án
1–202 … 19130 … 5
21–4022 … 49283 … 7
41–6052 … 99515 … 10
61–100534 … 100054119 … 26
  • 100 test, khớp đúng 4 subtask của đề. Ở subtask cuối có 10 test dài đúng 1000 kí tự. Khoảng một nửa số kí tự là chữ hoa.
  • Lưu ý 1: ví dụ của đề không nằm trong bộ test (test 1 là một xâu khác).
  • Lưu ý 2 – "tăng dần" là tăng ngặt: có test chứa hai chữ hoa giống nhau đứng liền nhau mà đáp án vẫn là 1, nên hai chữ bằng nhau không được cùng có mặt trong $T$.
  • Lưu ý 3: có test không có chữ hoa nào (test 9), đáp án là 0 – code phải in 0 chứ không được in 1.
  • Đáp án không bao giờ vượt quá 26 (chỉ có 26 chữ hoa A…Z, mỗi chữ dùng tối đa 1 lần). Có 3 test đạt đúng 26.

Bốn subtask và số test đúng của từng cách (chạy thật):

SubtaskĐiều kiệnCác testCách 1
đệ quy
Cách 2
$O(N^2)$
Cách 3
$O(26N)$
1 (20%)$N \le 20$1 – 2020/2020/2020/20
2 (20%)$N \le 50$21 – 4020/2020/2020/20
3 (20%)$N \le 100$41 – 6020/2020/2020/20
4 (40%)$N \le 1000$61 – 1000/4040/4040/40

(Số test đúng của bản C++, chạy thật với giới hạn 1 giây/test.)

Ý tưởngdp[i] – xâu tốt nhất kết thúc tại vị trí i

Với dãy con tăng, câu hỏi then chốt là: chữ hoa $s[i]$ có thể nối vào sau xâu nào? – vào sau bất kỳ xâu tăng nào kết thúc tại một chữ hoa $s[j]$ đứng trước ($j < i$) và nhỏ hơn ($s[j] < s[i]$). Muốn dài nhất thì chọn xâu dài nhất trong số đó. Vậy ta đặt:

Trạng thái và công thức
$dp[i]$ = độ dài xâu chữ hoa tăng dài nhất kết thúc đúng tại $s[i]$ (chỉ xét khi $s[i]$ là chữ hoa).
dp[i] = 1 + max{ dp[j] : j < i, s[j] hoa, s[j] < s[i] }
(nếu không có $j$ nào thì $dp[i] = 1$ – xâu chỉ gồm $s[i]$). Đáp án là $\max dp[i]$, hoặc 0 nếu xâu không có chữ hoa.

Em nhập xâu rồi bấm Tiếp. Ô viền cam là chữ đang xét, ô tím là các chữ đứng trước mà nhỏ hơn (nối được), ô xanh là chữ có dp lớn nhất được chọn để nối vào. Bước cuối lần ngược lại để in ra chính xâu $T$:

Ví dụ nhanh:

Cách 1Đệ quy chọn / bỏ từng kí tự

Cách "ngây thơ" nhất: đi qua từng kí tự, với mỗi kí tự quyết định chọn (chỉ được chọn khi là chữ hoa lớn hơn chữ vừa chọn trước đó) hoặc bỏ. Đi hết xâu thì cập nhật độ dài lớn nhất. Hàm Try(i, last, len) nhớ chữ hoa vừa chọn last để kiểm tra điều kiện tăng.

C++▶ Chạy tay từng dòng codeCách 1: Try(i, last, len) – chọn s[i] nếu là chữ hoa lớn hơn last, hoặc bỏ qua.
Dữ liệu mẫu:
Ví dụ của đề dài 18 kí tự, đệ quy sinh rất nhiều lời gọi nên khung này dùng các xâu ngắn (tối đa 7 kí tự).
Không gian$O(N)$ (độ sâu đệ quy)
Thời gianSố xâu con tăng – tăng theo hàm mũ

Kết quả chạy thật: bản C++ đúng 60/100 test, quá thời gian ở các test 61–100; test đúng chạy lâu nhất 0,007 s. Bản Python đúng 60/100 test, quá thời gian ở các test 61–100; test đúng chạy lâu nhất 0,117 s. Đệ quy đi qua mọi xâu con tăng, số lượng xâu này tăng theo hàm mũ khi xâu dài; nhờ điều kiện "chữ hoa và lớn hơn" cắt bớt nhiều nhánh nên vẫn qua được tới $N \le 100$, nhưng gặp $N = 1000$ (khoảng 500 chữ hoa) thì chịu thua.

Mẹo nhỏ: kết quả của Try chỉ phụ thuộc $(i, last)$ – có $1000 \times 27$ trạng thái. Nếu viết lại thành hàm trả về giá trị và nhớ kết quả từng trạng thái (đệ quy có nhớ) thì cũng đạt full điểm. Ở dưới mình đi theo hướng QHĐ lập bảng quen thuộc của bài LIS.

Cách 2Quy hoạch động dp[i] – O(N²) (full điểm)

Cài đặt đúng công thức ở phần ý tưởng: với mỗi chữ hoa $s[i]$, duyệt mọi $j < i$ để tìm xâu tốt nhất nối được.

C++▶ Chạy tay từng dòng codeCách 2: dp[i] = 1 + max dp[j] với j < i, s[j] hoa và nhỏ hơn s[i].
Dữ liệu mẫu:
Mỗi lần kiểm tra của vòng j dừng ở dòng if (gộp với dòng for).
  1. Chữ thường thì continue luôn, $dp[i]$ giữ giá trị 0 – và điều kiện isupper(s[j]) ở vòng trong bảo đảm không bao giờ nối vào chữ thường.
  2. Dùng dấu < (không phải <=) vì "tăng dần" là tăng ngặt.
  3. ans khởi tạo bằng 0 nên xâu không có chữ hoa sẽ in 0.
Không gian$O(N)$
Thời gian$O(N^2)$

Kết quả chạy thật: bản C++ đúng 100/100 test; test đúng chạy lâu nhất 0,006 s; bản Python đúng 100/100 test; test đúng chạy lâu nhất 0,048 s. Với $N \le 1000$ thì $N^2/2 = 5 \cdot 10^5$ phép so sánh – quá nhẹ.

Cách 3Chỉ có 26 chữ cái – O(26·N)

Ở vòng trong của cách 2, ta chỉ cần biết: "trong các chữ hoa đã gặp mà nhỏ hơn $s[i]$, xâu dài nhất kết thúc bằng chúng dài bao nhiêu?". Câu hỏi này chỉ phụ thuộc vào chữ cái, không phụ thuộc vị trí. Nên thay vì mảng $dp$ theo vị trí, ta giữ mảng $f[c]$ theo chữ cái:

Trạng thái theo chữ cái
$f[c]$ = độ dài xâu hoa tăng dài nhất (trong phần xâu đã đọc) kết thúc bằng chữ 'A' + c. Gặp chữ hoa $ch$:
f[ch] = max(f[ch], 1 + max(f['A'], …, f[ch − 1]))
C++▶ Chạy tay từng dòng codeCách 3: f[c] theo chữ cái, mỗi chữ hoa chỉ cần xem tối đa 25 ô.
Dữ liệu mẫu:
Không gian$O(26)$
Thời gian$O(26 \cdot N)$

Kết quả chạy thật: bản C++ đúng 100/100 test; test đúng chạy lâu nhất 0,004 s; bản Python đúng 100/100 test; test đúng chạy lâu nhất 0,022 s. Cách này đọc xâu một lượt, nên kể cả xâu dài $10^6$ kí tự vẫn chạy tốt.

Tổng kếtKết quả chạy thật trên bộ test

CáchNgôn ngữĐúngLâu nhất (test đúng)Ghi chú
1. Đệ quy chọn / bỏC++60/1000,007 sQuá thời gian ở test 61–100
Python60/1000,117 sQuá thời gian ở test 61–100
2. QHĐ dp[i]C++100/1000,006 sFull điểm
Python100/1000,048 sFull điểm
3. QHĐ theo chữ cáiC++100/1000,004 sFull điểm
Python100/1000,022 sFull điểm
Ghi chú của mình: cả 3 cách (C++ và Python) đều được chấm trên đủ 100 test của CLUE. Đệ quy vét cạn ăn được 3 subtask đầu; QHĐ $O(N^2)$ và QHĐ theo chữ cái $O(26N)$ đều full điểm. Hai chỗ hay sai của bài: dùng <= thay vì <, và quên trường hợp không có chữ hoa (phải in 0).
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇

Nhận xét