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

Xâu đặc biệt

Xâu đặc biệt
Nguồn: CLUE OJ – Map/Set Độ khó: Trung bình Map Set Tổng tiền tố

Đề bài

Yên và Định là hai học sinh giỏi bộ môn Tin học. Yên có một nhóm chữ cái tiếng Anh mà bạn ấy yêu thích, ghép các chữ cái này lại ta được xâu $a$. Định cũng vậy, Định có nhóm chữ cái tiếng Anh yêu thích của riêng mình, ghép các chữ cái yêu thích của Định ta được xâu $b$. Khi lấy hai nhóm chữ cái yêu thích của Yên và Định so sánh với nhau thì thấy tất cả chúng đều là chữ cái in thường và không có chữ cái nào cùng thuộc cả hai nhóm.

Yên và Định gọi xâu $x$ là xâu đặc biệt nếu trong xâu $x$ thỏa mãn:

  • Các kí tự trong xâu $x$ hoặc là chữ cái yêu thích của Yên, hoặc là chữ cái yêu thích của Định, hoặc là dấu cách;
  • Số lượng chữ cái thuộc nhóm chữ cái yêu thích của Yên bằng số lượng chữ cái thuộc nhóm yêu thích của Định.

Yêu cầu: Với một xâu $S$ chỉ gồm các chữ cái in thường và dấu cách cho trước. Hãy cho biết có bao nhiêu xâu con liên tiếp của $S$ là xâu đặc biệt theo cách gọi của Yên và Định.

Dữ liệu vào từ file XDB.INP gồm 3 dòng:

  • Dòng 1: chứa xâu $a$ (gồm các chữ cái tiếng Anh in thường yêu thích của Yên);
  • Dòng 2: chứa xâu $b$ (gồm các chữ cái tiếng Anh in thường yêu thích của Định);
  • Dòng 3: chứa xâu $S$.

Kết quả ghi ra file XDB.OUT số lượng xâu con liên tiếp của $S$ là xâu đặc biệt.

Ràng buộc:

  • Có 30% số test ứng với 30% số điểm của bài cho xâu $S$ có độ dài $\le 10^2$;
  • Có 30% số test ứng với 30% số điểm của bài cho xâu $S$ có độ dài $\le 10^4$;
  • Có 40% số test ứng với 40% số điểm của bài cho xâu $S$ có độ dài $\le 10^5$.

Giới hạn: 1 giây, 256 MB. Trên CLUE OJ dữ liệu đọc/ghi qua file XDB.INP / XDB.OUT.

Ví dụ – input
ab
fd
abfe agfdaa
Ví dụ – output
4

Giải thích ví dụ: có 4 xâu thoả mãn là xâu bf; da; fdaa và xâu có một dấu cách (xâu chỉ gồm dấu cách có 0 chữ của Yên và 0 chữ của Định, bằng nhau nên cũng là xâu đặc biệt).

Lời mở đầu

Bài này nghe thì có vẻ là bài xâu, nhưng thực chất là bài tổng tiền tố + đếm bằng map rất quen thuộc: "đếm số đoạn con có tổng bằng 0". Mình sẽ đi từ cách vét cạn cho subtask 1, rồi tối ưu dần. Tất cả các cách đều được chạy thật trên 100 test của CLUE OJ.

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ì?

Bộ test gồm 100 test. Mình gọi chữ "xấu" là chữ cái không thuộc $a$ cũng không thuộc $b$ (một xâu con chứa chữ xấu thì chắc chắn không đặc biệt).

