DP nhân ma trận (Matrix Exponentiation)
Lũy thừa ma trận (matrix exponentiation) là kỹ thuật biểu diễn một công thức truy hồi tuyến tính dưới dạng phép nhân ma trận, rồi dùng lũy thừa nhanh (binary exponentiation) để tính số hạng thứ trong thời gian thay vì như khi lặp tuần tự. Nhờ đó ta tính được những bài mà lên tới — quá lớn để duyệt từng bước, nhưng nên chỉ cần khoảng 60 phép nhân ma trận.
Kỹ thuật này áp dụng cho mọi DP/truy hồi mà trạng thái tiếp theo là tổ hợp tuyến tính của một số cố định trạng thái trước đó: dãy Fibonacci, đếm số đường đi đúng bước trên đồ thị, đếm cách lát/xếp với chiều dài khổng lồ, v.v.
Ý tưởng / Trực giác
Giả sử ta gom giá trị gần nhất của dãy vào một vector trạng thái. Với Fibonacci (), chọn vector 2 chiều .
Câu hỏi mấu chốt: có tồn tại một ma trận cố định biến vector trạng thái tại bước thành vector tại bước không? Nếu truy hồi là tuyến tính thì có:
Hàng đầu nói "" — đúng công thức truy hồi. Hàng hai chỉ là "" để dịch cửa sổ trượt xuống một bậc.
Vì không đổi giữa các bước, áp liên tiếp lần đưa vector ban đầu tới vector thứ :
Vì sao đúng? Phép nhân ma trận có tính kết hợp: . Mà lũy thừa tính được bằng kỹ thuật bình phương liên tiếp, giống lũy thừa số nguyên: (13 = 1101 nhị phân). Mỗi lần ta hoặc bình phương (tăng số mũ lên gấp đôi), hoặc nhân kết quả với hiện tại — tổng cộng chỉ phép nhân ma trận. Đây chính là chỗ đổi thành .
Trực giác tổng quát: mỗi phép nhân ma trận = "đi một bước truy hồi"; lũy thừa ma trận = "đi bước một lúc" bằng cách gộp các bước theo lũy thừa của 2.
Ví dụ chạy tay
Tính với (đáp số đúng là ). Dùng , cần vì .
Số mũ . Lũy thừa nhanh duyệt bit từ thấp lên cao, giữ result (khởi tạo = ma trận đơn vị ) và base (khởi tạo = ):
p = 4 (binary 100) result = [1 0] base = [1 1]
[0 1] [1 0]
--- bit 0 của p = 0 -> KHÔNG nhân result ---
base = base*base = M^2:
[1 1][1 1] [2 1]
[1 0][1 0] = [1 1]
p >>= 1 -> p = 2 (binary 10)
result = [1 0] base = [2 1]
[0 1] [1 1]
--- bit 0 của p = 0 -> KHÔNG nhân result ---
base = base*base = M^4:
[2 1][2 1] [5 3]
[1 1][1 1] = [3 2]
p >>= 1 -> p = 1 (binary 1)
result = [1 0] base = [5 3]
[0 1] [3 2]
--- bit 0 của p = 1 -> NHÂN result --- (ô đang ghi: result)
result = result*base = I*M^4 = [5 3]
[3 2]
p >>= 1 -> p = 0 -> DỪNG
Vậy . Nhân với vector đầu:
Đọc ra , — khớp. Chú ý phần tử chính bằng : một mẹo hay để lấy kết quả mà không cần nhân vector.
Cài đặt
Bộ khung dùng chung cho mọi bài: kiểu ma trận, phép nhân (có lấy modulo), và lũy thừa nhanh.
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1e9 + 7;
typedef vector<vector<long long>> Matrix;
int sz; // kích thước ma trận (k x k)
// Nhân hai ma trận k x k, lấy modulo MOD
Matrix multiply(const Matrix& A, const Matrix& B) {
Matrix C(sz, vector<long long>(sz, 0));
for (int i = 0; i < sz; i++)
for (int k = 0; k < sz; k++) {
if (A[i][k] == 0) continue; // bỏ qua phần tử 0 để tăng tốc
for (int j = 0; j < sz; j++)
// A[i][k], B[k][j] < MOD ~ 1e9 nên tích < 1e18, vừa khít long long
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
}
return C;
}
// Tính A^p bằng bình phương liên tiếp
Matrix matpow(Matrix A, long long p) {
Matrix result(sz, vector<long long>(sz, 0));
for (int i = 0; i < sz; i++) result[i][i] = 1; // ma trận đơn vị I
while (p > 0) {
if (p & 1) result = multiply(result, A); // bit hiện tại = 1: gộp vào kết quả
A = multiply(A, A); // bình phương: số mũ gấp đôi
p >>= 1;
}
return result;
}
Ví dụ 1 — Fibonacci thứ (), với :
long long fibonacci(long long n) {
if (n == 0) return 0;
sz = 2;
Matrix M = {{1, 1}, {1, 0}};
Matrix R = matpow(M, n); // M^n[0][0] = F_{n+1}, M^n[0][1] = F_n
return R[0][1]; // chính là F_n
}
Ví dụ 2 — Truy hồi tuyến tính bậc tổng quát :
// init = {f(0), f(1), ..., f(k-1)}; c = {c1, c2, ..., ck}
long long solve(long long n, vector<long long> c, vector<long long> init) {
int k = c.size();
sz = k;
if (n < k) return init[n];
// Hàng đầu là các hệ số; các hàng dưới dịch cửa sổ xuống một bậc
Matrix M(k, vector<long long>(k, 0));
for (int j = 0; j < k; j++) M[0][j] = c[j] % MOD;
for (int i = 1; i < k; i++) M[i][i-1] = 1;
// Trạng thái ban đầu V = [f(k-1), f(k-2), ..., f(0)]^T (giảm dần)
Matrix R = matpow(M, n - (k - 1));
long long ans = 0;
for (int j = 0; j < k; j++)
ans = (ans + R[0][j] * init[k - 1 - j]) % MOD;
return ans;
}
Ví dụ 3 — Đếm số đường đi đúng bước trên đồ thị: nếu là ma trận kề ( = số cạnh từ tới ) thì = số đường đi (theo cạnh, có thể lặp đỉnh) độ dài đúng từ tới . Chỉ cần lập rồi matpow(A, N).
Độ phức tạp
| Lý giải | ||
|---|---|---|
| Thời gian | Mỗi phép nhân hai ma trận tốn (ba vòng lặp lồng nhau); lũy thừa nhanh thực hiện phép nhân. | |
| Bộ nhớ | Chỉ lưu vài ma trận cùng lúc (result, base, ma trận tích tạm). |
Ước lượng thực tế: với và , ta có — chạy trong khoảng dưới một giây. Khi lớn hơn (vd ) thì và tổng lên , cần tối ưu hằng số (bỏ phần tử 0, dùng mảng phẳng thay vector<vector>).
⚠️ Lỗi thường gặp
- Tràn số khi nhân. Tích với hai thừa số cỡ cho ra , sát trần
long long(). Nếu cộng dồn nhiều tích trước khi lấy modulo, hoặc dùngint, sẽ tràn. Cách tránh: ép kiểulong long, lấy% MODngay sau mỗi lần cộng một tích. - Khởi tạo sai phần tử trung tính.
matpowphải bắt đầuresult= ma trận đơn vị (đường chéo 1, còn lại 0), không phải ma trận 0. Lấy ma trận 0 thì mọi tích về 0 và kết quả luôn sai. (Trong biến thể min-plus, phần tử trung tính lại là ma trận có 0 trên đường chéo, ngoài đường chéo — đừng nhầm.) - Lệch số mũ (off-by-one). Phải xác định rõ vector ban đầu ứng với chỉ số nào. Với thì số mũ là , không phải . Sai một bậc cho ra . Luôn kiểm bằng một giá trị nhỏ tự tính tay (như ở trên).
- Quên xử lý nhỏ. Khi (vd hỏi hay với truy hồi bậc 3), số mũ có thể âm, làm
matpowchạy sai. Phải trả thẳnginit[n]cho các trường hợp cơ sở trước khi gọi lũy thừa. - Modulo âm. Khi truy hồi có hệ số trừ (vd ), kết quả
% MODcó thể âm. Sửa bằng((x % MOD) + MOD) % MOD. - Tưởng nhanh nhưng vẫn ẩn. Nếu lỡ tính bằng vòng lặp nhân lần thay vì bình phương liên tiếp, độ phức tạp tụt về — vô dụng với . Đảm bảo
p >>= 1chứ không phảip--.
Biến thể / Mở rộng
- Thêm hằng số / đa thức vào truy hồi. Với , đưa thêm các biến phụ ( và hằng ) vào vector trạng thái để giữ tính tuyến tính. Ví dụ vector và ma trận tương ứng tự cập nhật .
- Min-plus (tropical) matrix exponentiation. Thay phép trong nhân ma trận bằng : . Khi đó cho đường đi ngắn nhất dùng đúng cạnh (phần tử trung tính là ngoài đường chéo, trên đường chéo). Đây là kỹ thuật cho bài
graphpath2. - Lũy thừa ma trận trên số mũ. Khi dãy nhân (vd ), lấy logarit/quan tâm số mũ của từng thừa số rồi dùng matrix exp trên các số mũ (theo modulo nhờ định lý Fermat nhỏ). Đây là bài
prodrec. - Liên hệ. Kỹ thuật bình phương liên tiếp giống hệt lũy thừa nhanh số nguyên; ý tưởng "gộp nhiều bước truy hồi" cũng xuất hiện trong nhiều bài quy hoạch động.
Bài tập luyện
- Số Fibonacci (fibonum) — (Intermediate) Tính với : bài mẫu kinh điển để dựng ma trận và lũy thừa nhanh.
- Đường đi trong đồ thị I (graphpath1) — (Advanced) Đếm số đường đi đúng cạnh từ đỉnh tới : chính là với là ma trận kề.
- Đường đi trong đồ thị II (graphpath2) — (Advanced) Đường đi ngắn nhất đúng cạnh: luyện biến thể min-plus (tropical) của lũy thừa ma trận.
- Truy hồi tích luỹ (prodrec) — (Expert) Dãy nhân : luyện lũy thừa ma trận trên số mũ (modulo ).
- Ngôn ngữ COWBASIC (cowbasic) — (Expert) Mô phỏng vòng lặp lồng nhau tới lần: biểu diễn mỗi vòng lặp thành ma trận biến đổi tuyến tính rồi lũy thừa.