Hướng dẫn giải của Thống kê chuỗi con
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ả: , , ,
Editorial for cprefix: Thống kê chuỗi con
Hiểu bài toán
Cho một xâu S độ dài N (N ≤ 10^5). Cần tính một dãy t1, t2, ..., tN, trong đó ti là số lần xuất hiện của xâu con S[1...i] (tiền tố độ dài i) trong toàn bộ xâu S. Ví dụ, với S = 'abababa', tiền tố 'a' (i=1) xuất hiện 4 lần, 'ab' (i=2) xuất hiện 3 lần, ..., 'abababa' (i=7) xuất hiện 1 lần.
Các cách tiếp cận
Cách Z-Algorithm (Z-function)
#include <bits/stdc++.h>
using namespace std;
vector<int> z_function(const string &s) {
int n = s.size();
vector<int> z(n);
int l = 0, r = 0;
for (int i = 1; i < n; i++) {
if (i <= r) z[i] = min(r - i + 1, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
}
return z;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
int n = s.size();
vector<int> z = z_function(s);
vector<int> cnt(n + 2, 0);
// Dùng kỹ thuật difference array để đếm
// Nếu tại vị trí i, z[i] = k, nghĩa là có 1 tiền tố con trùng với S[1...k] tại vị trí i.
// Do đó, các tiền tố S[1...len] với 1 <= len <= k đều xuất hiện thêm 1 lần.
// Ta đánh dấu: tăng cnt[1], giảm cnt[k+1].
for (int i = 1; i < n; i++) {
if (z[i] > 0) {
cnt[1]++;
cnt[z[i] + 1]--;
}
}
// Cộng dồn để có số lượng xuất hiện cho các prefix từ các vị trí i > 0
for (int i = 1; i <= n; i++) cnt[i] += cnt[i - 1];
// In kết quả. Mỗi prefix đều xuất hiện ít nhất 1 lần (chính nó), nên cộng thêm 1.
// Với prefix độ dài i, t_i = cnt[i] + 1.
for (int i = 1; i <= n; i++) {
cout << cnt[i] + 1 << " ";
}
return 0;
}
- Time Complexity: O(N)
- Space Complexity: O(N)
Cách tiếp cận này sử dụng Z-function để tìm độ dài tiền tố dài nhất chung giữa S và hậu tố bắt đầu từ mỗi vị trí i.
- Tính mảng Z cho xâu S. Z[i] là độ dài tiền tố chung dài nhất của S và S[i...n-1].
- Z[i] = k có nghĩa là tại vị trí i, xâu S có 1 tiền tố con độ dài k trùng với S[1...k].
- Do đó, tại vị trí i, tất cả các tiền tố S[1...len] với 1 <= len <= k đều xuất hiện thêm một lần.
- Ta dùng Difference Array (mảng cộng dồn) để xử lý cập nhật khoảng [1, k] này một cách nhanh chóng. Tăng cnt[1] và giảm cnt[k+1].
- Sau khi duyệt qua hết các vị trí i, ta cộng dồn mảng cnt. Giá trị cnt[i] lúc này chính là số lần xuất hiện thêm của tiền tố độ dài i từ các vị trí khác ngoài vị trí đầu tiên.
- Vì tiền tố luôn xuất hiện ít nhất 1 lần (tại chính nó), ta in ra cnt[i] + 1.
Cách KMP Algorithm (Prefix Function)
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
string s;
cin >> s;
int n = s.size();
// Tính mảng tiền tố (pi array)
vector<int> pi(n, 0);
for (int i = 1; i < n; ++i) {
int j = pi[i - 1];
while (j > 0 && s[i] != s[j])
j = pi[j - 1];
if (s[i] == s[j])
++j;
pi[i] = j;
}
// freq[i] sẽ lưu số lần xuất hiện của tiền tố độ dài i
// Ban đầu, ta đếm số lần mỗi tiền tố dài i là tiền tố chung dài nhất tại một vị trí
vector<int> freq(n + 1, 0);
for (int i = 0; i < n; ++i) {
freq[pi[i]]++;
}
// Duyệt ngược để cộng dồn số lần xuất hiện
// Nếu tiền tố độ dài i xuất hiện freq[i] lần,
// thì các tiền tố của nó (tiền tố của tiền tố) cũng xuất hiện ít nhất ngần đó lần.
// Ta dùng mảng pi để biết quan hệ cha-con giữa các tiền tố.
for (int i = n - 1; i > 0; --i) {
freq[pi[i - 1]] += freq[i];
}
// Mỗi tiền tố xuất hiện ít nhất 1 lần (chính nó)
for (int i = 1; i <= n; ++i) {
freq[i]++;
}
for (int i = 0; i < n; ++i) {
cout << freq[i + 1] << " ";
}
return 0;
}
- Time Complexity: O(N)
- Space Complexity: O(N)
Cách tiếp cận này sử dụng thuật toán KMP (mảng tiền tố).
- Tính mảng pi. pi[i] là độ dài tiền tố dài nhất của S[0...i] cũng là hậu tố của xâu con này.
- Bước 1 (tính freq): Duyệt qua mảng pi, tăng biên dịch cho freq[pi[i]]. Điều này đếm số lần mỗi tiền tố dài k là tiền tố chung dài nhất tại một vị trí.
- Bước 2 (cộng dồn): Ta duyệt ngược từ n về 1. Nếu một tiền tố dài i xuất hiện, thì tiền tố dài pi[i-1] (là tiền tố của nó) chắc chắn cũng xuất hiện ít nhất ngần đó lần. Ta cộng dồn số lần này vào.
- Bước 3: Cộng thêm 1 cho mỗi tiền tố vì nó luôn xuất hiện ít nhất một lần.
- In kết quả.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(N) | O(N) | Z-Algorithm (Z-function) |
| 2 | O(N) | O(N) | KMP Algorithm (Prefix Function) |
Bài học kinh nghiệm
- Bài toán yêu cầu đếm số lần xuất hiện của tất cả các tiền tố của S trong S.
- Có thể sử dụng Z-function để tìm các sự xuất hiện trực tiếp, hoặc KMP để phân tích cấu trúc quan hệ giữa các tiền tố.
- Kỹ thuật Difference Array (Z-function) hoặc Duyệt ngược (KMP) là chìa khóa để tính kết quả tổng thể trong thời gian tuyến tính thay vì lặp lại nhiều lần.
Lỗi thường gặp
- Xử lý sai trường hợp tiền tố xuất hiện tại chính nó (vị trí bắt đầu).
- Lỗi off-by-one (sai biên) khi xử lý mảng cộng dồn hoặc truy cập mảng dựa trên độ dài xâu.
Bình luận