Hướng dẫn giải của Chữ số thứ k


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
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ập

Tác giả: Hiếu Nguyễn, sussyboy, trungdtvt24, congtyluuthaibao1978

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.

  1. 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.
  2. 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.

  1. Tính best[x] (khoảng cách lớn nhất giữa các lần xuất hiện). Nếu best[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.
  2. 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).
  3. 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_min sẽ được cập nhật bằng res[k] (nếu nhỏ hơn).
    • In ra current_min (hoặc -1 nếu vẫn là INF).

Độ 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

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.