Wiki Thuật toán Toán học Nhân ma trận (Matrix Exponentiation)

Nhân ma trận (Matrix Exponentiation)

huunguyen huunguyen Updated Tháng sáu 1, 2026

Nhân ma trận nhanh (Matrix Exponentiation) là kỹ thuật tính lũy thừa Mn của một ma trận vuông K×K trong O(K3logN) thay vì O(N) 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 n lên tới 1018, và đếm/đo đường đi có đúng K 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 Fn=Fn1+Fn2 làm ví dụ. Gọi trạng thái là vector cột chứa hai số liên tiếp:

vn=(FnFn1).

Ta muốn tìm một ma trận M sao cho vn=M·vn1. Vì Fn=1·Fn1+1·Fn2Fn1=1·Fn1+0·Fn2:

(FnFn1)=(1110)(Fn1Fn2).

Đây là chìa khoá. Áp dụng M một lần là "đi một bước". Áp dụng nó n lần liên tiếp:

vn=M·vn1=M2·vn2==Mn1·v1.

Câu hỏi còn lại: làm sao tính Mn1 nhanh? Nếu nhân M với chính nó n1 lần thì vẫn O(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ụ M13=M8·M4·M1 (vì 13=11012). Ta chỉ cần bình phương liên tiếp M,M2,M4,M8, — tốn logn 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 Ma·Mb=Ma+b, nên gộp các "khối" lũy thừa 2 lại đúng bằng Mn. Một phép nhân hai ma trận K×K tốn O(K3), ta làm O(logn) lần, nên tổng cộng O(K3logn).

Tổng quát: bất kỳ đại lượng nào ở bước n đượ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ị, (AK)[i][j] — số đường đi K cạnh từ i tới j — chính là một dạng "truy hồi" trên ma trận kề, vì đường đi K cạnh = đi 1 cạnh rồi đi K1 cạnh.

Ví dụ chạy tay

Tính F5 qua M4·v1 với v1=(F1F0)=(10)M=(1110).

Số mũ là 4=1002. 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 M4=(5332). Nhân với v1:

M4v1=(5332)(10)=(53)=(F5F4).

Kết quả F5=5, đúng (dãy 0,1,1,2,3,5). Chỉ tốn 2 phép bình phương ma trận thay vì 4 bước cộng — với n=1018 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ế:

M2=(1110)(1110)=(1·1+1·11·1+1·01·1+0·11·1+0·0)=(2111).

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 k tổng quát an=c1an1++ckank, 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 K cạnh trong đồ thị N đỉnh: lấy ma trận kề A (với A[i][j]=1 nếu có cạnh ij), thì (AK)[i][j] là số đường đi K cạnh từ i tới j.

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ớ
Mn với ma trận K×K O(K3logn) O(K2)
Fibonacci / truy hồi bậc 2, số hạng thứ n O(logn) O(1)
Truy hồi tuyến tính bậc K O(K3logn) O(K2)
Đường đi đúng K cạnh, đồ thị N đỉnh O(N3logK) O(N2)

Vì sao O(K3logn)? Một phép nhân hai ma trận K×K có ba vòng lặp lồng nhau nên tốn O(K3). Lũy thừa nhị phân thực hiện O(logn) phép nhân (mỗi bit của n 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 O(K3logn).

Bộ nhớ O(K2): ta chỉ lưu vài ma trận K×K (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). A[i][k]·B[k][j] với hai số cỡ 109 cho tích cỡ 1018 — vượt int (giới hạn ~2,1×109) và sát trần long long (9,2×1018). Dùng long long cho tích, và luôn lấy modulo trước để mọi phần tử ma trận đều <MOD, đảm bảo tích từng cặp <1018. Cộng dồn nhiều tích vẫn an toàn vì ta % MOD ngay sau mỗi lần cộng.

  • Quên khởi tạo ma trận đơn vị I. Nếu result khởi tạo bằng ma trận 0 (hoặc bằng A), kết quả sẽ sai hoàn toàn. Phần tử trung hoà của phép nhân ma trận là I (đườ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 vn=Mn1v1 chứ không phải Mnv1; 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 n nhỏ (F5=5) trước khi tin vào công thức.

  • Quên xử lý biên n nhỏ. Với truy hồi bậc k, khi n<k thì đáp án nằm thẳng trong giá trị khởi tạo init[n], không được đưa vào mat_pow (số mũ âm sẽ sai hoặc vòng while bỏ qua, trả về I cho kết quả lệch). Kiểm tra if (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 an=an1an2), kết quả % MOD có 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ề [0,MOD) 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 mink(A[i][k]+B[k][j]) và "cộng" thay bằng min. Đừ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 I số học.

Biến thể / Mở rộng

  • Đại số min-plus / max-plus (tropical semiring): thay (+,×) bằng (min,+) để tìm đường đi ngắn nhất có đúng K cạnh, hoặc bằng (max,+) cho bài tối đa. Khung lũy thừa nhị phân giữ nguyên, chỉ đổi định nghĩa multiply và ma trận đơn vị.
  • Truy hồi có hằng số (affine): với an=c1an1++d (có số hạng cộng cố định d), thêm một thành phần hằng 1 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 Fn với n1018, bài nhập môn kinh điển: dựng ma trận 2×2 rồi lũy thừa nhanh.
  • Đường đi trong đồ thị I (graphpath1)(Advanced) đếm số đường đi đúng k cạnh từ đỉnh 1 tới n, chính là (Ak)[1][n] với A là ma trận kề.
  • Đường đi trong đồ thị II (graphpath2)(Advanced) tìm đường đi ngắn nhất đúng k cạnh, luyện biến thể min-plus: thay phép nhân ma trận bằng min(A[i][k]+B[k][j]).
  • Truy hồi tích luỹ (prodrec)(Expert) truy hồi nhân fx=c2x6fx1fx2fx3: 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 MOO lặp tới 105 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 O(logn).
gnatmake 12.2.0 a68g 3.1.2 nasm 2.16.1 as_x64 2.46 awk 1.3.4 gcc 16.1.0 csc 6.12.0.200 g++ 16.1.0 g++-themis 16.1.0 g++17 16.1.0 g++20 16.1.0 g++23 16.1.0 clang++ 22.1.6 dmd 2.112.0 dart 3.12.1 gforth 0.7.3 gfortran 12.2.0 go 1.26.3 groovyc 5.0.6 javac 25.0.3 node 26.2.0 kotlinc 2.3.21 sbcl 2.2.9 lua 5.4.8 nim 2.2.10 fpc 3.2.2 fpc-themis 3.2.2 perl 5.36.0 php 8.5.6 pike 8.0 pypy3 7.3.23 python3 3.14.5 racket 8.7 ruby 4.0.5 rustc 1.96.0 csc 5.3.0 ctoj-scratch 0.0.1 sed 4.9 tclsh 8.6 bun 1.3.14 deno 2.8.1 v 0.5.1 zig 0.16.0