Hướng dẫn giải của Xuất hiện nhiều hơn K lần (đề Lào Cai 2021)
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.
Hướng dẫn giải
Cho dãy N số. Đếm số đoạn liên tiếp [i..j] mà trong đó có ít nhất một giá trị xuất hiện từ K lần trở lên. Số đoạn có thể lên tới khoảng N^2/2 nên cần dùng long long.
Sub 1: Vét cạn — ~O(n^2)~, dùng khi ~n \le 1000~
Với mỗi điểm đầu i, mở rộng điểm cuối j từ i đến N, cập nhật số lần xuất hiện của a_j và giữ giá trị lớn nhất của số lần. Nếu giá trị lớn nhất này >= K thì đoạn [i..j] được tính. Các giá trị được nén về 0..m-1 để dùng mảng đếm.
Chấm riêng sub này: đúng 20/25 test (các test có n <= 1000 đều đúng).
Sub 2: Đếm phần bù bằng hai con trỏ — ~O(n \log n)~, dùng cho mọi n
Đếm các đoạn "xấu" (mọi giá trị đều xuất hiện ít hơn K lần) rồi lấy tổng số đoạn trừ đi.
- Với mỗi đầu phải r, giữ con trỏ l sao cho đoạn [l..r] là đoạn xấu dài nhất kết thúc tại r. Khi thêm ar mà số lần của ar đạt K, tăng l và bỏ dần phần đầu cho đến khi a_r còn dưới K lần.
- Mọi đoạn [s..r] với s >= l đều xấu, nên có r - l + 1 đoạn xấu kết thúc tại r.
- Đáp án là n(n+1)/2 - tổng số đoạn xấu.
Ví dụ: N = 4, K = 2, dãy 1 2 1 2. Tổng số đoạn là 10. Đoạn xấu gồm 4 đoạn độ dài 1 và 3 đoạn độ dài 2 ({1,2}, {2,1}, {1,2}), tổng 7. Các đoạn độ dài 3 trở lên đều có một giá trị xuất hiện 2 lần nên không xấu. Đáp án là 10 - 7 = 3, đúng với đề.
Bẫy: a_i có thể lên tới 10^9 nên cần nén giá trị; n lên tới 5·10^5 nên dùng long long cho tổng.
Chương trình hoàn chỉnh
main đọc N, K và dãy a, nén giá trị, nếu n <= 1000 gọi sub1, còn lại gọi sub2.
#include <bits/stdc++.h>
using namespace std;
int n, K;
vector<int> a; // a[0..n-1] da nen thanh chi so 0..m-1
int m; // so gia tri khac nhau
// Đếm đoạn [i..j] có ít nhất một giá trị xuất hiện >= K lần.
// Sub 1: vét cạn — với mỗi điểm đầu i, mở rộng j và cập nhật số lần xuất hiện, kiểm tra max >= K. O(n^2). Dùng khi n <= 1000.
void sub1() {
vector<int> cnt(m, 0);
long long ans = 0;
for (int i = 0; i < n; i++) {
fill(cnt.begin(), cnt.end(), 0);
int mx = 0;
for (int j = i; j < n; j++) {
mx = max(mx, ++cnt[a[j]]);
if (mx >= K) ans++;
}
}
cout << ans << "\n";
}
// Sub 2: đếm phần bù rồi trừ. Đoạn "xấu" là đoạn mà mọi giá trị đều xuất hiện < K lần.
// Với mỗi đầu phải r, giữ cửa sổ [l..r] nhỏ nhất sao cho mọi giá trị trong đó xuất hiện < K lần (hai con trỏ).
// Khi đó có r - l + 1 đoạn xấu kết thúc tại r. Đáp án = n(n+1)/2 - tổng đoạn xấu. O(n log n).
void sub2() {
vector<int> cnt(m, 0);
long long bad = 0;
int l = 0;
for (int r = 0; r < n; r++) {
cnt[a[r]]++;
while (cnt[a[r]] >= K) { // bỏ dần phần đầu cho đến khi giá trị a[r] còn < K lần
cnt[a[l]]--;
l++;
}
bad += r - l + 1;
}
long long total = (long long)n * (n + 1) / 2;
cout << total - bad << "\n";
}
int main() {
freopen("cau3.inp", "r", stdin);
freopen("cau3.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> K;
vector<long long> raw(n);
for (auto &x : raw) cin >> x;
// nén giá trị về 0..m-1
vector<long long> vals = raw;
sort(vals.begin(), vals.end());
vals.erase(unique(vals.begin(), vals.end()), vals.end());
m = vals.size();
a.resize(n);
for (int i = 0; i < n; i++)
a[i] = lower_bound(vals.begin(), vals.end(), raw[i]) - vals.begin();
if (n <= 1000) sub1(); // dữ liệu nhỏ: vét cạn
else sub2(); // hai con trỏ
return 0;
}
Bình luận