Hướng dẫn giải của [Trại hè HV 2025 Tin 11] COUNT
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
Đếm các đoạn con [l..r] sao cho với mọi j \in {1..k} luôn có ít nhất một giá trị xuất hiện đúng j lần trong đoạn.
Sub 1: Vét cạn — ~O(n^2)~, dùng khi ~n \le 5000~
Với mỗi l, mở rộng r sang phải. Duy trì cnt[v] là số lần giá trị v xuất hiện và eq[j] là số giá trị xuất hiện đúng j lần. Mỗi khi thêm a_r, chỉ cần sửa eq ở hai vị trí cũ và mới, nên mỗi bước là O(k). Đoạn tốt khi eq[1..k] đều dương. Nếu chỉ nộp sub này: đúng 12/40 test.
Sub 2: Cây đoạn với inclusion-exclusion — ~O(n \cdot 2^k \cdot k \cdot \log n)~
Cố định đầu phải r. Với mỗi j, gọi covj(l) là số giá trị có đúng j lần trong [l..r]. Đoạn [l..r] tốt khi covj(l) > 0 với mọi j. Một giá trị v có m lần xuất hiện ở các vị trí q1 < ... < qm thì nó có đúng j lần trong [l..r] khi l thuộc (q{m-j}, q{m-j+1}]. Khi r tăng, chỉ giá trị a_r thay đổi, nên ta bỏ đóng góp cũ và thêm đóng góp mới, mỗi lần là O(k) đoạn cập nhật.
Với mỗi tập con S của {1..k}, đặt WS(l) = tổng covj(l) với j \in S. Vì các cov không âm, WS(l) = 0 khi và chỉ khi mọi covj(l) với j \in S bằng 0. Theo nguyên lý bù trừ, số l tốt là tổng với dấu (-1)^{|S|} của số l có WS(l) = 0, S chạy qua mọi tập con (kể cả rỗng, khi đó số l là r). Mỗi WS được quản lý bằng một cây đoạn lưu (giá trị nhỏ nhất, số lần xuất hiện của nó) với cập nhật cộng trên đoạn. Đếm số 0 trên [1..r] chính là số lần xuất hiện của min nếu min bằng 0.
Bẫy: đáp án có thể rất lớn (lên tới khoảng n^2 / 2), nên dùng long long.
Nếu chỉ nộp sub này: đúng 40/40 test.
Chương trình hoàn chỉnh
Hàm main đọc dữ liệu. Nếu n \le 5000 gọi sub1, ngược lại gọi sub2.
#include <bits/stdc++.h>
using namespace std;
int n, K;
vector<int> a; // a[1..n]
// Sub 1: vet can. Voi moi dau trai l, mo rong r ve phai, duy tri eq[j] = so gia tri xuat hien dung j lan.
// Doan [l..r] tot khi eq[1..K] deu > 0. O(n^2 * K) nhung moi buoc chi O(K). Dung khi n <= 5000
void sub1() {
long long ans = 0;
vector<int> cnt(n + 1, 0), eq(K + 2, 0);
for (int l = 1; l <= n; l++) {
fill(cnt.begin(), cnt.end(), 0);
fill(eq.begin(), eq.end(), 0);
for (int r = l; r <= n; r++) {
int v = a[r], c = cnt[v];
if (c >= 1 && c <= K) eq[c]--; // gia tri v roi khoi so dem cu
cnt[v]++;
c++;
if (c <= K) eq[c]++; // gia tri v vao so dem moi
bool ok = true;
for (int j = 1; j <= K; j++) if (eq[j] == 0) { ok = false; break; }
if (ok) ans++;
}
}
cout << ans << "\n";
}
// Sub 2 (day du): dem theo dau phai r, voi moi dau trai l co "good" neu voi moi j in [1..K] co mot gia tri
// xuat hien dung j lan trong [l..r]. Voi moi j, cov_j(l) = so gia tri co dung j lan trong [l..r].
// Gia tri v co dung j lan trong [l..r] khi l thuoc (q_{m-j}, q_{m-j+1}], voi q la vi tri xuat hien cua v.
// Dung cay doan (min, dem min) cho moi tap con S cua {1..K} luu W_S(l) = tong cov_j(l) voi j trong S.
// Inclusion-exclusion: so l tot = sum_S (-1)^{|S|} #{l : W_S(l) = 0}. O(n * 2^K * K * log n). Dung cho moi n
struct Seg {
int n;
vector<int> mn, cn, lz;
void init(int sz) {
n = sz;
mn.assign(4 * n + 4, 0); cn.assign(4 * n + 4, 0); lz.assign(4 * n + 4, 0);
build(1, 1, n);
}
void build(int x, int l, int r) {
cn[x] = r - l + 1;
if (l == r) return;
int m = (l + r) / 2;
build(2 * x, l, m); build(2 * x + 1, m + 1, r);
}
void apply(int x, int v) { mn[x] += v; lz[x] += v; }
void push(int x) {
if (lz[x]) { apply(2 * x, lz[x]); apply(2 * x + 1, lz[x]); lz[x] = 0; }
}
void pull(int x) {
int L = 2 * x, R = 2 * x + 1;
if (mn[L] < mn[R]) { mn[x] = mn[L]; cn[x] = cn[L]; }
else if (mn[L] > mn[R]) { mn[x] = mn[R]; cn[x] = cn[R]; }
else { mn[x] = mn[L]; cn[x] = cn[L] + cn[R]; }
}
void upd(int x, int l, int r, int ql, int qr, int v) {
if (qr < l || r < ql) return;
if (ql <= l && r <= qr) { apply(x, v); return; }
push(x);
int m = (l + r) / 2;
upd(2 * x, l, m, ql, qr, v); upd(2 * x + 1, m + 1, r, ql, qr, v);
pull(x);
}
pair<int, int> qry(int x, int l, int r, int ql, int qr) {
if (qr < l || r < ql) return {INT_MAX, 0};
if (ql <= l && r <= qr) return {mn[x], cn[x]};
push(x);
int m = (l + r) / 2;
auto L = qry(2 * x, l, m, ql, qr), R = qry(2 * x + 1, m + 1, r, ql, qr);
if (L.first < R.first) return L;
if (L.first > R.first) return R;
return {L.first, L.second + R.second};
}
// so vi tri l trong [1..r] co W(l) = 0 (W >= 0 nen min = 0 la du)
int zeros(int r) {
auto p = qry(1, 1, n, 1, r);
return p.first == 0 ? p.second : 0;
}
};
void sub2() {
int full = 1 << K; // tap con S cua {1..K} duoc ma hoa bit (j-1)
vector<Seg> seg(full);
for (int m = 1; m < full; m++) seg[m].init(n);
vector<vector<int>> pos(n + 1); // vi tri xuat hien cua moi gia tri
// them (sign=+1) hoac bo (sign=-1) dong gop cua gia tri v khi no co m lan xuat hien
auto contrib = [&](int v, int m, int sign) {
for (int j = 1; j <= min(K, m); j++) {
int hi = pos[v][m - j]; // q_{m-j+1}
int lo = (m - j - 1 >= 0) ? pos[v][m - j - 1] : 0; // q_{m-j}
if (lo + 1 > hi) continue;
for (int mask = 1; mask < full; mask++)
if ((mask >> (j - 1)) & 1) seg[mask].upd(1, 1, n, lo + 1, hi, sign);
}
};
long long total = 0;
for (int r = 1; r <= n; r++) {
int v = a[r];
int mo = pos[v].size();
if (mo > 0) contrib(v, mo, -1);
pos[v].push_back(r);
contrib(v, mo + 1, +1);
long long good = r; // S rong, dau +
for (int mask = 1; mask < full; mask++) {
long long z = seg[mask].zeros(r);
if (__builtin_popcount(mask) & 1) good -= z; else good += z;
}
total += good;
}
cout << total << "\n";
}
int main() {
freopen("COUNT.INP", "r", stdin);
freopen("COUNT.OUT", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> K;
a.assign(n + 1, 0);
for (int i = 1; i <= n; i++) cin >> a[i];
if (n <= 5000) sub1();
else sub2();
return 0;
}
Bình luận