Nhóm testSố testĐộ dài SSố chữ cái
được thích (|a|+|b|)
Số dấu cách
(max)
Số chữ "xấu"Đoạn không có
chữ xấu dài nhất
Đáp án
lớn nhất
1–30305 – 204 – 1950 – 121215
31–603010.000 – 10.0007 – 252.096287 – 5.90225026.551
61–10040100.000 – 100.00026 – 2620.2210 – 0100.0001.617.496
SubtaskĐiều kiện (theo đề)TestThực tế trong bộ test
1 (30%)$|S| \le 10^2$1 – 30$|S|$ từ 5 đến 20
2 (30%)$|S| \le 10^4$31 – 60$|S| = 10^4$, có nhiều chữ "xấu" nên các đoạn tốt ngắn (≤ 250)
3 (40%)$|S| \le 10^5$61 – 100$|S| = 10^5$, cả 26 chữ cái đều được thích → không có chữ xấu nào
3 cái bẫy khi đọc dữ liệu
  • Xâu $S$ có dấu cách, nên phải đọc cả dòng bằng getline, không dùng cin >> s được.
  • Dấu cách ở đầu và cuối dòng cũng tính: có 19 test mà $S$ bắt đầu bằng dấu cách và 22 test kết thúc bằng dấu cách. Tuyệt đối đừng "trim" xâu.
  • Cả 100 test đều được tạo trên Windows nên cuối mỗi dòng có thêm ký tự '\r'. Nếu không bỏ đi, ký tự này bị coi là chữ "xấu" và làm sai kết quả. Trong code mình viết hàm readLine() đọc 1 dòng rồi xoá '\r' ở cuối nếu có.

Ý tưởngĐổi chữ thành số: +1, −1, 0 và "vách ngăn"

Ta gán cho mỗi ký tự của $S$ một "trọng số":

  • Chữ của Yên (thuộc $a$): $+1$;
  • Chữ của Định (thuộc $b$): $-1$;
  • Dấu cách: $0$;
  • Chữ "xấu" (không ai thích): là một vách ngăn – không xâu đặc biệt nào được chứa nó.

Khi đó một xâu con là đặc biệt khi và chỉ khi nó không chứa vách ngăn và tổng trọng số bằng 0 (số chữ của Yên trừ số chữ của Định bằng 0).

Gọi $P_i$ là tổng trọng số của $i$ ký tự đầu (tính từ vách ngăn gần nhất). Tổng của đoạn từ vị trí $j+1$ tới $i$ bằng $P_i - P_j$, nên:

Mấu chốt
Đoạn $(j, i]$ là xâu đặc biệt $\iff P_i = P_j$. Vậy khi đi tới vị trí $i$, số xâu đặc biệt kết thúc tại $i$ chính là số tiền tố trước đó có cùng giá trị $P_i$. Ta dùng map<int, int> cnt để đếm số lần mỗi giá trị tiền tố đã xuất hiện. Gặp vách ngăn thì xoá sạch map và bắt đầu lại.

Với ví dụ của đề: abfe agfdaa có chữ e và g là vách ngăn, chia $S$ thành 3 đoạn abf, ␣a, fdaa. Em bấm Tiếp để xem map đếm tiền tố hoạt động:

Ví dụ nhanh:

Cách 1Vét cạn mọi xâu con – Subtask 1

Duyệt mọi cặp $(l, r)$, với mỗi xâu con $S[l..r]$ đếm số chữ của Yên (ca), số chữ của Định (cb) và kiểm tra có chữ xấu không. Hàm a.find(c) trả về string::npos nếu ký tự c không có trong xâu a.

C++▶ Chạy tay từng dòng codeCách 1: duyệt mọi xâu con s[l..r], đếm ca, cb và kiểm tra chữ xấu.
Dữ liệu mẫu:
Khung nhập: dòng 1 là a, dòng 2 là b, dòng 3 là S (dấu cách đầu/cuối dòng 3 được giữ nguyên).
Không gian$O(n)$
Thời gian$O(n^3)$

Kết quả chạy thật: bản C++ đúng 30/100 test – ăn trọn Subtask 1; sai mọi test từ test 31 trở đi (quá thời gian); test đúng chậm nhất chạy dưới 0,01s. Bản Python đúng 30/100 test – ăn trọn Subtask 1; sai mọi test từ test 31 trở đi (quá thời gian); test đúng chậm nhất chạy 0,03s. Với $|S| = 10^4$ đã phải xét $5 \cdot 10^7$ xâu con, mỗi xâu dài trung bình hàng nghìn ký tự → quá chậm.

Cách 2Cố định đầu trái, cộng dồn và dừng ở vách ngăn – Subtask 1, 2

Hai cải tiến so với cách 1:

  1. Tính trước mảng trọng số w[c] cho từng ký tự (256 ô theo mã ASCII) để không phải find trong xâu $a$, $b$ nữa.
  2. Cố định $l$, cho $r$ tăng dần và cộng dồn bal += w[s[r]] thay vì đếm lại từ đầu. Gặp chữ xấu thì break ngay vì mọi xâu dài hơn đều chứa nó.
