Hướng dẫn giải của Mật khẩu_TĐ2
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Hiểu bài toán
Xác định số lượng xâu con liên tiếp của xâu S có độ dài ít nhất 6 ký tự và chứa ít nhất một ký tự hoa, một ký tự thường và một ký tự số. Xâu S chỉ chứa các ký tự in hoa (A-Z), in thường (a-z) và số (0-9).
Các cách tiếp cận
Cách Two Pointers (Hai Con Trỏ)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
freopen("matkhau.inp", "r", stdin);
freopen("matkhau.out", "w", stdout);
string s;
cin >> s;
int n = s.size();
// Duyệt qua từng vị trí kết thúc 'i' của xâu con
// Giữ 'l' là vị trí bắt đầu nhỏ nhất thỏa mãn điều kiện
// a, b, c là số lượng ký tự thường, hoa, số trong đoạn [l, i]
long long ans = 0;
int l = 0;
long long a = 0, b = 0, c = 0;
for (int i = 0; i < n; ++i) {
// Thêm ký tự s[i] vào đoạn hiện tại
if ('a' <= s[i] && s[i] <= 'z') a++;
else if ('A' <= s[i] && s[i] <= 'Z') b++;
else c++;
// Thúc l về phải cho đến khi đoạn [l, i] không còn đủ điều kiện
// Điều kiện: độ dài >= 6 (i - l + 1 >= 6) và có đủ 3 loại ký tự
while (l <= i && i - l + 1 >= 6 && a > 0 && b > 0 && c > 0) {
ans += (n - i); // Với kết thúc i, tất cả các xâu con bắt đầu từ l đến i đều hợp lệ
// Loại bỏ ký tự s[l] để kiểm tra tiếp
if ('a' <= s[l] && s[l] <= 'z') a--;
else if ('A' <= s[l] && s[l] <= 'Z') b--;
else c--;
l++;
}
}
cout << ans;
return 0;
}
- Time Complexity: O(n)
- Space Complexity: O(1)
Thuật toán sử dụng kỹ thuật hai con trỏ (sliding window). Ta duyệt con trỏ i từ đầu đến cuối xâu để đại diện cho điểm kết thúc của xâu con. Duy trì con trỏ l là điểm bắt đầu. Với mỗi i, ta cập nhật số lượng các loại ký tự. Nếu đoạn [l, i] thỏa mãn điều kiện (độ dài >= 6 và có đủ 3 loại), ta biết rằng tất cả các xâu con bắt đầu từ l đến i và kết thúc tại i đều hợp lệ. Do đó, ta cộng n - i vào kết quả và cố gắng tăng l lên để tìm các xâu con ngắn hơn. Vòng lặp while đảm bảo l luôn là vị trí bắt đầu nhỏ nhất thỏa mãn điều kiện cho i hiện tại.
Cách Binary Search on Prefix Sums
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
freopen("matkhau.inp", "r", stdin);
freopen("matkhau.out", "w", stdout);
string s;
cin >> s;
int n = s.size();
// Mảng tiền tố đếm số lượng mỗi loại ký tự
vector<int> u(n + 1, 0), l(n + 1, 0), d(n + 1, 0);
for (int i = 0; i < n; i++) {
u[i + 1] = u[i] + (isupper(s[i]) ? 1 : 0);
l[i + 1] = l[i] + (islower(s[i]) ? 1 : 0);
d[i + 1] = d[i] + (isdigit(s[i]) ? 1 : 0);
}
long long k = 0;
// Duyệt qua từng vị trí bắt đầu i
for (int i = 0; i < n; i++) {
int left = i + 6, right = n, res = -1;
// Tìm vị trí kết thúc j (thông qua độ dài) nhỏ nhất thỏa mãn điều kiện
while (left <= right) {
int mid = (left + right) / 2;
// Kiểm tra đoạn [i, mid-1]
if (u[mid] - u[i] > 0 && l[mid] - l[i] > 0 && d[mid] - d[i] > 0) {
res = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
if (res != -1) {
// Nếu tìm thấy, tất cả các xâu con bắt đầu tại i và có độ dài từ (res - i) đến (n - i) đều hợp lệ
k += n - res + 1;
}
}
cout << k;
return 0;
}
- Time Complexity: O(n log n)
- Space Complexity: O(n)
Phương pháp này duyệt qua từng vị trí bắt đầu i của xâu con. Với mỗi i, ta cần tìm độ dài nhỏ nhất len sao cho xâu con S[i...i+len-1] chứa đủ 3 loại ký tự. Vì điều kiện "có đủ 3 loại" là đơn điệu (nếu xâu dài hơn thì chắc chắn vẫn chứa đủ 3 loại nếu xâu ngắn hơn đã chứa), ta có thể sử dụng Binary Search để tìm độ dài này. Mảng tiền tố (prefix sum) được dùng để kiểm tra điều kiện trong thời gian O(1). Sau khi tìm được độ dài nhỏ nhất, ta suy ra tất cả các xâu con dài hơn cũng hợp lệ.
Cách Precompute Last Occurrence
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
void solve() {
freopen("MATKHAU.INP", "r", stdin);
freopen("MATKHAU.OUT", "w", stdout);
ios_base::sync_with_stdio(false);
cin.tie(NULL);
string S;
if (!(cin >> S)) return;
int n = S.length();
if (n < 6) {
cout << 0 << "\n";
return;
}
// L_H[j]: Index của lần cuối xuất hiện ký tự hoa trong đoạn [0, j-1]
vector<int> L_H(n + 1, 0), L_L(n + 1, 0), L_D(n + 1, 0);
for (int j = 1; j <= n; ++j) {
char c = S[j - 1];
L_H[j] = L_H[j - 1];
L_L[j] = L_L[j - 1];
L_D[j] = L_D[j - 1];
if (c >= 'A' && c <= 'Z') L_H[j] = j;
else if (c >= 'a' && c <= 'z') L_L[j] = j;
else if (c >= '0' && c <= '9') L_D[j] = j;
}
long long ans = 0;
for (int j = 6; j <= n; ++j) {
// Vị trí bắt đầu phải nhỏ hơn vị trí cuối cùng của từng loại ký tự trong đoạn [0, j]
int start = 0;
int h = L_H[j];
int l = L_L[j];
int d = L_D[j];
if (h == 0 || l == 0 || d == 0) continue;
// Vị trí bắt đầu i phải thỏa mãn: i <= h-1, i <= l-1, i <= d-1
// Và i phải thỏa mãn j - i + 1 >= 6 => i <= j - 5
// Do đó, i phải nhỏ hơn hoặc bằng min(h, l, d, j-5)
int max_start = min({h, l, d, j - 5});
ans += max_start;
}
cout << ans << "\n";
}
int main() {
solve();
return 0;
}
- Time Complexity: O(n)
- Space Complexity: O(n)
Thay vì duyệt theo điểm bắt đầu hoặc sử dụng Sliding Window, ta duyệt theo điểm kết thúc j. Ta cần tính toán có bao nhiêu điểm bắt đầu i hợp lệ cho điểm kết thúc j cố định.
Điều kiện để xâu con S[i...j] hợp lệ là:
j - i + 1 >= 6=>i <= j - 5.- Xâu con chứa ký tự hoa =>
i <= index_hoa_cuoi_cung_trong_doan[0...j] - 1. Tương tự cho ký tự thường và số. Ta dùng mảngL_H,L_L,L_Dđể lưu vị trí cuối cùng xuất hiện của từng loại ký tự tính đếnj. Sau đó, vị trí bắt đầuiphải nhỏ hơn hoặc bằng giá trị nhỏ nhất trong tập hợp các ràng buộc trên. Số lượng giá trịidương thỏa mãn chính là giá trị đó.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(n) | O(1) | Two Pointers (Hai Con Trỏ) |
| 2 | O(n log n) | O(n) | Binary Search on Prefix Sums |
| 3 | O(n) | O(n) | Precompute Last Occurrence |
Bài học kinh nghiệm
- Bài toán có thể được giải quyết hiệu quả bằng cách đếm số lượng xâu con hợp lệ thay vì liệt kê chúng.
- Kỹ thuật Sliding Window (Hai con trỏ) thường là lựa chọn tối ưu nhất cho các bài toán xâu liên tiếp có ràng buộc về thành phần (phải có đủ các loại ký tự).
- Biến đổi quy hoạch động hoặc tiền xử lý (Prefix Sums, Last Occurrence) giúp kiểm tra điều kiện trong thời gian O(1) sau khi tiền xử lý O(n).
Lỗi thường gặp
- Quên kiểm tra độ dài tối thiểu của xâu con (phải >= 6).
- Sử dụng thuật toán O(n^2)导致 Timeout cho n lớn (ví dụ 10^5).
- Lỗi Off-by-one khi tính toán chỉ số trong mảng tiền tố (Prefix Array) hoặc khi tính độ dài xâu con.
- Trong cách tiếp cận Sliding Window, nếu không xử lý vòng lặp while đúng cách có thể bỏ sót các trường hợp hợp lệ.
Bình luận