Hướng dẫn giải của Chữ số thứ k
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
Cho một mảng a gồm n số nguyên, với mỗi số từ 1 đến n. Với mỗi k từ 1 đến n, ta cần tìm 'số k' của mảng. 'Số k' được định nghĩa là số nguyên dương nhỏ nhất sao cho số đó xuất hiện trong tất cả các mảng con có độ dài k của mảng a. Nếu không có số nào thỏa mãn, 'số k' là -1.
Phân tích ví dụ: Mảng [1, 3, 1, 5, 3, 1].
- k=1: Các mảng con độ dài 1 là [1], [3], [1], [5], [3], [1]. Không có số nào xuất hiện trong tất cả (1 không có ở vị trí 2, 4; 3 không có ở 1, 3, 6...). => -1.
- k=2: Các mảng con độ dài 2. Số 1 có thể không có trong mảng con [3, 5] hoặc [5, 3]. Số 3 không có trong [1, 3] (mảng con đầu tiên).
- k=3: Xét số 1. Liệu 1 có trong mọi mảng con độ dài 3? Khoảng cách giữa các lần xuất hiện của 1 là 2 (vị trí 0, 2, 5 -> khoảng cách 2 và 3). Nếu k=3, mọi đoạn dài 3 đều phải chứa 1. Khoảng cách lớn nhất giữa các lần xuất hiện của 1 là 3 (từ vị trí 2 đến 5). Nếu k > khoảng cách này (k=4), thì chắc chắn có 1 trong mọi đoạn. Nhưng nếu k=3, ta cần kiểm tra. Thực tế, với k=3, số 1 xuất hiện ở mọi đoạn. => 1.
Nhìn chung, một số x xuất hiện trong mọi mảng con độ dài k nếu và chỉ nếu khoảng cách lớn nhất giữa các lần xuất hiện của x (hoặc từ đầu đến lần đầu, hoặc từ lần cuối đến cuối mảng) nhỏ hơn k.
Các cách tiếp cận
Cách Brute Force
// Duyệt k từ 1 đến n
// Duyệt i từ 0 đến n-k (mảng con bắt đầu tại i)
// Với mỗi mảng con, dùng mảng đếm tần suất để kiểm tra số nào xuất hiện
// Hoặc tốt hơn: Với mỗi số x, kiểm tra xem có đoạn con độ dài k nào không chứa x không.
// Pseudocode:
for k in 1..n:
ans = -1
for x in 1..n:
bool ok = true
int last = -1
// Kiểm tra các khoảng trống
for each index i of x:
if i - last > k: ok = false; break
last = i
if n - last > k: ok = false
if ok: ans = x; break
print ans
- Time Complexity: O(n^3) đến O(n^2) (nếu tối ưu)
- Space Complexity: O(n)
Cách tiếp cận trực tiếp nhất. Với mỗi k, ta kiểm tra từng số x từ nhỏ đến lớn. Để kiểm tra xem x có phải là 'số k' không, ta cần đảm bảo mọi đoạn con độ dài k đều chứa x. Điều này tương đương với việc khoảng cách lớn nhất giữa các vị trí của x phải nhỏ hơn k. Nếu tối ưu hóa việc tính khoảng cách, độ phức tạp có thể xuống O(n^2). Tuy nhiên, với n ~ 1000, cách này có thể chấp nhận được nhưng không phải tối ưu nhất về mặt tổng quát.
Cách Optimal: Khoảng cách & Tối ưu hóa
#include <bits/stdc++.h>
using namespace std;
int main() {
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
// last[x] lưu vị trí cuối cùng của x
// max_gap[x] lưu khoảng cách lớn nhất giữa các lần xuất hiện của x
vector<int> last(n + 1, -1);
vector<int> max_gap(n + 1, 0);
for (int i = 0; i < n; i++) {
int x = a[i];
if (last[x] != -1) {
max_gap[x] = max(max_gap[x], i - last[x]);
} else {
// Lần đầu xuất hiện, khoảng cách từ đầu mảng (vị trí -1)
max_gap[x] = max(max_gap[x], i - (-1));
}
last[x] = i;
}
// Kiểm tra khoảng cách từ lần xuất hiện cuối cùng đến cuối mảng
for (int x = 1; x <= n; x++) {
if (last[x] != -1) {
max_gap[x] = max(max_gap[x], n - last[x]);
}
}
// Tính kết quả
// ans[k] = số nhỏ nhất có max_gap < k
vector<int> ans(n + 1, -1);
// Cách 1: Duyệt k và tìm min
// O(n^2)
for (int k = 1; k <= n; k++) {
int min_val = INT_MAX;
for (int x = 1; x <= n; x++) {
if (max_gap[x] < k) { // Nếu khoảng cách nhỏ hơn k, x có trong mọi đoạn
min_val = min(min_val, x);
}
}
if (min_val == INT_MAX) cout << -1 << " ";
else cout << min_val << " ";
}
cout << "\n";
}
}
- Time Complexity: O(n^2)
- Space Complexity: O(n)
Đây là cách tiếp cận hiệu quả dựa trên nhận xét: Số x xuất hiện trong mọi mảng con độ dài k nếu và chỉ nếu khoảng cách lớn nhất giữa các lần xuất hiện của x nhỏ hơn k.
Tính khoảng cách (Gap): Với mỗi số x, ta tính:
- Khoảng cách từ đầu mảng đến lần xuất hiện đầu tiên.
- Khoảng cách giữa các lần xuất hiện liên tiếp.
- Khoảng cách từ lần xuất hiện cuối cùng đến cuối mảng. Max_gap[x] là giá trị lớn nhất của các khoảng cách này.
Tìm kết quả cho mỗi k:
- Với mỗi k từ 1 đến n, ta duyệt qua các số x (từ 1 đến n).
- Nếu Max_gap[x] < k, thì x là ứng cử viên cho 'số k'.
- Chọn giá trị nhỏ nhất trong các ứng cử viên.
Độ phức tạp: O(n^2) do với mỗi k (n lần), ta duyệt qua n số. Với n <= 1000, n^2 = 10^6, hoàn toàn chạy được.
Cách Optimal: Cập nhật theo mảng
#include <bits/stdc++.h>
using namespace std;
int main() {
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<int> last(n + 1, -1);
vector<int> best(n + 1, 0);
// Tính khoảng cách giữa các lần xuất hiện của mỗi x
for (int i = 0; i < n; i++) {
int x = a[i];
best[x] = max(best[x], i - last[x] - 1); // -1 vì khoảng cách là số phần tử nằm giữa
last[x] = i;
}
for (int x = 1; x <= n; x++) {
best[x] = max(best[x], n - last[x] - 1);
}
vector<int> ans(n + 1, -1);
// Gán cho mọi k >= best[x] + 1
for (int x = 1; x <= n; x++) {
if (last[x] == -1) continue; // số không xuất hiện
int need = best[x] + 1; // k cần thiết để x xuất hiện
if (need <= n) {
// Nếu ans[need] chưa có, gán. Nếu đã có, ta chỉ cần gán khi x nhỏ hơn (nhưng ta duyệt x tăng dần nên không cần)
// Tuy nhiên, ta phải fill all k >= need
// Tối ưu: Nếu ans[need] đã có giá trị, nó là x nhỏ hơn (vì duyệt từ 1). Nhưng ta cần fill k >= need
// Nếu ans[need] != -1, nó đã được gán bởi số nhỏ hơn. Ta không cần gán lại.
// Nhưng ta phải fill các k > need nếu chưa có.
if (ans[need] == -1) ans[need] = x;
}
}
// Propagate: Nếu k có đáp án là -1, nó có thể lấy đáp án từ k-1 (vì k càng lớn, điều kiện càng dễ thỏa)
// Tuy nhiên, theo đề bài, ta cần số NHỎ NHẤT.
// Nếu ans[k] = -1, ta không thể lấy ans[k-1] vì ans[k-1] có thể lớn hơn.
// Phải làm gì?
// Ta chỉ fill ans[need] = x. Sau đó ta cần tìm min cho mọi k.
// Ví dụ: x=3 cần k=3. ans[3]=3. x=5 cần k=2. ans[2]=5.
// Kết quả k=3: min(ans[3], ans[...]) -> 3.
// Kết quả k=4: min(ans[4], ans[3], ans[2]) ...
// Cách khác: Dùng vector res[k] lưu giá trị nhỏ nhất cho độ dài k.
vector<int> res(n + 2, INT_MAX);
for (int x = 1; x <= n; x++) {
if (last[x] == -1) continue;
int need = best[x] + 1;
if (res[need] > x) res[need] = x;
}
int current_min = INT_MAX;
for (int k = 1; k <= n; k++) {
if (res[k] < current_min) current_min = res[k];
if (current_min == INT_MAX) cout << -1 << " ";
else cout << current_min << " ";
}
cout << "\n";
}
}
- Time Complexity: O(n)
- Space Complexity: O(n)
Đây là cách tối ưu nhất.
- Tính
best[x](khoảng cách lớn nhất giữa các lần xuất hiện). Nếubest[x] = d, thì số x sẽ xuất hiện trong mọi mảng con có độ dài k nếu k >= d + 1. - Dùng mảng
resđể lưu số nhỏ nhất cho từng độ dài k tối thiểu.- Với mỗi số x, ta biết nó có thể là đáp án cho mọi k >=
best[x] + 1. - Ta cập nhật
res[best[x] + 1] = min(res[best[x] + 1], x).
- Với mỗi số x, ta biết nó có thể là đáp án cho mọi k >=
- Sau đó duyệt qua k từ 1 đến n, duy trì biến
current_minđể tìm số nhỏ nhất có thể cho k hiện tại.- Tại mỗi k,
current_minsẽ được cập nhật bằngres[k](nếu nhỏ hơn). - In ra
current_min(hoặc -1 nếu vẫn là INF).
- Tại mỗi k,
Độ phức tạp: O(n) để tính khoảng cách + O(n) để cập nhật res + O(n) để in -> Tổng O(n).
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(n^3) đến O(n^2) (nếu tối ưu) | O(n) | Brute Force |
| 2 | O(n^2) | O(n) | Optimal: Khoảng cách & Tối ưu hóa |
| 3 | O(n) | O(n) | Optimal: Cập nhật theo mảng |
Bài học kinh nghiệm
- Một số x xuất hiện trong mọi mảng con độ dài k nếu và chỉ nếu khoảng cách lớn nhất giữa các vị trí liên tiếp của x (hoặc từ biên đến x) nhỏ hơn k.
- Nếu một số x thỏa mãn cho độ dài k, nó cũng sẽ thỏa mãn cho mọi độ dài k' > k. Tuy nhiên, ta cần tìm số NHỎ NHẤT cho mỗi k.
- Bài toán có thể biến đổi thành: Với mỗi số x, tìm kmin(x) là độ dài k nhỏ nhất để x xuất hiện trong mọi đoạn. Sau đó, với mỗi k, ta cần tìm min{x | kmin(x) <= k}.
Lỗi thường gặp
- Xử lý sai khoảng cách: Cần tính khoảng cách giữa các lần xuất hiện, hoặc từ biên đến lần xuất hiện. Ví dụ, nếu số ở đầu mảng, khoảng cách từ biên là chỉ số của nó.
- Quên các số không xuất hiện trong mảng: Các số này không bao giờ là đáp án.
- Sai logic cập nhật kết quả: Nếu chỉ gán ans[k] = x, ta có thể bỏ lỡ các số nhỏ hơn nhưng yêu cầu k lớn hơn. Cần phải so sánh và cập nhật min cho mọi k.
Bình luận