Đếm Ký Tự (HSG9 TP Đà Nẵng 2018-2019 – Bài 1)
Đề bài
Câu 1. Đếm ký tự (3 điểm) – Kì thi chọn học sinh giỏi lớp 9 cấp thành phố, TP Đà Nẵng, năm học 2018-2019.
Viết chương trình thực hiện: Nhập vào từ bàn phím một xâu kí tự $S$ (không chứa kí tự dấu cách và có không quá 255 kí tự) và in ra màn hình số lượng các kí tự chỉ xuất hiện đúng 1 lần trong xâu $S$.
Dữ liệu vào: một dòng duy nhất là xâu $S$ (không chứa dấu cách, độ dài không quá 255).
Kết quả: một số nguyên là số lượng các kí tự chỉ xuất hiện đúng 1 lần trong $S$.
Giới hạn thời gian trên hệ thống chấm: 1 giây.
abbacdmedc2Giải thích: Trong xâu kí tự được nhập từ bàn phím có 2 kí tự là m và e chỉ xuất hiện đúng 1 lần.
Lời mở đầu
Bài này nằm trong đề thi học sinh giỏi tin học lớp 9 thành phố Đà Nẵng, mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇.
Trước khi vào bài, các bạn nhớ giúp mình điều này: với các hệ thống chấm giới hạn 1 giây, toàn bài chỉ nên lặp khoảng $(3 \to 5) \cdot 10^7$ lần là an toàn, tệ nhất là $10^8$ (lúc này hên xui). Đề cho 2 giây thì nhân 2 ngưỡng lên, 100 ms thì chia 10. Ngưỡng này là cho C/C++, còn Python, C#, Java chạy chậm hơn nên ngưỡng phải thấp hơn nữa nha.
Giải thích ví dụ một chút: xâu abbacdmedc có số lần xuất hiện của từng kí tự như sau, chỉ m và e xuất hiện đúng 1 lần nên đáp án là 2.
| Kí tự | a | b | c | d | e | m |
|---|---|---|---|---|---|---|
| Số lần | 2 | 2 | 2 | 2 | 1 | 1 |
a…z. Coi chừng sai nhé! Đề chỉ nói xâu $S$ không có dấu cách, chứ không hề nói chỉ gồm chữ cái thường. Xâu hoàn toàn có thể có chữ in hoa A, B, C, chữ số, hay các kí tự lạ như @ ! $ % & *… Dù bạn có nộp AC trên trang web thì cũng chưa chắc đúng, vì bộ test trên web là do admin tự làm chứ không phải bộ test chính thức của Sở Giáo dục.Với độ dài tối đa chỉ 255 kí tự thì dù thuật toán tệ cỡ nào cũng khó mà TLE được (trừ khi phải tệ lắm lắm hihi). Nên ở bài gốc, chủ yếu là làm sao ra đáp án đúng. Còn trong bài viết này mình sẽ nâng độ khó lên: giả sử xâu dài tới $10^6$ kí tự thì sao?
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
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:
Cách 1Vét cạn – O(N²)
Tư tưởng đơn giản nhất là vét cạn: với mỗi kí tự $s[i]$, ta so sánh nó với toàn bộ các kí tự trong xâu (kể cả với chính nó). Mỗi lần trùng thì tăng biến đếm cnt. Sau cùng nếu cnt == 1 tức là $s[i]$ chỉ trùng với chính nó, không trùng với kí tự nào khác ⇒ nó được tính vào đáp án. Đếm xem có bao nhiêu kí tự như vậy là xong.
cin >> s được. Còn nếu đề không có ràng buộc đó (xâu có thể có dấu cách) thì không được dùng cin >>, vì nó dừng lại khi gặp khoảng trắng nên có thể đọc chưa hết xâu. Lúc đó phải dùng getline(cin, s); nha.Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
int res = 0;
for(int i = 0; i < (int)s.length(); ++i){
int cnt = 0;
for(int j = 0; j < (int)s.length(); ++j){
if(s[i] == s[j]){
cnt++;
}
}
if(cnt == 1){
res++;
}
}
cout << res;
return 0;
}
s = input().strip()
res = 0
for i in range(len(s)):
cnt = 0
for j in range(len(s)):
if s[i] == s[j]:
cnt += 1
if cnt == 1:
res += 1
print(res)
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(N)$ với $N$ là độ dài xâu $s$.
- Thời gian: $O(N)$ cho bước nhập xâu + $O(N^2)$ cho bước xử lý chính (hai vòng lặp lồng nhau). Theo quy tắc cộng BigO ta lấy thành phần lớn nhất ⇒ $O(N^2)$. (Dấu
^ở đây là lũy thừa nha, không phải phép xor.)
Với $N$ tối đa 255 thì $255^2 = 65\,025$ phép so sánh, quá nhỏ so với ngưỡng $(3 \to 5) \cdot 10^7$, nên dù là cách tệ nhất này cũng không thể TLE được.
Nhưng giờ giả sử xâu dài tới $10^6$ kí tự. Lúc đó $(10^6)^2 = 10^{12}$, vượt quá xa ngưỡng cho phép ⇒ cách này chắc chắn TLE. Phải tìm cách khác tốt hơn.
Cách 2Sắp xếp rồi xét hai hàng xóm – O(N log N)
Cách 1 dở ở chỗ: với mỗi $s[i]$ phải duyệt hết cả xâu mới biết có bao nhiêu kí tự trùng với nó. Lý do phải duyệt hết là vì các kí tự đang nằm lộn xộn. Vậy nếu ta gom các kí tự giống nhau nằm sát cạnh nhau thì để biết một kí tự xuất hiện mấy lần chỉ cần nhìn các vị trí lân cận thôi, các bạn đồng ý không?
Muốn vậy thì chỉ cần sắp xếp xâu tăng dần (theo mã ASCII): các kí tự giống nhau sẽ nằm liền một cụm. Khi đó kí tự $s[i]$ xuất hiện đúng 1 lần khi và chỉ khi nó khác cả hàng xóm bên trái $s[i-1]$ lẫn hàng xóm bên phải $s[i+1]$.
i - 1 >= 0 và i + 1 < s.length() trước khi truy cập, tránh truy cập ra ngoài xâu.Minh hoạ sắp xếp và xét hàng xóm
Em nhập xâu bất kì (không dấu cách) rồi bấm Tiếp để xem xâu được sắp xếp, rồi từng kí tự được so với hai hàng xóm như thế nào:
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
int res = 0;
sort(s.begin(), s.end());
for(int i = 0; i < (int)s.length(); ++i){
bool check = true;
if(i - 1 >= 0 && s[i - 1] == s[i]){
check = false;
}
if(i + 1 < (int)s.length() && s[i + 1] == s[i]){
check = false;
}
if(check == true){
res++;
}
}
cout << res;
return 0;
}
s = sorted(input().strip())
n = len(s)
res = 0
for i in range(n):
check = True
if i - 1 >= 0 and s[i - 1] == s[i]:
check = False
if i + 1 < n and s[i + 1] == s[i]:
check = False
if check:
res += 1
print(res)
Giải thích một số chỗ trong code:
sort(s.begin(), s.end());sắp xếp xâu $s$ tăng dần. Hàm này nằm trong thư viện<algorithm>; mình include<bits/stdc++.h>là bao hết rồi, còn bạn nào không dùng thì nhớ#include <algorithm>nha.- Biến cờ hiệu
checkmặc định làtrue(hợp lệ), sau đó xét các tình huống làm nó không hợp lệ để gánfalse. Cuối cùng nếu vẫntruethì $s[i]$ chỉ xuất hiện 1 lần và được tính vào kết quả. - Hai trường hợp gán
check = falselà 2 cáiifriêng, không phảiif … else if. Vì đó là 2 trường hợp riêng lẻ, phải xét đồng thời cả 2; dùngelse iflà chỉ xét 1 trong 2 thì không đúng. - Chỗ
if(check == true){ res++; }mình cố tình viết rõ ra, chứ bình thường mình sẽ viếtres += check;. Trong C/C++truelà 1,falselà 0, nên cộng thẳngcheckvàoreslà y chang.
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(N)$ với $N$ là độ dài xâu $s$.
- Thời gian: $O(N)$ nhập xâu + $O(N \log N)$ sắp xếp + $O(N)$ duyệt đếm ⇒ tổng quát $O(N \log N)$. Với $N = 10^6$: khoảng $10^6 \cdot 20 = 2 \cdot 10^7$, vẫn trong ngưỡng an toàn.
sort của C++ luôn đảm bảo $O(N \log N)$ (nó kết hợp nhiều thuật toán sắp xếp tối ưu bên trong). Tự cài Quick Sort, Merge Sort, Heap Sort thì mất công hơn, lỡ cài sai, hoặc Quick Sort rơi vào trường hợp xấu nhất cũng thành $O(N^2)$. Còn nếu cài Interchange Sort, Selection Sort, Bubble Sort… thì là $O(N^2)$, ngang với cách 1 ⇒ chắc chắn TLE khi $N = 10^6$.Đến đây các bạn đã thấy cái hay của giải thuật rồi chứ? Từ $O(N^2)$ xuống $O(N \log N)$: với $N = 255$ là từ $65\,025$ xuống khoảng $2\,040$; với $N = 10^6$ là từ $10^{12}$ (1000 tỷ) xuống $2 \cdot 10^7$ (20 triệu). Cứ tưởng tượng khoản nợ 1000 tỷ giờ giảm còn 20 triệu là thấy nó bớt cỡ nào hehe 😆.
a…z thì ta dùng mảng đánh dấu 26 phần tử như nói lúc đầu: duyệt xâu 1 lần để thống kê, rồi duyệt lại mảng đánh dấu là xong, độ phức tạp chỉ $O(N)$.char trong C++ chỉ chiếm 1 byte, tức là chỉ có tối đa 256 giá trị khác nhau. Nên kể cả khi xâu có kí tự bất kì, ta vẫn dùng được mảng đánh dấu 256 phần tử (không phải 26) để đạt $O(N)$: int cnt[256] = {0}; for(char c : s) cnt[(unsigned char)c]++; rồi đếm các x có cnt[x] == 1. Ép sang unsigned char để chỉ số không bị âm với các byte lớn hơn 127.Counter – khớp hết (đoạn mảng đánh dấu 256 phần tử ở trên cũng được thử với 200 bộ, khớp hết). Với xâu $10^6$ kí tự, cách 2 (C++) chạy khoảng 0.1 giây.- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét