Berlekamp-Massey
Berlekamp-Massey (BM) là thuật toán tìm công thức truy hồi tuyến tính (linear recurrence) ngắn nhất sinh ra một dãy số cho trước. Nói cách khác: cho vài chục phần tử đầu của một dãy , BM tìm bậc nhỏ nhất và các hệ số sao cho
Khi đã có công thức truy hồi bậc , ta tính phần tử thứ (với rất lớn, vd ) trong bằng nhân ma trận lũy thừa (matrix exponentiation), hoặc / bằng Kitamasa. So với việc lặp phần tử thì đây là bước nhảy vọt khi khổng lồ và nhỏ.
Điểm mạnh thực chiến: nhiều bài đếm/đại số cho ra một dãy mà ta không biết công thức, nhưng nếu sinh được phần tử đầu bằng vét cạn / DP, BM sẽ "đoán" ra công thức truy hồi, rồi matrix-expo nhảy tới phần tử thứ . BM chạy trong với là số phần tử đưa vào.
Ý tưởng / Trực giác
BM xử lý dãy tăng dần từng phần tử và duy trì một bộ hệ số ứng viên — gọi là đa thức đặc trưng bậc — sao cho công thức truy hồi tương ứng đúng cho toàn bộ tiền tố đã đọc.
Ý tưởng cốt lõi nằm ở khái niệm độ lệch (discrepancy) . Giả sử ta đang ở phần tử thứ và đang dùng truy hồi hiện tại có bậc . Ta tính giá trị mà truy hồi dự đoán cho , rồi lấy
- Nếu : truy hồi hiện tại vẫn đúng tới , không cần làm gì.
- Nếu : truy hồi sai ở vị trí này, phải sửa. Mấu chốt là ta đã lưu lại "lần sửa trước đó" — bộ hệ số cùng độ lệch tại bước mà nó lần đầu thất bại. Khi đó cộng thêm một bản dịch (shift) và co giãn của vào :
Tại sao phép cộng này lại "chữa" được đúng một độ lệch mà không phá các vị trí trước? Vì được chọn sao cho khi nhân với , nó tạo ra độ lệch đúng tại vị trí và bằng 0 tại mọi vị trí trước đó. Nhân hệ số làm độ lệch nó tạo ra đúng bằng , nên khi trừ đi, độ lệch tại bị triệt tiêu hoàn hảo.
Việc tăng bậc chỉ xảy ra khi . Đây là bất biến đảm bảo tính ngắn nhất: có một định lý (liên quan tới chặn dưới của độ phức tạp tuyến tính của LFSR) khẳng định rằng nếu một dãy tiền tố độ dài không thể sinh bởi truy hồi bậc thì bậc tối thiểu mới phải là . BM cập nhật đúng theo công thức này, nên kết quả luôn là truy hồi bậc nhỏ nhất.
Trực giác tổng: BM giống như giải hệ phương trình tuyến tính tìm hệ số truy hồi, nhưng thực hiện gia tăng (incremental) — mỗi khi thêm một phương trình (một phần tử mới), nó chỉ chỉnh đúng phần cần chỉnh thay vì giải lại từ đầu, đạt thay vì của khử Gauss.
Ví dụ chạy tay
Lấy dãy (chính là ). Ta kỳ vọng BM tìm ra truy hồi bậc 2: .
Quy ước: là đa thức hiện tại (lưu dạng , tức , là hệ số của ). là bậc truy hồi, là bản sao "lần sửa trước", là độ lệch lúc đó, là khoảng dịch.
Khởi tạo: , , , , .
n=0: a=1
d = s[0] = 1 (chưa có hạng tử nào để trừ)
d != 0, và 2L=0 <= n=0 -> tăng bậc
C cũ = [1]; coef = d/b = 1/1 = 1
C trở thành [1] - 1*x^1*[1] = [1, -1]
L = 0+1-0 = 1 ; B = [1] (C cũ) ; b = 1 ; x = 1
Trạng thái: C=[1,-1] L=1
chỉ số: 0 1
ý nghĩa: a_n = 1*a_{n-1}
n=1: a=3
dự đoán = c1*a0 = 1*1 = 1 ; d = 3 - 1 = 2
d != 0, nhưng 2L=2 > n=1 -> KHÔNG tăng bậc, chỉ chỉnh C
coef = d/b = 2/1 = 2
C = [1,-1] - 2*x^1*[1] = [1, -1-2] = [1, -3]
x++ -> x=2
Trạng thái: C=[1,-3] L=1
a_n = 3*a_{n-1} (đúng tới n=1: 3 = 3*1 ✓)
n=2: a=7
dự đoán = 3*a1 = 3*3 = 9 ; d = 7 - 9 = -2
d != 0, và 2L=2 <= n=2 -> tăng bậc
C cũ = [1,-3]; coef = d/b = -2/2 = -1 (b vẫn =1 từ n=0!)
--- lưu ý: b=1 (độ lệch lần tăng bậc trước, ở n=0), x=2
coef = d/b = -2/1 = -2
C = [1,-3] - (-2)*x^2*[1] = [1, -3, +2] = [1,-3,2]
L = 2+1-1 = 2 ; B = [1,-3] (C cũ) ; b = -2 ; x = 1
Trạng thái: C=[1,-3,2] L=2
a_n = 3*a_{n-1} - 2*a_{n-2}
Kiểm tra: 3*7 - 2*3 = 21-6 = 15 = a3 ✓
n=3: a=15
dự đoán = 3*a2 - 2*a1 = 21 - 6 = 15 ; d = 15-15 = 0 -> bỏ qua, x++
n=4: a=31
dự đoán = 3*a3 - 2*a2 = 45 - 14 = 31 ; d = 0 -> bỏ qua
Kết quả: , tức , hay
Chuyển sang dạng hệ số dương trả về: , đúng như kỳ vọng. (Trong cài đặt dưới đây mọi phép tính làm trên modulo, sẽ thành .)
Cài đặt
Cài đặt BM trên trường ( nguyên tố, dùng nghịch đảo modulo). Sau đó kết hợp với nhân ma trận để tính phần tử thứ .
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1e9 + 7;
ll power(ll a, ll b, ll mod = MOD) {
ll res = 1; a %= mod; if (a < 0) a += mod;
for (; b > 0; b >>= 1) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
}
return res;
}
ll inv(ll a) { return power(a, MOD - 2); } // nghịch đảo: chỉ đúng khi MOD nguyên tố
// Trả về rec sao cho a[n] = sum_{i} rec[i] * a[n-1-i]
vector<ll> berlekampMassey(const vector<ll>& s) {
vector<ll> C = {1}, B = {1}; // C: đa thức hiện tại; B: bản sao lần sửa trước
int L = 0, m = 1; // L: bậc truy hồi; m: khoảng dịch (shift)
ll b = 1; // độ lệch tại lần tăng bậc gần nhất
for (int n = 0; n < (int)s.size(); n++) {
// tính độ lệch d = a[n] - (dự đoán theo truy hồi hiện tại)
ll d = s[n] % MOD;
for (int i = 1; i <= L; i++)
d = (d + C[i] * s[n - i]) % MOD;
d = (d % MOD + MOD) % MOD;
if (d == 0) { m++; continue; } // truy hồi vẫn đúng tới n
if (2 * L <= n) { // cần tăng bậc
vector<ll> T = C; // lưu C trước khi sửa
ll coef = d % MOD * inv(b) % MOD;
// C(x) -= coef * x^m * B(x)
while ((int)C.size() < (int)B.size() + m) C.push_back(0);
for (int i = 0; i < (int)B.size(); i++)
C[i + m] = (C[i + m] - coef * B[i] % MOD + MOD) % MOD;
L = n + 1 - L; // bậc tối thiểu mới
B = T; b = d; m = 1; // cập nhật bản sao + độ lệch + reset dịch
} else { // chỉ chỉnh hệ số, không tăng bậc
ll coef = d % MOD * inv(b) % MOD;
while ((int)C.size() < (int)B.size() + m) C.push_back(0);
for (int i = 0; i < (int)B.size(); i++)
C[i + m] = (C[i + m] - coef * B[i] % MOD + MOD) % MOD;
m++;
}
}
// C = [1, -r1, -r2, ...]; trả rec[i] = -C[i+1]
vector<ll> rec(C.size() - 1);
for (int i = 1; i < (int)C.size(); i++)
rec[i - 1] = (MOD - C[i]) % MOD;
return rec;
}
// Nhân ma trận lũy thừa để tính a[n] từ truy hồi rec bậc K và K phần tử đầu
ll kth_term(const vector<ll>& rec, const vector<ll>& init, ll n) {
int K = rec.size();
if (n < (int)init.size()) return init[n] % MOD;
// ma trận chuyển trạng thái K x K
auto mul = [&](vector<vector<ll>>& A, vector<vector<ll>>& Bm) {
vector<vector<ll>> R(K, vector<ll>(K, 0));
for (int i = 0; i < K; i++)
for (int k = 0; k < K; k++) if (A[i][k])
for (int j = 0; j < K; j++)
R[i][j] = (R[i][j] + A[i][k] * Bm[k][j]) % MOD;
return R;
};
vector<vector<ll>> M(K, vector<ll>(K, 0)), Res(K, vector<ll>(K, 0));
for (int j = 0; j < K; j++) M[0][j] = rec[j]; // hàng đầu = hệ số truy hồi
for (int i = 1; i < K; i++) M[i][i - 1] = 1; // dịch trạng thái
for (int i = 0; i < K; i++) Res[i][i] = 1; // ma trận đơn vị
ll e = n - (K - 1);
for (; e > 0; e >>= 1) { if (e & 1) Res = mul(Res, M); M = mul(M, M); }
// trạng thái ban đầu [a_{K-1}, ..., a_0]^T
ll ans = 0;
for (int j = 0; j < K; j++)
ans = (ans + Res[0][j] * init[K - 1 - j]) % MOD;
return ans;
}
Ví dụ dùng:
vector<ll> fib = {0, 1, 1, 2, 3, 5, 8, 13};
auto rec = berlekampMassey(fib); // rec = {1, 1}: a_n = a_{n-1} + a_{n-2}
cout << kth_term(rec, {0, 1}, 100); // F_100 mod 1e9+7
Độ phức tạp
- Berlekamp-Massey: thời gian (chính xác hơn, tổng kích thước nhân số phần tử) với là số phần tử đưa vào và là bậc truy hồi tìm được, cộng cho mỗi nghịch đảo modulo (có thể gộp lại còn vài lần). Mỗi phần tử phải duyệt qua hệ số để tính độ lệch, nên tổng là . Bộ nhớ cho các đa thức cộng lưu dãy đầu vào.
- Nhân ma trận lũy thừa: thời gian — mỗi phép nhân hai ma trận tốn , và lũy thừa cần phép nhân. Bộ nhớ . Đây là lý do BM chỉ hiệu quả khi nhỏ (vài chục) còn rất lớn ().
- Kitamasa (thay matrix-expo): , hoặc nếu nhân đa thức bằng NTT — tốt hơn khi lớn.
Để BM tìm đúng truy hồi bậc , cần cấp ít nhất phần tử đầu (an toàn nên lấy nhiều hơn, vd ); nếu không truy hồi có thể bị "khớp giả" với bậc thấp hơn thực tế.
⚠️ Lỗi thường gặp
- Tràn số khi nhân: với vượt phạm vi
int. Mọi tích phải éplong longvà lấy modulo ngay:(a * b) % MOD. Quên% MODtrong vòng tính độ lệch là lỗi phổ biến nhất gây sai âm thầm. - Modulo âm: độ lệch và các hệ số sau phép trừ có thể âm. Luôn chuẩn hóa
(x % MOD + MOD) % MOD. Đặc biệt khi trả vềrec[i] = (MOD - C[i]) % MOD, nếuC[i] = 0mà quên% MODcuối thì raMODthay vì0. - Nghịch đảo khi MOD không nguyên tố:
inv(b) = power(b, MOD-2)chỉ đúng theo định lý Fermat khi MOD nguyên tố. Nếu bài cho modulo hợp số (vd vẫn nguyên tố, nhưng thì không),power(b, MOD-2)cho kết quả sai — phải dùng nghịch đảo modulo qua Euclid mở rộng hoặc đổi cách tiếp cận. Ngoài ra nếu chia hết cho MOD thì không tồn tại nghịch đảo. - Cấp không đủ phần tử: đưa vào dưới phần tử khiến BM "đoán" ra truy hồi bậc thấp hơn thực, đúng với mẫu đã cho nhưng sai với phần tử kế tiếp. Luôn sinh dư phần tử và, nếu có thể, kiểm tra lại truy hồi tìm được trên vài phần tử cuối chưa dùng.
- Sai chỉ số init khi matrix-expo: vector trạng thái ban đầu phải là theo đúng thứ tự ma trận; đảo thứ tự
inithoặc nhầm số mũ là bẫy off-by-one khiến lệch một phần tử. - Dãy không là truy hồi tuyến tính: BM luôn trả về một kết quả, kể cả khi dãy không hề tuân theo truy hồi tuyến tính cố định (vd dãy ngẫu nhiên) — khi đó bậc sẽ ≈ và vô nghĩa. BM chỉ áp dụng khi bạn có cơ sở tin dãy là truy hồi tuyến tính (đếm cấu hình theo , lũy thừa ma trận chuyển trạng thái cố định...).
Biến thể / Mở rộng
- Kitamasa thay nhân ma trận: tính trong vành đa thức để lấy , giảm từ xuống (hoặc với NTT) — bắt buộc khi lớn.
- BM trên GF(2): dùng cho dãy bit / thanh ghi dịch phản hồi tuyến tính (LFSR), cài bằng
bitset, hữu ích trong mật mã và một số bài XOR. - Đoán công thức (guessing): mẹo thực chiến — viết brute-force sinh – phần tử đầu của đáp số theo , cho qua BM; nếu ra bậc nhỏ ổn định thì gần như chắc chắn đã tìm đúng truy hồi, rồi matrix-expo nhảy tới lớn. Liên hệ với nhân ma trận lũy thừa như công cụ tính phần tử thứ .
Bài tập luyện
- Số Fibonacci (fibonum) — (Intermediate) Tính với : bài "đích ngắm" kinh điển của truy hồi tuyến tính bậc 2, luyện đúng bước matrix-expo (hoặc BM tìm ra rồi nhảy tới ).
- DZY và dãy Fibonacci (fibadd) — (Expert) Cập nhật đoạn cộng dãy Fibonacci và truy vấn tổng: khai thác cấu trúc truy hồi tuyến tính của Fibonacci để gộp cập nhật trên cây phân đoạn — luyện tư duy "Fibonacci như nghiệm của truy hồi tuyến tính".
- Truy hồi tích lũy (prodrec) — (Expert) Truy hồi dạng tích với : lấy logarit (làm việc trên số mũ trong vành chỉ số) đưa về truy hồi tuyến tính bậc 3 cộng hạng tử đa thức, rồi matrix-expo — đúng tinh thần BM/Kitamasa khi khổng lồ.