C++▶ Chạy tay từng dòng codeCách 2: cố định l, cộng dồn bal, gặp vách ngăn thì dừng.
Dữ liệu mẫu:
Khung nhập: dòng 1 là a, dòng 2 là b, dòng 3 là S.

Độ phức tạp là $O(\sum \text{độ dài đoạn tốt}^2)$. Ở subtask 2 các đoạn tốt rất ngắn (dài nhất 250 ký tự) nên chạy nhanh. Nhưng ở subtask 3 cả 26 chữ cái đều được thích, không có vách ngăn nào nên vòng r luôn chạy tới cuối: $\frac{10^{10}}{2}$ phép tính.

Không gian$O(n)$
Thời gian$O(n^2)$

Kết quả chạy thật: bản C++ đúng 60/100 test – ăn trọn Subtask 1, Subtask 2; sai mọi test từ test 61 trở đi (quá thời gian); test đúng chậm nhất chạy dưới 0,01s. Bản Python đúng 60/100 test – ăn trọn Subtask 1, Subtask 2; sai mọi test từ test 61 trở đi (quá thời gian); test đúng chậm nhất chạy 0,10s.

Cách 3Tổng tiền tố + map – Full điểm

Cài đặt đúng ý tưởng ở trên: duyệt $S$ một lần, giữ tổng tiền tố $p$ và map cnt. Ban đầu cnt[0] = 1 ứng với tiền tố rỗng (để đếm được cả các xâu con bắt đầu ngay từ đầu đoạn).

C++▶ Chạy tay từng dòng codeCách 3 (full điểm): tổng tiền tố p, map cnt đếm số tiền tố có cùng giá trị.
Dữ liệu mẫu:
Khung nhập: dòng 1 là a, dòng 2 là b, dòng 3 là S.
  1. ans += cnt[p] phải đặt trước cnt[p]++: ta chỉ ghép vị trí hiện tại với các tiền tố trước đó, không ghép với chính nó (xâu rỗng không tính).
  2. Gặp chữ xấu: cnt.clear(), đặt lại cnt[0] = 1 và p = 0 – coi như bắt đầu một xâu mới ngay sau vách ngăn.
  3. Đáp án có thể lên tới $\frac{n(n+1)}{2} \approx 5 \cdot 10^9$ (ví dụ $S$ toàn dấu cách) nên ans phải là long long.
  4. $p$ luôn nằm trong $[-n, n]$ nên cũng có thể dùng mảng cnt[2n + 1] với chỉ số lệch $p + n$ thay cho map. Dùng map thì không phải lo chỉ số âm.
Không gian$O(n)$
Thời gian$O(n \log n)$

Kết quả chạy thật: bản C++ đúng 100/100 test – ăn trọn Subtask 1, Subtask 2, Subtask 3; test đúng chậm nhất chạy 0,03s. Bản Python dùng dict đúng 100/100 test – ăn trọn Subtask 1, Subtask 2, Subtask 3; test đúng chậm nhất chạy 0,09s.

Tổng kếtKết quả chạy thật của các cách

CáchSubtask 1
test 1–30
Subtask 2
test 31–60
Subtask 3
test 61–100
TổngChậm nhất
(test đúng)
Cách 1 – C++30/300/30
quá thời gian
0/40
quá thời gian
30/1000,01s
Cách 1 – Python30/300/30
quá thời gian
0/40
quá thời gian
30/1000,03s
Cách 2 – C++30/3030/300/40
quá thời gian
60/1000,01s
Cách 2 – Python30/3030/300/40
quá thời gian
60/1000,10s
Cách 3 – C++30/3030/3040/40100/1000,03s
Cách 3 – Python30/3030/3040/40100/1000,09s
Ghi chú của mình: số liệu trên là chạy thật trên 100 test của CLUE OJ, thời gian là thời gian CPU đo trên máy của mình (biên dịch g++ -O2), giới hạn 1 giây. Cách 3 bản C++ đạt 100/100 test. Nhớ 3 cái bẫy đọc dữ liệu: dùng getline, giữ nguyên dấu cách đầu/cuối, và bỏ ký tự '\r'.
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