Nhân ma trận (Matrix Exponentiation)
Nhân ma trận nhanh (Matrix Exponentiation) là kỹ thuật tính lũy thừa của một ma trận vuông trong thay vì phép cộng/nhân ngây thơ. Nhờ đó nó giải được công thức truy hồi tuyến tính (như Fibonacci) với lên tới , và đếm/đo đường đi có đúng cạnh trong đồ thị — những bài mà mô phỏng từng bước là bất khả thi.
Ý tưởng / Trực giác
Mọi công thức truy hồi tuyến tính đều có thể viết lại thành một phép nhân ma trận với vector trạng thái. Lấy Fibonacci làm ví dụ. Gọi trạng thái là vector cột chứa hai số liên tiếp:
Ta muốn tìm một ma trận sao cho . Vì và :
Đây là chìa khoá. Áp dụng một lần là "đi một bước". Áp dụng nó lần liên tiếp:
Câu hỏi còn lại: làm sao tính nhanh? Nếu nhân với chính nó lần thì vẫn , chẳng lợi gì. Nhưng phép nhân ma trận có tính kết hợp, nên ta dùng đúng ý tưởng của lũy thừa nhị phân (binary exponentiation): phân tích số mũ theo cơ số 2. Ví dụ (vì ). Ta chỉ cần bình phương liên tiếp — tốn phép nhân ma trận — rồi nhân vào kết quả những lũy thừa ứng với bit bằng 1.
Vì sao đúng? Tính kết hợp của nhân ma trận đảm bảo , nên gộp các "khối" lũy thừa 2 lại đúng bằng . Một phép nhân hai ma trận tốn , ta làm lần, nên tổng cộng .
Tổng quát: bất kỳ đại lượng nào ở bước được tính từ một tổ hợp tuyến tính cố định của các đại lượng ở các bước trước đều đặt được vào khuôn này. Với đồ thị, — số đường đi cạnh từ tới — chính là một dạng "truy hồi" trên ma trận kề, vì đường đi cạnh = đi 1 cạnh rồi đi cạnh.
Ví dụ chạy tay
Tính qua với và .
Số mũ là . Lũy thừa nhị phân duyệt các bit của 4 từ thấp lên cao, vừa bình phương A, vừa nhân vào result khi bit = 1:
n=4 (100b) bit result (ban đầu = I) A (ban đầu = M)
----------- --- -------------------------- ----------------
bước 1 0 I (không nhân) A = M^2 = |2 1|
|1 1|
bước 2 0 I (không nhân) A = M^4 = |5 3|
^ |3 2|
bit này
bước 3 1 result = I * M^4 = M^4 (dừng)
^
bit cao nhất
Vậy . Nhân với :
Kết quả , đúng (dãy ). Chỉ tốn 2 phép bình phương ma trận thay vì 4 bước cộng — với thì khác biệt là sống còn.
Kiểm tra nhanh một phép bình phương để thấy cơ chế:
Cài đặt
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Matrix;
const long long MOD = 1e9 + 7;
// Nhân hai ma trận A (n x p) và B (p x m), kết quả lấy theo MOD.
Matrix multiply(const Matrix& A, const Matrix& B) {
int n = A.size(), p = B.size(), m = B[0].size();
Matrix C(n, vector<long long>(m, 0));
for (int i = 0; i < n; i++)
for (int k = 0; k < p; k++)
if (A[i][k]) // bỏ qua phần tử 0 để tăng tốc
for (int j = 0; j < m; j++)
// A[i][k] đã < MOD nên A[i][k]*B[k][j] < 1e18, an toàn với long long
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
return C;
}
// Tính A^n bằng lũy thừa nhị phân: O(K^3 log n).
Matrix mat_pow(Matrix A, long long n) {
int sz = A.size();
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 (n > 0) {
if (n & 1) result = multiply(result, A); // bit thấp nhất = 1 -> nhân vào
A = multiply(A, A); // bình phương để "lên một bậc lũy thừa 2"
n >>= 1;
}
return result;
}
// Fibonacci thứ n (F_0 = 0, F_1 = 1) theo MOD.
long long fibonacci(long long n) {
if (n == 0) return 0;
Matrix M = {{1, 1}, {1, 0}};
Matrix R = mat_pow(M, n); // R = M^n; khi đó R[0][1] = F_n
return R[0][1];
}
int main() {
long long n;
cin >> n;
cout << fibonacci(n) << "\n";
}
Với truy hồi tuyến tính bậc tổng quát , ma trận chuyển là ma trận đồng hành (companion matrix):
// init = {a_0, a_1, ..., a_{k-1}}; c = {c_1, ..., c_k}
long long linear_recurrence(vector<long long> c, vector<long long> init, long long n) {
int k = c.size();
if (n < k) return init[n] % MOD;
Matrix M(k, vector<long long>(k, 0));
for (int j = 0; j < k; j++) M[0][j] = c[j]; // hàng đầu là các hệ số
for (int i = 1; i < k; i++) M[i][i-1] = 1; // dịch trạng thái xuống dưới
Matrix R = mat_pow(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)) % MOD;
return ans % MOD;
}
Với đếm đường đi đúng cạnh trong đồ thị đỉnh: lấy ma trận kề (với nếu có cạnh ), thì là số đường đi cạnh từ tới .
Matrix A_k = mat_pow(adj, K); // adj là ma trận kề N x N
long long paths = A_k[s][t]; // số đường đi đúng K cạnh từ s tới t
Độ phức tạp
| Bài toán | Thời gian | Bộ nhớ |
|---|---|---|
| với ma trận | ||
| Fibonacci / truy hồi bậc 2, số hạng thứ | ||
| Truy hồi tuyến tính bậc | ||
| Đường đi đúng cạnh, đồ thị đỉnh |
Vì sao ? Một phép nhân hai ma trận có ba vòng lặp lồng nhau nên tốn . Lũy thừa nhị phân thực hiện phép nhân (mỗi bit của tốn nhiều nhất 2 phép: một bình phương, một nhân vào kết quả). Nhân hai con số đó ra .
Bộ nhớ : ta chỉ lưu vài ma trận (kết quả, ma trận đang bình phương, ma trận tạm trong multiply). Không cần lưu lịch sử các bước, vì lũy thừa nhị phân chỉ giữ trạng thái hiện tại.
⚠️ Lỗi thường gặp
Tràn số khi nhân (overflow). với hai số cỡ cho tích cỡ — vượt
int(giới hạn ~) và sát trầnlong long(). Dùnglong longcho tích, và luôn lấy modulo trước để mọi phần tử ma trận đều , đảm bảo tích từng cặp . Cộng dồn nhiều tích vẫn an toàn vì ta% MODngay sau mỗi lần cộng.Quên khởi tạo ma trận đơn vị . Nếu
resultkhởi tạo bằng ma trận 0 (hoặc bằng ), kết quả sẽ sai hoàn toàn. Phần tử trung hoà của phép nhân ma trận là (đường chéo 1, còn lại 0), giống vai trò của số 1 trong lũy thừa số học.Sai số mũ (off-by-one). Với Fibonacci chứ không phải ; tuỳ cách định nghĩa trạng thái mà số mũ lệch 1. Hãy kiểm tra bằng tay với nhỏ () trước khi tin vào công thức.
Quên xử lý biên nhỏ. Với truy hồi bậc , khi thì đáp án nằm thẳng trong giá trị khởi tạo
init[n], không được đưa vàomat_pow(số mũ âm sẽ sai hoặc vòngwhilebỏ qua, trả về cho kết quả lệch). Kiểm traif (n < k) return init[n];ngay đầu hàm.Modulo âm khi bài toán có phép trừ. Nếu công thức truy hồi có hệ số âm (vd ), kết quả
% MODcó thể âm trong C++. Sửa bằng((x % MOD) + MOD) % MOD. Tương tự khi đưa hệ số âm vào ma trận, hãy chuẩn hoá về trước.Modulo trên bài "đường đi ngắn nhất K cạnh". Bài tối ưu (min/max) dùng đại số min-plus: phép "nhân" thay bằng và "cộng" thay bằng . Đừng lấy modulo và đừng dùng phép cộng/nhân thường — phần tử trung hoà lúc này là ma trận toàn với đường chéo 0, không phải số học.
Biến thể / Mở rộng
- Đại số min-plus / max-plus (tropical semiring): thay bằng để tìm đường đi ngắn nhất có đúng cạnh, hoặc bằng cho bài tối đa. Khung lũy thừa nhị phân giữ nguyên, chỉ đổi định nghĩa
multiplyvà ma trận đơn vị. - Truy hồi có hằng số (affine): với (có số hạng cộng cố định ), thêm một thành phần hằng vào vector trạng thái và một hàng/cột tương ứng trong ma trận để "mang" hằng số đó. Đây là mấu chốt để biến vòng lặp gán tuyến tính thành lũy thừa ma trận.
- Liên hệ: kỹ thuật này dựa trực tiếp trên lũy thừa nhị phân áp dụng lên ma trận thay vì số.
Bài tập luyện
- Số Fibonacci (fibonum) — (Intermediate) tính với , bài nhập môn kinh điển: dựng ma trận rồi lũy thừa nhanh.
- Đường đi trong đồ thị I (graphpath1) — (Advanced) đếm số đường đi đúng cạnh từ đỉnh 1 tới , chính là với là ma trận kề.
- Đường đi trong đồ thị II (graphpath2) — (Advanced) tìm đường đi ngắn nhất đúng cạnh, luyện biến thể min-plus: thay phép nhân ma trận bằng .
- Truy hồi tích luỹ (prodrec) — (Expert) truy hồi nhân : lấy log/đếm số mũ để đưa về truy hồi tuyến tính trên mũ rồi lũy thừa ma trận (kết hợp với định lý Fermat nhỏ).
- Ngôn Ngữ COWBASIC (cowbasic) — (Expert) mỗi vòng lặp
MOOlặp tới lần là một phép gán affine: gộp thân vòng thành một ma trận chuyển trạng thái rồi lũy thừa để mô phỏng cả vòng trong .