Hướng dẫn cho Bài 4. Xóa xâu (HSG 9 Hải Phòng 2025-2026)
Chỉ sử dụng khi thực sự cần thiết như một cách tôn trọng tác giả và người viết hướng dẫn này.
Chép code từ bài hướng dẫn để nộp bài là hành vi có thể dẫn đến khóa tài khoản.
Chép code từ bài hướng dẫn để nộp bài là hành vi có thể dẫn đến khóa tài khoản.
Tóm tắt đề bài
Cho xâu \(S\) độ dài \(n\) gồm các chữ in hoa A...Z. Thực hiện tuần tự \(q\) lệnh xóa:
- Loại
0: xóa kí tự đầu tiên từ trái sang phải có giá trị từ điển nhỏ nhất trong xâu hiện tại. - Loại
1: xóa kí tự đầu tiên từ trái sang phải có giá trị từ điển lớn nhất trong xâu hiện tại.
Hãy in ra xâu còn lại sau \(q\) lần xóa.
Phân tích
- \(n \le 2\cdot 10^5\), \(q \le 10^5\) nên không thể mô phỏng trực tiếp mỗi lệnh bằng cách quét toàn bộ xâu (sẽ là \(O(nq)\)).
- Bảng chữ cái chỉ có \(26\) ký tự.
- Mỗi thao tác yêu cầu:
- Tìm chữ nhỏ nhất/lớn nhất còn tồn tại.
- Xóa vị trí xuất hiện sớm nhất của chữ đó (tức chỉ số nhỏ nhất).
Nhận xét then chốt:
- Nếu ta lưu với mỗi chữ cái một cấu trúc chứa các vị trí còn “sống” của chữ đó, thì:
- “kí tự đầu tiên” của chữ đó chính là vị trí nhỏ nhất trong cấu trúc.
- Chữ nhỏ nhất/lớn nhất còn tồn tại có thể tìm bằng cách duyệt từ
AđếnZhoặc ngược lại (chỉ 26 bước).
Vì vậy, mỗi lệnh có thể làm trong \(O(26 + \log n)\), tức gần như \(O(\log n)\).
Hướng giải quyết
Ý tưởng
- Tiền xử lý: với mỗi ký tự \(c \in [0..25]\), lưu tất cả vị trí \(i\) sao cho \(S[i]=c\) vào một
set<int>(tự sắp xếp). - Khi gặp lệnh:
- Nếu là loại
0:- Duyệt \(c\) từ
AđếnZđể tìm chữ đầu tiên cóset[c]không rỗng. - Lấy vị trí nhỏ nhất
pos = *set[c].begin()và xóa nó khỏi set.
- Duyệt \(c\) từ
- Nếu là loại
1:- Duyệt \(c\) từ
ZvềAđể tìm chữ đầu tiên còn tồn tại. - Xóa vị trí nhỏ nhất của chữ đó (vẫn là
begin()vì yêu cầu “đầu tiên từ trái qua phải”).
- Duyệt \(c\) từ
- Nếu là loại
- Sau khi xử lý xong \(q\) lệnh, ta cần in xâu còn lại theo thứ tự ban đầu:
- Duyệt từng vị trí \(i=1..n\), nếu \(i\) còn nằm trong set của ký tự \(S[i]\) thì in ra $S[i]`.
- Để kiểm tra nhanh, ta có thể dùng mảng
alive[i]ban đầu làtrue, khi xóa thì đặtfalse.
Thuật toán
- Đọc \(n, q\), đọc xâu \(S\) (đánh chỉ số từ \(1\) đến \(n\)).
- Khởi tạo
set<int> pos[26]. - Với mỗi vị trí \(i\):
pos[S[i]-'A'].insert(i).
- Với mỗi lệnh \(t\):
- Nếu \(t = 0\):
- Tìm \(c\) nhỏ nhất sao cho
pos[c]không rỗng. i = *pos[c].begin(), xóaivà đánh dấualive[i]=false.
- Tìm \(c\) nhỏ nhất sao cho
- Nếu \(t = 1\):
- Tìm \(c\) lớn nhất sao cho
pos[c]không rỗng. i = *pos[c].begin(), xóaivà đánh dấualive[i]=false.
- Tìm \(c\) lớn nhất sao cho
- Nếu \(t = 0\):
- In các ký tự \(S[i]\) với
alive[i]=true.
Lưu ý / lỗi hay gặp
- Dù là xóa chữ lớn nhất, vẫn phải xóa lần xuất hiện đầu tiên của chữ đó, nên vẫn lấy phần tử nhỏ nhất trong set (không phải lớn nhất).
- \(q\) có thể gần \(n\), nhưng đề không nói chắc \(q \le n\); thực tế khi xóa quá số ký tự thì không còn gì để xóa. Với bài chuẩn thường có \(q \le n\), nhưng vẫn có thể phòng thủ bằng cách dừng nếu mọi set đều rỗng.
Độ phức tạp
- Mỗi lệnh:
- Duyệt tối đa \(26\) chữ cái để tìm loại cần xóa.
- Xóa khỏi
set: \(O(\log n)\).
- Tổng:
- Thời gian: \(O\big(q(26 + \log n) + n\big)\), phù hợp.
- Bộ nhớ: \(O(n)\).
Code tham khảo
C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
string S;
cin >> S;
S = " " + S; // 1-index
array<set<int>, 26> pos;
vector<char> alive(n + 1, 1);
for (int i = 1; i <= n; i++) {
pos[S[i] - 'A'].insert(i);
}
for (int k = 0; k < q; k++) {
int t;
cin >> t;
int c = -1;
if (t == 0) {
for (int x = 0; x < 26; x++) {
if (!pos[x].empty()) {
c = x;
break;
}
}
} else {
for (int x = 25; x >= 0; x--) {
if (!pos[x].empty()) {
c = x;
break;
}
}
}
// Nếu không còn ký tự nào để xóa (phòng thủ)
if (c == -1) break;
int i = *pos[c].begin(); // vị trí xuất hiện sớm nhất
pos[c].erase(pos[c].begin());
alive[i] = 0;
}
string ans;
ans.reserve(n);
for (int i = 1; i <= n; i++) {
if (alive[i]) ans.push_back(S[i]);
}
cout << ans << "\n";
return 0;
}
Bình luận