Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Chuyên đề nâng cao 2: Lũy thừa ma trận trong C++ và Python: nhân ma trận, Fibonacci thứ n với n tới 10^18, truy hồi tuyến tính, đếm đường đi độ dài k

Chuyên đề nâng cao 2: Lũy thừa ma trận trong C++ và Python: nhân ma trận, Fibonacci thứ n với n tới 10^18, truy hồi tuyến tính, đếm đường đi độ dài k
Series: Chuyên đề nâng cao Độ khó: Khá Chuyên đề nâng cao 2 / Thuật toán nâng cao Ma trận Lũy thừa nhanh
// Chuyên đề nâng cao · Chuyên đề 2
Lũy thừa ma trận: Fibonacci thứ 1018, truy hồi tuyến tính, đếm đường đi độ dài k
Một bài quy hoạch động mà mỗi bước chỉ “nhân với cùng một bảng số” thì $n = 10^{18}$ bước cũng chỉ tốn vài chục phép nhân ma trận. Đây là vũ khí biến những bài QHĐ quen thuộc thành bài có $n$ cực lớn.

Lời mở đầu

Ở bài 19 (quy hoạch động) em đã đếm số cách leo cầu thang, số cách lát gạch, số xâu nhị phân hợp lệ bằng một vòng for từ 1 tới $n$. Ở bài 26 (toán modulo) em đã học lũy thừa nhanh: tính $a^b \bmod p$ với $b = 10^{18}$ chỉ bằng khoảng 60 vòng lặp. Đề thi HSG tỉnh và chuyên Tin rất hay “ghép” hai thứ đó lại: bài QHĐ quen thuộc nhưng $n$ tới $10^{18}$, vòng for chạy tới tận thế kỷ sau cũng chưa xong.

Chìa khoá là nhận ra: mỗi bước QHĐ chỉ là nhân vector trạng thái với một ma trận cố định. Mà phép nhân ma trận có tính kết hợp, nên $n$ bước nhân liên tiếp gộp lại thành $M^n$, và $M^n$ tính được bằng đúng thuật toán lũy thừa nhanh em đã biết. Chuyên đề này dạy em làm việc đó từ đầu tới cuối: nhân ma trận, lũy thừa ma trận, cách dựng ma trận chuyển cho mọi công thức truy hồi tuyến tính, và ứng dụng đẹp nhất: đếm đường đi độ dài $k$ trên đồ thị (đã gặp đồ thị ở bài 21).

Trong bài này bạn sẽ học: nhân hai ma trận (hàng × cột), độ phức tạp $O(k^3)$, ma trận đơn vị và cách lấy modulo không tràn số; lũy thừa nhanh ma trận $O(k^3 \log n)$ theo từng bit của $n$; tính Fibonacci thứ $n$ với $n \le 10^{18}$; khuôn mẫu dựng ma trận chuyển cho truy hồi tuyến tính bậc $k$, kể cả khi có hằng số, số hạng $n$ hay tổng tiền tố; đếm đường đi độ dài $k$ bằng $A^k$ và biến bài QHĐ có $n$ lớn (đếm xâu, lát gạch) thành lũy thừa ma trận. Có khung chạy tay, 3 mô phỏng bấm từng bước và 8 bài tập kèm gợi ý.

Lộ trình bài viết, bấm vào từng phần để nhảy tới:

Phần 1Nhân ma trận

Ma trận cỡ $n \times m$ đơn giản là một mảng hai chiều $n$ hàng, $m$ cột (em đã dùng ở bài 7). Cho $A$ cỡ $n \times m$ và $B$ cỡ $m \times p$ (số cột của $A$ phải bằng số hàng của $B$), tích $C = A \cdot B$ là ma trận cỡ $n \times p$ với

$C[i][j] = \sum_{t=0}^{m-1} A[i][t] \cdot B[t][j]$.

Nói bằng lời: ô $C[i][j]$ lấy hàng $i$ của $A$ và cột $j$ của $B$, nhân từng cặp phần tử tương ứng rồi cộng lại. Ví dụ $\begin{pmatrix}1 & 2 \\ 3 & 4\end{pmatrix} \cdot \begin{pmatrix}5 & 6 \\ 7 & 8\end{pmatrix} = \begin{pmatrix}19 & 22 \\ 43 & 50\end{pmatrix}$, vì ô góc trên trái là $1 \cdot 5 + 2 \cdot 7 = 19$, ô góc trên phải là $1 \cdot 6 + 2 \cdot 8 = 22$, v.v.

Em bấm Tiếp để xem từng ô của $C$ được tính ra sao, rồi thử nhập ma trận khác (kể cả ma trận không vuông):

MÔ PHỎNGNhân hai ma trận: mỗi ô là hàng × cộtNhập A và B (các hàng cách nhau bởi dấu ;), tối đa 4×4. Bấm Tiếp để tính từng ô của C.
hàng i của Acột j của Bô C[i][j] vừa tínhchưa dùng / chưa tính

Độ phức tạp. $C$ có $n \cdot p$ ô, mỗi ô tốn $m$ phép nhân, tổng $O(n \cdot m \cdot p)$. Trong chuyên đề này ta chủ yếu nhân hai ma trận vuông cỡ $k \times k$, tốn $O(k^3)$: $k = 2$ chỉ 8 phép nhân, $k = 100$ là $10^6$ phép.

Hai tính chất quan trọng của phép nhân ma trận:

  • Có tính kết hợp: $(A \cdot B) \cdot C = A \cdot (B \cdot C)$. Nhờ vậy $M \cdot M \cdot \ldots \cdot M$ ($n$ lần) muốn gộp theo thứ tự nào cũng được, và đó chính là thứ lũy thừa nhanh cần.
  • Không giao hoán: thường $A \cdot B \ne B \cdot A$. Ví dụ trên đổi chỗ thì $\begin{pmatrix}5 & 6 \\ 7 & 8\end{pmatrix} \cdot \begin{pmatrix}1 & 2 \\ 3 & 4\end{pmatrix} = \begin{pmatrix}23 & 34 \\ 31 & 46\end{pmatrix}$, khác hẳn. Khi code em phải giữ đúng thứ tự nhân.

Ma trận đơn vị $I$ (đường chéo bằng 1, còn lại bằng 0) đóng vai số 1: $I \cdot X = X \cdot I = X$. Ta quy ước $M^0 = I$.

Xem code C++ / Python: nhân hai ma trận (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

long long A[105][105], B[105][105], C[105][105];

int main(){
    int n, m, p;
    cin >> n >> m >> p;                          // A cỡ n x m, B cỡ m x p
    for(int i = 0; i < n; i++)
        for(int t = 0; t < m; t++) cin >> A[i][t];
    for(int t = 0; t < m; t++)
        for(int j = 0; j < p; j++) cin >> B[t][j];
    for(int i = 0; i < n; i++)
        for(int j = 0; j < p; j++){
            C[i][j] = 0;
            for(int t = 0; t < m; t++)           // hàng i của A "nhân" cột j của B
                C[i][j] += A[i][t] * B[t][j];
        }
    for(int i = 0; i < n; i++){
        for(int j = 0; j < p; j++) cout << C[i][j] << " ";
        cout << "\n";
    }
    return 0;
}
import sys

data = sys.stdin.read().split()
n, m, p = int(data[0]), int(data[1]), int(data[2])
vals = list(map(int, data[3:]))
A = [vals[i * m:(i + 1) * m] for i in range(n)]
off = n * m
B = [vals[off + t * p: off + (t + 1) * p] for t in range(m)]
cols = list(zip(*B))                              # các cột của B
C = [[sum(x * y for x, y in zip(row, col)) for col in cols] for row in A]
for row in C:
    print(*row)
Nhân ma trận với modulo không bị tràn số
Khi các phần tử đã được lấy dư cho $p = 10^9 + 7$ (tức là $< p$), mỗi tích A[i][t] * B[t][j] nhỏ hơn $p^2 \approx 10^{18}$, vừa đủ chứa trong long long (tối đa khoảng $9{,}2 \cdot 10^{18}$). Nhưng cộng dồn hai tích là có thể vượt! Vì vậy hãy lấy dư sau mỗi lần cộng: C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;. Một lỗi rất hay gặp nữa là để phần tử ban đầu âm hoặc $\ge p$ (ví dụ hệ số $-1$): phải đưa về đoạn $[0, p)$ trước khi nhân.

Phần 2Lũy thừa nhanh ma trận

Cần tính $M^n$ với $M$ là ma trận vuông $k \times k$ và $n$ có thể tới $10^{18}$. Nhân lần lượt $n$ lần thì không bao giờ xong. Nhưng nhớ lại lũy thừa nhanh ở bài 26: viết $n$ ra hệ nhị phân, ví dụ $13 = 1101_2 = 8 + 4 + 1$, thì $M^{13} = M^{8} \cdot M^{4} \cdot M^{1}$. Các lũy thừa $M^1, M^2, M^4, M^8, \ldots$ có được bằng cách bình phương liên tục. Thuật toán y hệt bản cho số, chỉ thay hai chỗ:

  • res = 1 thành res = I (ma trận đơn vị);
  • phép nhân hai số thành phép nhân hai ma trận (hàm mul ở phần 1, có lấy dư).

Vì phép nhân ma trận có tính kết hợp nên ghép các $M^{2^i}$ lại theo thứ tự nào cũng ra đúng $M^n$ (và các lũy thừa của cùng một ma trận thì nhân với nhau giao hoán được, nên ta không lo thứ tự). Số vòng lặp là số bit của $n$, khoảng $\log_2 n$; mỗi vòng tối đa 2 phép nhân ma trận $O(k^3)$. Tổng cộng $O(k^3 \log n)$: với $k = 2$, $n = 10^{18}$ chỉ khoảng $2 \cdot 60 \cdot 8 \approx 1000$ phép nhân số.

Bit iBit của 13P = $M^{2^i}$Nhân vào res?res
01$\begin{pmatrix}1 & 1 \\ 1 & 0\end{pmatrix}$✔ có$M^1 = \begin{pmatrix}1 & 1 \\ 1 & 0\end{pmatrix}$
10$M^2 = \begin{pmatrix}2 & 1 \\ 1 & 1\end{pmatrix}$✘ không$M^1$
21$M^4 = \begin{pmatrix}5 & 3 \\ 3 & 2\end{pmatrix}$✔ có$M^5 = \begin{pmatrix}8 & 5 \\ 5 & 3\end{pmatrix}$
31$M^8 = \begin{pmatrix}34 & 21 \\ 21 & 13\end{pmatrix}$✔ có$M^{13} = \begin{pmatrix}377 & 233 \\ 233 & 144\end{pmatrix}$

Bảng 1. Tính $M^{13}$ với $M = \begin{pmatrix}1 & 1 \\ 1 & 0\end{pmatrix}$ (ma trận Fibonacci ở phần 3): chỉ 4 vòng lặp, 6 phép nhân ma trận (3 lần nhân vào res ứng với 3 bit 1, 3 lần bình phương; vòng cuối không bình phương nữa) thay vì 12.

Mô phỏng dưới đây cho em thấy hai ma trận res và P thay đổi qua từng bit. Em thử nhập ma trận $3 \times 3$ và $n$ thật lớn để thấy số phép nhân vẫn rất ít:

MÔ PHỎNGLũy thừa nhanh ma trận: M, M², M⁴, M⁸… theo từng bit của nNhập ma trận vuông M (tối đa 3×3) và số mũ n. Bit nào của n bằng 1 thì P hiện tại được nhân vào res.
bit đang xét / P vừa bình phươngbit 1: nhân P vào resbit 0: bỏ quabit đã xét
Xem code C++ / Python: lũy thừa ma trận k × k (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;
int k;

Mat mul(const Mat &A, const Mat &B){
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

Mat matPow(Mat M, long long n){
    Mat res(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) res[i][i] = 1;   // ma trận đơn vị I
    while(n > 0){
        if(n & 1) res = mul(res, M);             // bit 1: nhân M^(2^i) vào res
        n >>= 1;
        if(n > 0) M = mul(M, M);                 // còn bit thì mới bình phương: M^(2^i) -> M^(2^(i+1))
    }
    return res;
}

int main(){
    long long n;
    cin >> k >> n;
    Mat M(k, vector<long long>(k));
    for(int i = 0; i < k; i++)
        for(int j = 0; j < k; j++){ cin >> M[i][j]; M[i][j] %= MOD; }
    Mat R = matPow(M, n);
    for(int i = 0; i < k; i++){
        for(int j = 0; j < k; j++) cout << R[i][j] << " ";
        cout << "\n";
    }
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))                          # các cột của B
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, n):
    k = len(M)
    res = [[int(i == j) for j in range(k)] for i in range(k)]   # ma trận đơn vị I
    while n > 0:
        if n & 1:
            res = mul(res, M)                     # bit 1: nhân M^(2^i) vào res
        n >>= 1
        if n > 0:                                 # còn bit thì mới bình phương
            M = mul(M, M)
    return res

data = sys.stdin.read().split()
k, n = int(data[0]), int(data[1])
vals = [int(x) % MOD for x in data[2:2 + k * k]]
M = [vals[i * k:(i + 1) * k] for i in range(k)]
for row in mat_pow(M, n):
    print(*row)

Em chạy tay từng dòng để thấy biến M bình phương liên tục (trừ vòng cuối, khi $n$ đã hết bit thì không cần bình phương nữa), còn res chỉ nhận thêm thừa số khi gặp bit 1. Bấm dữ liệu mẫu “n = 0” để kiểm tra trường hợp biên: vòng lặp không chạy lần nào và kết quả là $I$.

C++▶ Chạy tay từng dòng codeLũy thừa nhanh ma trận: res bắt đầu là I; mỗi vòng, bit thấp nhất của n bằng 1 thì res = res · M; rồi n >>= 1, và nếu n còn khác 0 thì M = M · M.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3typedef vector<vector<long long>> Mat;
4const long long MOD = 1000000007;
5int k;
6
7Mat mul(const Mat &A, const Mat &B){
8 Mat C(k, vector<long long>(k, 0));
9 for(int i = 0; i < k; i++)
10 for(int t = 0; t < k; t++)
11 for(int j = 0; j < k; j++)
12 C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
13 return C;
14}
15
16int main(){
17 long long n;
18 cin >> k >> n;
19 Mat M(k, vector<long long>(k));
20 for(int i = 0; i < k; i++)
21 for(int j = 0; j < k; j++) cin >> M[i][j];
22 Mat res(k, vector<long long>(k, 0));
23 for(int i = 0; i < k; i++) res[i][i] = 1;
24 while(n > 0){
25 if(n & 1) res = mul(res, M);
26 n >>= 1;
27 if(n > 0) M = mul(M, M);
28 }
29 for(int i = 0; i < k; i++){
30 for(int j = 0; j < k; j++) cout << res[i][j] << " ";
31 cout << "\n";
32 }
33 return 0;
34}
Khuôn mẫu lũy thừa ma trận
Mat matPow(Mat M, long long n){
    Mat res = I;                       // ma trận đơn vị
    while(n > 0){
        if(n & 1) res = mul(res, M);
        n >>= 1;
        if(n > 0) M = mul(M, M);   // hết bit thì khỏi bình phương
    }
    return res;
}
Đây là đoạn code em sẽ chép lại trong mọi bài của chuyên đề. Phần “khó” của mỗi bài không nằm ở đây mà ở chỗ dựng ra ma trận $M$ (phần 3, 4, 5).

Phần 3Fibonacci thứ $n$ với $n$ tới $10^{18}$

Dãy Fibonacci: $F(0) = 0$, $F(1) = 1$, $F(n) = F(n-1) + F(n-2)$. Để tính $F(n)$ ta chỉ cần nhớ hai số liên tiếp. Gọi cặp $(F(n), F(n-1))$ là trạng thái tại bước $n$. Từ trạng thái này sang trạng thái kế tiếp $(F(n+1), F(n))$:

$F(n+1) = 1 \cdot F(n) + 1 \cdot F(n-1)$,    $F(n) = 1 \cdot F(n) + 0 \cdot F(n-1)$.

Cả hai thành phần mới đều là tổ hợp tuyến tính (nhân hệ số rồi cộng) của thành phần cũ, nên viết được thành phép nhân ma trận với vector cột:

$\begin{pmatrix}F(n+1) \\ F(n)\end{pmatrix} = \begin{pmatrix}1 & 1 \\ 1 & 0\end{pmatrix} \cdot \begin{pmatrix}F(n) \\ F(n-1)\end{pmatrix}$.

Hàng thứ nhất của ma trận là hệ số của công thức tính $F(n+1)$, hàng thứ hai là hệ số để “chép” $F(n)$ xuống. Đặt $M = \begin{pmatrix}1 & 1 \\ 1 & 0\end{pmatrix}$, áp dụng liên tiếp $n$ lần từ trạng thái đầu $(F(1), F(0)) = (1, 0)$:

$\begin{pmatrix}F(n+1) \\ F(n)\end{pmatrix} = M^n \cdot \begin{pmatrix}1 \\ 0\end{pmatrix}$,    và thật ra $M^n = \begin{pmatrix}F(n+1) & F(n) \\ F(n) & F(n-1)\end{pmatrix}$.

Vậy $F(n)$ chính là ô $[0][1]$ của $M^n$. Em kiểm tra lại Bảng 1: $M^{13}$ có ô $[0][1] = 233 = F(13)$, ô $[0][0] = 377 = F(14)$.

0F(0)1F(1)1F(2)2F(3)3F(4)5F(5)8F(6)13F(7)21F(8)nhân thêm M một lầntrạng thái (F(4), F(3)) = (3, 2)trạng thái mới (F(5), F(4)) = (5, 3)

Hình 1. Trạng thái là một “cửa sổ” gồm 2 số Fibonacci liên tiếp. Mỗi lần nhân với $M$, cửa sổ trượt sang phải 1 ô. Trượt $n$ lần = nhân $M^n$.

Xem code C++ / Python: Fibonacci thứ n, n ≤ 10^18 (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;
struct M2 { ll a, b, c, d; };                    // ma trận [[a, b], [c, d]]

M2 mul(M2 x, M2 y){
    return { (x.a * y.a + x.b * y.c) % MOD, (x.a * y.b + x.b * y.d) % MOD,
             (x.c * y.a + x.d * y.c) % MOD, (x.c * y.b + x.d * y.d) % MOD };
}

int main(){
    ll n;
    cin >> n;
    M2 res = {1, 0, 0, 1}, M = {1, 1, 1, 0};      // res = I, M = ma trận Fibonacci
    while(n > 0){
        if(n & 1) res = mul(res, M);
        M = mul(M, M);
        n >>= 1;
    }
    cout << res.b;                                // M^n = [[F(n+1), F(n)], [F(n), F(n-1)]]
    return 0;
}
MOD = 10**9 + 7

def mul(x, y):                                    # ma trận 2x2 lưu dạng (a, b, c, d)
    a, b, c, d = x
    e, f, g, h = y
    return ((a * e + b * g) % MOD, (a * f + b * h) % MOD,
            (c * e + d * g) % MOD, (c * f + d * h) % MOD)

n = int(input())
res, M = (1, 0, 0, 1), (1, 1, 1, 0)               # res = I, M = ma trận Fibonacci
while n > 0:
    if n & 1:
        res = mul(res, M)
    M = mul(M, M)
    n >>= 1
print(res[1])                                     # M^n = [[F(n+1), F(n)], [F(n), F(n-1)]]
Vì sao viết riêng ma trận 2 × 2?
Với $k$ nhỏ cố định, viết tay 4 công thức như struct M2 ở trên chạy nhanh hơn vector<vector> nhiều lần (không cấp phát bộ nhớ). Khi đề có nhiều truy vấn $F(n)$, mỗi truy vấn $O(\log n)$, cách này giúp em không bị quá thời gian. Ngoài ra còn có công thức “nhân đôi”: $F(2m) = F(m)\,(2F(m+1) - F(m))$ và $F(2m+1) = F(m)^2 + F(m+1)^2$, thật ra chính là bình phương ma trận $M^m$ viết gọn lại.

Phần 4Truy hồi tuyến tính tổng quát: dựng ma trận chuyển

Fibonacci chỉ là trường hợp riêng. Mọi công thức dạng

$f(n) = c_1 f(n-1) + c_2 f(n-2) + \ldots + c_k f(n-k)$

(gọi là truy hồi tuyến tính bậc $k$) đều giải được bằng lũy thừa ma trận $k \times k$. Trạng thái là $k$ giá trị gần nhất $v(n) = (f(n), f(n-1), \ldots, f(n-k+1))$, và ma trận chuyển $T$ gồm: hàng 0 là các hệ số $c_1, c_2, \ldots, c_k$ (để tính $f(n+1)$); các hàng còn lại có một số 1 ngay dưới đường chéo (để dịch các giá trị cũ xuống một ô):

$\begin{pmatrix}f(n+1) \\ f(n) \\ f(n-1) \\ \vdots \\ f(n-k+2)\end{pmatrix} = \begin{pmatrix}c_1 & c_2 & \cdots & c_{k-1} & c_k \\ 1 & 0 & \cdots & 0 & 0 \\ 0 & 1 & \cdots & 0 & 0 \\ \vdots & & \ddots & & \vdots \\ 0 & 0 & \cdots & 1 & 0\end{pmatrix} \cdot \begin{pmatrix}f(n) \\ f(n-1) \\ f(n-2) \\ \vdots \\ f(n-k+1)\end{pmatrix}$.

Ta biết sẵn $v(k-1) = (f(k-1), \ldots, f(0))$, nên $v(n) = T^{\,n-k+1} \cdot v(k-1)$ và đáp án là thành phần đầu của $v(n)$. Nếu $n < k$ thì in luôn $f(n)$ có sẵn.

Xem code C++ / Python: truy hồi tuyến tính bậc k bằng lũy thừa ma trận (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;
int k;

Mat mul(const Mat &A, const Mat &B){
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

int main(){
    long long n;
    cin >> k >> n;
    vector<long long> c(k), f(k);
    for(auto &x : c) cin >> x;                    // c1..ck
    for(auto &x : f) cin >> x;                    // f(0)..f(k-1)
    if(n < k){ cout << f[n] % MOD; return 0; }
    Mat T(k, vector<long long>(k, 0));
    for(int j = 0; j < k; j++) T[0][j] = c[j] % MOD;      // hàng 0: f(n) = c1 f(n-1) + ... + ck f(n-k)
    for(int i = 1; i < k; i++) T[i][i - 1] = 1;           // các hàng sau: dịch trạng thái xuống 1 ô
    Mat R(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) R[i][i] = 1;
    long long e = n - k + 1;                              // v(n) = T^(n-k+1) . v(k-1)
    while(e > 0){
        if(e & 1) R = mul(R, T);
        T = mul(T, T);
        e >>= 1;
    }
    long long ans = 0;                                    // v(k-1) = (f(k-1), f(k-2), ..., f(0))
    for(int j = 0; j < k; j++) ans = (ans + R[0][j] * (f[k - 1 - j] % MOD)) % MOD;
    cout << ans;
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, e):
    k = len(M)
    res = [[int(i == j) for j in range(k)] for i in range(k)]
    while e > 0:
        if e & 1:
            res = mul(res, M)
        M = mul(M, M)
        e >>= 1
    return res

data = sys.stdin.read().split()
k, n = int(data[0]), int(data[1])
c = [int(x) % MOD for x in data[2:2 + k]]
f = [int(x) % MOD for x in data[2 + k:2 + 2 * k]]
if n < k:
    print(f[n])
else:
    T = [[0] * k for _ in range(k)]
    T[0] = c[:]                                   # hàng 0: công thức truy hồi
    for i in range(1, k):
        T[i][i - 1] = 1                           # các hàng sau: dịch trạng thái
    R = mat_pow(T, n - k + 1)
    v = f[::-1]                                   # v(k-1) = (f(k-1), ..., f(0))
    print(sum(R[0][j] * v[j] for j in range(k)) % MOD)

Khi công thức có thêm “đồ phụ”. Đề thi hiếm khi cho truy hồi “sạch”. Bí quyết: đưa mọi thứ cần dùng vào vector trạng thái, kể cả hằng số 1, chỉ số $n$, hay tổng tiền tố. Miễn là mỗi thành phần ở bước sau đều là tổ hợp tuyến tính của các thành phần ở bước trước, ta dựng được ma trận. Bảng dưới là các dạng hay gặp nhất:

Công thứcVector trạng tháiMa trận chuyển $T$
$f(n) = f(n-1) + f(n-2) + d$$(f(n), f(n-1), 1)$$\begin{pmatrix}1 & 1 & d \\ 1 & 0 & 0 \\ 0 & 0 & 1\end{pmatrix}$
$f(n) = f(n-1) + n$$(f(n), n+1, 1)$$\begin{pmatrix}1 & 1 & 0 \\ 0 & 1 & 1 \\ 0 & 0 & 1\end{pmatrix}$
Tổng $S(n) = F(1) + \ldots + F(n)$ (Fibonacci)$(S(n), F(n+1), F(n))$$\begin{pmatrix}1 & 1 & 0 \\ 0 & 1 & 1 \\ 0 & 1 & 0\end{pmatrix}$
Hai dãy đan nhau: $a_n = a_{n-1} + 2b_{n-1}$, $b_n = a_{n-1} + b_{n-1}$$(a_n, b_n)$$\begin{pmatrix}1 & 2 \\ 1 & 1\end{pmatrix}$
$f(n) = 4f(n-2) - f(n-4)$ (hệ số âm)$(f(n), f(n-2))$, bước nhảy 2$\begin{pmatrix}4 & -1 \\ 1 & 0\end{pmatrix}$ với $-1 \equiv p - 1$

Bảng 2. Mỗi hàng của $T$ trả lời câu hỏi: “thành phần này ở bước sau bằng bao nhiêu lần mỗi thành phần ở bước trước?”. Ví dụ hàng 2 của dạng $f(n) = f(n-1) + n$: $(n+2) = 1 \cdot (n+1) + 1 \cdot 1$.

Thử kiểm tra dạng thứ hai với $f(0) = 0$: trạng thái đầu $v(0) = (0, 1, 1)$. Nhân $T$ được $(0 + 1, 1 + 1, 1) = (1, 2, 1)$, tức $f(1) = 1$; nhân tiếp được $(1 + 2, 3, 1)$, tức $f(2) = 3 = 0 + 1 + 2$. Đúng!

Khuôn mẫu dựng ma trận chuyển
1. Chọn vector trạng thái $v$ chứa đủ thông tin để tính bước kế tiếp (thiếu gì thì thêm vào: $f(n-2)$, hằng số 1, $n$, tổng $S$…).
2. Với từng thành phần của $v(n+1)$, viết nó thành tổ hợp tuyến tính của các thành phần của $v(n)$. Hệ số chính là một hàng của $T$.
3. Xác định trạng thái đầu $v(n_0)$ đã biết, rồi $v(n) = T^{\,n - n_0} \cdot v(n_0)$. Xử lý riêng $n < n_0$.
4. Kiểm tra bằng tay 2–3 bước đầu (như đoạn trên) trước khi nộp. Rất nhiều bạn sai vì lệch số mũ 1 đơn vị.
Những gì KHÔNG đưa được vào ma trận
Ma trận chỉ biểu diễn được phép cộng và nhân với hằng số. Công thức có $f(n-1) \cdot f(n-2)$, $f(n-1)^2$, $\max$, chia lấy nguyên… thì không dùng lũy thừa ma trận trực tiếp được. Hệ số phụ thuộc $n$ như $f(n) = n \cdot f(n-1)$ cũng không được (ma trận phải cố định ở mọi bước). Riêng số hạng cộng thêm là đa thức của $n$ (như $+n$, $+n^2$) thì vẫn được, bằng cách thêm $n$, $n^2$, 1 vào trạng thái.

Phần 5Đếm đường đi độ dài $k$ và QHĐ với $n$ lớn

Cho đồ thị có hướng $n$ đỉnh với ma trận kề $A$: $A[i][j]$ là số cạnh từ $i$ tới $j$. Hỏi có bao nhiêu đường đi độ dài đúng $k$ (đi qua đúng $k$ cạnh, được lặp lại đỉnh và cạnh) từ $u$ tới $v$?

Định lý: số đó bằng $(A^k)[u][v]$. Lý do rất tự nhiên: một đường đi độ dài $k$ từ $u$ tới $v$ = một đường đi độ dài $k-1$ từ $u$ tới đỉnh $w$ nào đó, rồi đi thêm cạnh cuối $w \to v$. Nên

$\text{cnt}_k[u][v] = \sum_{w} \text{cnt}_{k-1}[u][w] \cdot A[w][v]$,

và đây đúng là công thức nhân ma trận $\text{cnt}_k = \text{cnt}_{k-1} \cdot A$. Bắt đầu với $\text{cnt}_1 = A$ nên $\text{cnt}_k = A^k$. Em xem từng bước trong mô phỏng: mỗi lần nhân thêm $A$, ô $[u][v]$ được cộng từ những đỉnh $w$ có cạnh đi vào $v$:

MÔ PHỎNGĐếm đường đi độ dài k bằng A^kNhập số đỉnh (≤ 5), các cạnh có hướng “a b”, độ dài k (≤ 8) và hai đỉnh u, v. Mỗi bước nhân thêm ma trận kề A.
đỉnh xuất phát u, hàng uđỉnh đích v, ô [u][v]đỉnh w góp vào bước cuối
Xem code C++ / Python: đếm đường đi độ dài k từ s tới t (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;
int n;

Mat mul(const Mat &A, const Mat &B){
    Mat C(n, vector<long long>(n, 0));
    for(int i = 0; i < n; i++)
        for(int t = 0; t < n; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < n; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

int main(){
    int m, s, t;
    long long k;
    cin >> n >> m >> k >> s >> t;
    Mat A(n, vector<long long>(n, 0));
    for(int i = 0; i < m; i++){
        int u, v;
        cin >> u >> v;
        A[u - 1][v - 1]++;                        // cạnh có hướng u -> v
    }
    Mat R(n, vector<long long>(n, 0));
    for(int i = 0; i < n; i++) R[i][i] = 1;
    while(k > 0){
        if(k & 1) R = mul(R, A);
        A = mul(A, A);
        k >>= 1;
    }
    cout << R[s - 1][t - 1];                      // (A^k)[s][t] = số đường đi độ dài k từ s tới t
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

data = sys.stdin.read().split()
n, m, k, s, t = map(int, data[:5])
A = [[0] * n for _ in range(n)]
for i in range(m):
    u, v = int(data[5 + 2 * i]), int(data[6 + 2 * i])
    A[u - 1][v - 1] += 1                          # cạnh có hướng u -> v
R = [[int(i == j) for j in range(n)] for i in range(n)]
while k > 0:
    if k & 1:
        R = mul(R, A)
    A = mul(A, A)
    k >>= 1
print(R[s - 1][t - 1])                            # (A^k)[s][t]

Nối với quy hoạch động. Rất nhiều bài QHĐ đếm thực chất là đếm đường đi trên một đồ thị trạng thái nhỏ. Lấy ví dụ: đếm số xâu độ dài $n$ chỉ gồm chữ A, B, C mà không chứa “AB”. Khi thêm một chữ vào cuối, ta chỉ cần biết xâu hiện tại có tận cùng bằng A hay không:

  • Trạng thái 0 (không tận cùng bằng A): thêm A sang trạng thái 1; thêm B hoặc C vẫn ở trạng thái 0.
  • Trạng thái 1 (tận cùng bằng A): thêm A vẫn ở 1; thêm C về 0; thêm B thì tạo ra “AB”, cấm.
0không tận cùng A1tận cùng là Athêm B hoặc C (2 cách)thêm Athêm Athêm Cthêm Bcó “AB”T = [[2, 1], [1, 1]]: T[s][s'] = số chữ cái đưa trạng thái s sang s'

Hình 2. Đồ thị trạng thái của bài đếm xâu không chứa “AB”. Mỗi xâu hợp lệ độ dài $n$ là một đường đi độ dài $n$ xuất phát từ trạng thái 0 (xâu rỗng).

QHĐ thông thường: $dp[i][s]$ = số xâu độ dài $i$ kết thúc ở trạng thái $s$, và $dp[i][s'] = \sum_s dp[i-1][s] \cdot T[s][s']$, chạy $O(n)$. Nhưng công thức ấy chính là nhân vector hàng $dp[i-1]$ với ma trận $T$ cố định, nên $dp[n] = dp[0] \cdot T^n$ với $dp[0] = (1, 0)$. Đáp án là tổng hàng 0 của $T^n$.

$i$$dp[i][0]$$dp[i][1]$TổngKiểm tra
0101xâu rỗng
1$1 \cdot 2 = 2$$1 \cdot 1 = 1$3A, B, C
2$2 \cdot 2 + 1 \cdot 1 = 5$$2 \cdot 1 + 1 \cdot 1 = 3$8$3^2 - 1$ (bỏ “AB”)
3$5 \cdot 2 + 3 = 13$$5 + 3 = 8$21$27 - 6$ (bỏ ABx, xAB)

Bảng 3. QHĐ đếm xâu không chứa “AB” trên ba bước đầu. Với $n = 10^{18}$, thay vòng lặp bằng $T^n$.

Xem code C++ / Python: đếm xâu A, B, C không chứa “AB” với n ≤ 10^18 (bấm để mở)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;
typedef array<array<ll, 2>, 2> M2;

M2 mul(const M2 &A, const M2 &B){
    M2 C{};
    for(int i = 0; i < 2; i++)
        for(int t = 0; t < 2; t++)
            for(int j = 0; j < 2; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
    return C;
}

int main(){
    ll n;
    cin >> n;
    M2 T = {{{2, 1}, {1, 1}}};                    // T[s][s'] = số chữ cái đưa trạng thái s sang s'
    M2 R = {{{1, 0}, {0, 1}}};
    while(n > 0){
        if(n & 1) R = mul(R, T);
        T = mul(T, T);
        n >>= 1;
    }
    cout << (R[0][0] + R[0][1]) % MOD;            // xuất phát ở trạng thái 0 (xâu rỗng), kết thúc ở đâu cũng được
    return 0;
}
MOD = 10**9 + 7

def mul(A, B):
    return [[(A[i][0] * B[0][j] + A[i][1] * B[1][j]) % MOD for j in range(2)] for i in range(2)]

n = int(input())
T = [[2, 1], [1, 1]]                              # T[s][s'] = số chữ cái đưa trạng thái s sang s'
R = [[1, 0], [0, 1]]
while n > 0:
    if n & 1:
        R = mul(R, T)
    T = mul(T, T)
    n >>= 1
print((R[0][0] + R[0][1]) % MOD)                  # xuất phát ở trạng thái 0 (xâu rỗng)

Cùng một cách nghĩ, em giải được hàng loạt bài QHĐ quen thuộc với $n$ khổng lồ: leo cầu thang mỗi bước 1, 2 hoặc 3 bậc (bài tập 3), xâu nhị phân không có ba số 1 liên tiếp (bài tập 4), lát nền $2 \times n$ hay $3 \times n$ bằng gạch $1 \times 2$ (bài tập 7), xâu không chứa một mẫu bất kỳ (bài tập 8, trạng thái là độ dài phần đang khớp với mẫu, giống ý tưởng của KMP ở bài 28).

Khi nào nên (và không nên) dùng lũy thừa ma trận?
Dấu hiệu: $n$ rất lớn ($10^{9}$ tới $10^{18}$), số trạng thái $k$ nhỏ (thường $\le 50$, tối đa khoảng 100), và chuyển trạng thái giống nhau ở mọi bước. Độ phức tạp $O(k^3 \log n)$: $k = 100$, $n = 10^{18}$ là khoảng $10^6 \cdot 120 \approx 1{,}2 \cdot 10^8$ phép tính, sát giới hạn 1 giây trong C++ và quá chậm với Python. Nếu $n \le 10^7$ thì QHĐ $O(n \cdot k)$ bình thường vừa đơn giản vừa nhanh hơn. Mẹo trong C++: duyệt theo thứ tự i, t, j và bỏ qua A[i][t] == 0 như code mẫu giúp nhanh hơn đáng kể với ma trận thưa.
Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Lũy thừa nhanh.

Bài tậpLuyện lũy thừa ma trận

Bài 1Fibonacci tổng quátTrung bình

Dãy $G$ có $G(0) = a$, $G(1) = b$ và $G(n) = G(n-1) + G(n-2)$ với $n \ge 2$. Cho $a, b, n, m$ ($0 \le a, b \le 10^9$, $0 \le n \le 10^{18}$, $1 \le m \le 10^9 + 7$), hãy tính $G(n) \bmod m$.

Input
2 1 10 1000000007
Output
123

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Vẫn là ma trận $M = \begin{pmatrix}1 & 1 \\ 1 & 0\end{pmatrix}$, chỉ khác trạng thái đầu: $(G(n+1), G(n)) = M^n \cdot (b, a)$, nên $G(n) = M^n[1][0] \cdot b + M^n[1][1] \cdot a$. Với $a = 2$, $b = 1$ ta được dãy Lucas 2, 1, 3, 4, 7, 11, 18, 29, 47, 76, 123. Chú ý $m = 1$ thì đáp án luôn là 0, nên ma trận đơn vị phải khởi tạo bằng 1 % m.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll MOD;
struct M2 { ll a, b, c, d; };

M2 mul(M2 x, M2 y){
    return { (x.a * y.a + x.b * y.c) % MOD, (x.a * y.b + x.b * y.d) % MOD,
             (x.c * y.a + x.d * y.c) % MOD, (x.c * y.b + x.d * y.d) % MOD };
}

int main(){
    ll a, b, n;
    cin >> a >> b >> n >> MOD;
    M2 res = {1 % MOD, 0, 0, 1 % MOD}, M = {1 % MOD, 1 % MOD, 1 % MOD, 0};
    while(n > 0){
        if(n & 1) res = mul(res, M);
        M = mul(M, M);
        n >>= 1;
    }
    // (G(n+1), G(n)) = M^n . (G(1), G(0)) = M^n . (b, a)
    cout << (res.c * (b % MOD) + res.d * (a % MOD)) % MOD;
    return 0;
}
a, b, n, MOD = map(int, input().split())

def mul(x, y):
    p, q, r, s = x
    e, f, g, h = y
    return ((p * e + q * g) % MOD, (p * f + q * h) % MOD, (r * e + s * g) % MOD, (r * f + s * h) % MOD)

res, M = (1 % MOD, 0, 0, 1 % MOD), (1 % MOD, 1 % MOD, 1 % MOD, 0)
while n > 0:
    if n & 1:
        res = mul(res, M)
    M = mul(M, M)
    n >>= 1
print((res[2] * b + res[3] * a) % MOD)            # G(n) = hàng 1 của M^n nhân (b, a)
Bài 2Tổng các số FibonacciTrung bình

Cho $n$ ($0 \le n \le 10^{18}$). Tính $S(n) = F(1) + F(2) + \ldots + F(n)$ theo modulo $10^9 + 7$, với $F(1) = F(2) = 1$.

Input
5
Output
12

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Dùng dòng thứ ba của Bảng 2: trạng thái $(S(n), F(n+1), F(n))$, vì $S(n+1) = S(n) + F(n+1)$ và $F(n+2) = F(n+1) + F(n)$. Trạng thái đầu $v(0) = (0, 1, 0)$, nên $S(n)$ là ô $[0][1]$ của $T^n$. Ví dụ: $1 + 1 + 2 + 3 + 5 = 12$. (Có công thức $S(n) = F(n+2) - 1$, em tự chứng minh thử, nhưng mục tiêu bài này là luyện dựng ma trận.)
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;

Mat mul(const Mat &A, const Mat &B){
    int k = A.size();
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

Mat matPow(Mat M, long long e){
    int k = M.size();
    Mat R(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) R[i][i] = 1;
    while(e > 0){
        if(e & 1) R = mul(R, M);
        M = mul(M, M);
        e >>= 1;
    }
    return R;
}

int main(){
    long long n;
    cin >> n;
    // trạng thái (S(n), F(n+1), F(n)); S(n+1) = S(n) + F(n+1), F(n+2) = F(n+1) + F(n)
    Mat T = {{1, 1, 0},
             {0, 1, 1},
             {0, 1, 0}};
    Mat R = matPow(T, n);
    // v(0) = (S(0), F(1), F(0)) = (0, 1, 0) nên S(n) = R[0][1]
    cout << R[0][1];
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, e):
    k = len(M)
    R = [[int(i == j) for j in range(k)] for i in range(k)]
    while e > 0:
        if e & 1:
            R = mul(R, M)
        M = mul(M, M)
        e >>= 1
    return R

n = int(input())
# trạng thái (S(n), F(n+1), F(n))
T = [[1, 1, 0],
     [0, 1, 1],
     [0, 1, 0]]
print(mat_pow(T, n)[0][1])                        # v(0) = (0, 1, 0)
Bài 3Leo cầu thang 1, 2, 3 bậcTrung bình

Một cầu thang có $n$ bậc ($0 \le n \le 10^{18}$). Mỗi bước em leo được 1, 2 hoặc 3 bậc. Có bao nhiêu cách leo từ chân lên đúng bậc thứ $n$? In theo modulo $10^9 + 7$. (Với $n = 0$ có đúng 1 cách: đứng yên.)

Input
4
Output
7

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Bước cuối cùng dài 1, 2 hoặc 3 bậc nên $f(n) = f(n-1) + f(n-2) + f(n-3)$, với $f(0) = 1$, $f(1) = 1$, $f(2) = 2$. Đây là truy hồi bậc 3: ma trận $\begin{pmatrix}1 & 1 & 1 \\ 1 & 0 & 0 \\ 0 & 1 & 0\end{pmatrix}$, $v(n) = T^{\,n-2} \cdot (2, 1, 1)$. Bảy cách leo 4 bậc: 1111, 112, 121, 211, 22, 13, 31.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;

Mat mul(const Mat &A, const Mat &B){
    int k = A.size();
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

Mat matPow(Mat M, long long e){
    int k = M.size();
    Mat R(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) R[i][i] = 1;
    while(e > 0){
        if(e & 1) R = mul(R, M);
        M = mul(M, M);
        e >>= 1;
    }
    return R;
}

int main(){
    long long n;
    cin >> n;
    long long f[3] = {1, 1, 2};                   // f(0), f(1), f(2)
    if(n < 3){ cout << f[n]; return 0; }
    Mat T = {{1, 1, 1},                           // f(i+1) = f(i) + f(i-1) + f(i-2)
             {1, 0, 0},
             {0, 1, 0}};
    Mat R = matPow(T, n - 2);                     // v(n) = T^(n-2) . v(2), v(2) = (2, 1, 1)
    cout << (R[0][0] * 2 + R[0][1] + R[0][2]) % MOD;
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, e):
    k = len(M)
    R = [[int(i == j) for j in range(k)] for i in range(k)]
    while e > 0:
        if e & 1:
            R = mul(R, M)
        M = mul(M, M)
        e >>= 1
    return R

n = int(input())
f = [1, 1, 2]
if n < 3:
    print(f[n])
else:
    T = [[1, 1, 1], [1, 0, 0], [0, 1, 0]]
    R = mat_pow(T, n - 2)                         # v(2) = (2, 1, 1)
    print((R[0][0] * 2 + R[0][1] + R[0][2]) % MOD)
Bài 4Xâu nhị phân không có ba số 1 liên tiếpTrung bình

Đếm số xâu nhị phân độ dài $n$ ($1 \le n \le 10^{18}$) không chứa ba chữ số 1 đứng liền nhau. In theo modulo $10^9 + 7$.

Input
3
Output
7

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Trạng thái = số chữ 1 liên tiếp ở cuối xâu: 0, 1 hoặc 2. Thêm chữ 0 luôn về trạng thái 0; thêm chữ 1 thì từ $s$ sang $s + 1$, nhưng từ 2 thì không được thêm 1. Ma trận $T[s][s'] = \begin{pmatrix}1 & 1 & 0 \\ 1 & 0 & 1 \\ 1 & 0 & 0\end{pmatrix}$, đáp án là tổng hàng 0 của $T^n$ (xuất phát từ xâu rỗng). Với $n = 3$: 8 xâu trừ đi “111” còn 7.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;

Mat mul(const Mat &A, const Mat &B){
    int k = A.size();
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

Mat matPow(Mat M, long long e){
    int k = M.size();
    Mat R(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) R[i][i] = 1;
    while(e > 0){
        if(e & 1) R = mul(R, M);
        M = mul(M, M);
        e >>= 1;
    }
    return R;
}

int main(){
    long long n;
    cin >> n;
    // trạng thái s = số chữ 1 liên tiếp ở cuối xâu (0, 1, 2)
    Mat T = {{1, 1, 0},                           // từ 0: thêm '0' -> 0, thêm '1' -> 1
             {1, 0, 1},                           // từ 1: thêm '0' -> 0, thêm '1' -> 2
             {1, 0, 0}};                          // từ 2: chỉ được thêm '0' -> 0
    Mat R = matPow(T, n);
    cout << (R[0][0] + R[0][1] + R[0][2]) % MOD;  // bắt đầu từ xâu rỗng (trạng thái 0)
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, e):
    k = len(M)
    R = [[int(i == j) for j in range(k)] for i in range(k)]
    while e > 0:
        if e & 1:
            R = mul(R, M)
        M = mul(M, M)
        e >>= 1
    return R

n = int(input())
T = [[1, 1, 0],                                   # T[s][s'] = số cách thêm 1 ký tự đưa s sang s'
     [1, 0, 1],
     [1, 0, 0]]
print(sum(mat_pow(T, n)[0]) % MOD)
Bài 5Truy hồi kèm số hạng nKhá

Dãy $f$ cho bởi $f(0)$, $f(1)$ và $f(n) = a \cdot f(n-1) + b \cdot f(n-2) + n$ với $n \ge 2$. Cho $a, b, f(0), f(1)$ ($0 \le$ mỗi số $\le 10^9$) và $n$ ($0 \le n \le 10^{18}$), tính $f(n) \bmod (10^9 + 7)$.

Input
1 1 0 1 5
Output
26

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Thêm $n$ và hằng số 1 vào trạng thái: $v(i) = (f(i), f(i-1), i+1, 1)$. Bốn hàng của $T$: $f(i+1) = a f(i) + b f(i-1) + (i+1)$; $f(i)$ chép xuống; $(i+2) = (i+1) + 1$; $1 = 1$. Tức là $T = \begin{pmatrix}a & b & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 1 \\ 0 & 0 & 0 & 1\end{pmatrix}$, $v(1) = (f(1), f(0), 2, 1)$ và $v(n) = T^{\,n-1} v(1)$. Ví dụ: $f(2) = 1 + 0 + 2 = 3$, $f(3) = 3 + 1 + 3 = 7$, $f(4) = 7 + 3 + 4 = 14$, $f(5) = 14 + 7 + 5 = 26$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;

Mat mul(const Mat &A, const Mat &B){
    int k = A.size();
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

Mat matPow(Mat M, long long e){
    int k = M.size();
    Mat R(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) R[i][i] = 1;
    while(e > 0){
        if(e & 1) R = mul(R, M);
        M = mul(M, M);
        e >>= 1;
    }
    return R;
}

int main(){
    long long a, b, f0, f1, n;
    cin >> a >> b >> f0 >> f1 >> n;
    if(n == 0){ cout << f0 % MOD; return 0; }
    // trạng thái (f(i), f(i-1), i+1, 1)
    Mat T = {{a % MOD, b % MOD, 1, 0},            // f(i+1) = a f(i) + b f(i-1) + (i+1)
             {1, 0, 0, 0},                        // f(i)
             {0, 0, 1, 1},                        // (i+2) = (i+1) + 1
             {0, 0, 0, 1}};                       // 1 giữ nguyên
    Mat R = matPow(T, n - 1);                     // v(n) = T^(n-1) . v(1)
    long long v[4] = {f1 % MOD, f0 % MOD, 2, 1};
    long long ans = 0;
    for(int j = 0; j < 4; j++) ans = (ans + R[0][j] * v[j]) % MOD;
    cout << ans;
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, e):
    k = len(M)
    R = [[int(i == j) for j in range(k)] for i in range(k)]
    while e > 0:
        if e & 1:
            R = mul(R, M)
        M = mul(M, M)
        e >>= 1
    return R

a, b, f0, f1, n = map(int, input().split())
if n == 0:
    print(f0 % MOD)
else:
    T = [[a % MOD, b % MOD, 1, 0],                # trạng thái (f(i), f(i-1), i+1, 1)
         [1, 0, 0, 0],
         [0, 0, 1, 1],
         [0, 0, 0, 1]]
    R = mat_pow(T, n - 1)
    v = [f1 % MOD, f0 % MOD, 2, 1]
    print(sum(R[0][j] * v[j] for j in range(4)) % MOD)
Bài 6Đường đi độ dài không quá kKhá

Cho đồ thị có hướng $n$ đỉnh ($2 \le n \le 30$), $m$ cạnh ($m \le n^2$, không có hai cạnh trùng nhau, có thể có khuyên) và số $k$ ($1 \le k \le 10^{18}$). Đếm số đường đi từ đỉnh 1 tới đỉnh $n$ có độ dài từ 1 tới $k$ (được lặp đỉnh, lặp cạnh). In theo modulo $10^9 + 7$. Dòng đầu là $n, m, k$, $m$ dòng sau mỗi dòng một cạnh $u \; v$.

Input
3 4 4
1 2
2 3
3 1
1 3
Output
5

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Cần $\sum_{j=1}^{k} A^j[1][n]$, nhưng không thể cộng $10^{18}$ ma trận. Mẹo thêm đỉnh “hố” $h$: thêm cạnh $n \to h$ và khuyên $h \to h$. Một đường đi độ dài $j \le k$ tới $n$, nối thêm cạnh $n \to h$ rồi đứng chờ ở $h$ thêm $k - j$ bước, thành đúng một đường đi độ dài $k + 1$ tới $h$, và ngược lại. Vậy đáp án là $A'^{\,k+1}[1][h]$ với ma trận $(n+1) \times (n+1)$. Năm đường đi trong ví dụ: 1→3; 1→2→3; 1→3→1→3; 1→2→3→1→3; 1→3→1→2→3.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;

Mat mul(const Mat &A, const Mat &B){
    int k = A.size();
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

Mat matPow(Mat M, long long e){
    int k = M.size();
    Mat R(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) R[i][i] = 1;
    while(e > 0){
        if(e & 1) R = mul(R, M);
        M = mul(M, M);
        e >>= 1;
    }
    return R;
}

int main(){
    int n, m;
    long long k;
    cin >> n >> m >> k;
    Mat A(n + 1, vector<long long>(n + 1, 0));    // đỉnh 0..n-1 là đỉnh thật, đỉnh n là "hố"
    for(int i = 0; i < m; i++){
        int u, v;
        cin >> u >> v;
        A[u - 1][v - 1]++;
    }
    A[n - 1][n] = 1;                              // từ đích rơi vào hố
    A[n][n] = 1;                                  // hố tự lặp: đứng chờ cho đủ k + 1 bước
    Mat R = matPow(A, k + 1);
    cout << R[0][n];
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, e):
    k = len(M)
    R = [[int(i == j) for j in range(k)] for i in range(k)]
    while e > 0:
        if e & 1:
            R = mul(R, M)
        M = mul(M, M)
        e >>= 1
    return R

data = sys.stdin.read().split()
n, m, k = int(data[0]), int(data[1]), int(data[2])
A = [[0] * (n + 1) for _ in range(n + 1)]         # đỉnh n (đánh số từ 0) là "hố"
for i in range(m):
    u, v = int(data[3 + 2 * i]), int(data[4 + 2 * i])
    A[u - 1][v - 1] += 1
A[n - 1][n] = 1                                   # từ đích rơi vào hố
A[n][n] = 1                                       # hố tự lặp
print(mat_pow(A, k + 1)[0][n])
Bài 7Lát gạch 3 × nKhá

Có bao nhiêu cách lát kín một nền hình chữ nhật $3 \times n$ ($0 \le n \le 10^{18}$) bằng các viên gạch $1 \times 2$ (được xoay dọc hoặc ngang)? In theo modulo $10^9 + 7$.

Input
4
Output
11

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Nền có $3n$ ô, $n$ lẻ thì không lát được. Với $n = 2m$, gọi $g(m)$ là số cách; người ta chứng minh được (bằng QHĐ trạng thái theo cột) rằng $g(m) = 4g(m-1) - g(m-2)$ với $g(0) = 1$, $g(1) = 3$: 1, 3, 11, 41, 153… Ma trận có hệ số âm: $\begin{pmatrix}4 & -1 \\ 1 & 0\end{pmatrix}$, và $-1$ phải viết thành $10^9 + 6$ trước khi nhân, nếu không phép % trong C++ cho ra số âm.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;

Mat mul(const Mat &A, const Mat &B){
    int k = A.size();
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

Mat matPow(Mat M, long long e){
    int k = M.size();
    Mat R(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) R[i][i] = 1;
    while(e > 0){
        if(e & 1) R = mul(R, M);
        M = mul(M, M);
        e >>= 1;
    }
    return R;
}

int main(){
    long long n;
    cin >> n;
    if(n % 2 == 1){ cout << 0; return 0; }
    long long m = n / 2;                          // g(m) = 4 g(m-1) - g(m-2), g(0) = 1, g(1) = 3
    if(m == 0){ cout << 1; return 0; }
    Mat T = {{4, MOD - 1},                        // -1 viết thành MOD - 1
             {1, 0}};
    Mat R = matPow(T, m - 1);                     // v(m) = T^(m-1) . (g(1), g(0)) = T^(m-1) . (3, 1)
    cout << (R[0][0] * 3 + R[0][1]) % MOD;
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, e):
    k = len(M)
    R = [[int(i == j) for j in range(k)] for i in range(k)]
    while e > 0:
        if e & 1:
            R = mul(R, M)
        M = mul(M, M)
        e >>= 1
    return R

n = int(input())
if n % 2 == 1:
    print(0)
elif n == 0:
    print(1)
else:
    T = [[4, MOD - 1], [1, 0]]                    # g(m) = 4 g(m-1) - g(m-2)
    R = mat_pow(T, n // 2 - 1)
    print((R[0][0] * 3 + R[0][1]) % MOD)          # (g(1), g(0)) = (3, 1)
Bài 8Xâu không chứa mẫuKhó

Xét các xâu độ dài $n$ ($1 \le n \le 10^{18}$) chỉ gồm $\sigma$ chữ cái thường đầu tiên của bảng chữ cái ($1 \le \sigma \le 26$). Cho xâu mẫu $P$ ($1 \le |P| \le 10$, chỉ gồm các chữ cái đó). Đếm số xâu không chứa $P$ như một đoạn con liên tiếp, theo modulo $10^9 + 7$. Dữ liệu: dòng 1 là $n$ và $\sigma$, dòng 2 là $P$.

Input
3 3
ab
Output
21

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Tổng quát hoá Hình 2: trạng thái $j$ = độ dài tiền tố dài nhất của $P$ đang khớp ở cuối xâu ($0 \le j < |P|$). Từ trạng thái $j$, thêm chữ $c$: xét xâu $P[0..j-1] + c$, tìm hậu tố dài nhất của nó cũng là tiền tố của $P$ (vì $|P| \le 10$ nên thử trực tiếp mọi độ dài, không cần KMP). Nếu khớp đủ $|P|$ ký tự thì bỏ (xâu đã chứa $P$). Được ma trận $T$ cỡ $|P| \times |P|$, đáp án là tổng hàng 0 của $T^n$. Ví dụ chính là bài “không chứa AB” ở phần 5 với chữ thường.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;
typedef vector<vector<long long>> Mat;
const long long MOD = 1000000007;

Mat mul(const Mat &A, const Mat &B){
    int k = A.size();
    Mat C(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++)
        for(int t = 0; t < k; t++){
            if(A[i][t] == 0) continue;
            for(int j = 0; j < k; j++)
                C[i][j] = (C[i][j] + A[i][t] * B[t][j]) % MOD;
        }
    return C;
}

Mat matPow(Mat M, long long e){
    int k = M.size();
    Mat R(k, vector<long long>(k, 0));
    for(int i = 0; i < k; i++) R[i][i] = 1;
    while(e > 0){
        if(e & 1) R = mul(R, M);
        M = mul(M, M);
        e >>= 1;
    }
    return R;
}

int main(){
    long long n;
    int sigma;
    string P;
    cin >> n >> sigma >> P;
    int L = P.size();
    // trạng thái j = độ dài tiền tố dài nhất của P đang khớp ở cuối xâu (0..L-1)
    Mat T(L, vector<long long>(L, 0));
    for(int j = 0; j < L; j++)
        for(int c = 0; c < sigma; c++){
            string s = P.substr(0, j) + char('a' + c);
            int nj = min((int)s.size(), L);
            while(nj > 0 && s.substr(s.size() - nj) != P.substr(0, nj)) nj--;   // L <= 10 nên thử hết
            if(nj < L) T[j][nj]++;               // nj == L: vừa xuất hiện P, bỏ
        }
    Mat R = matPow(T, n);
    long long ans = 0;
    for(int j = 0; j < L; j++) ans = (ans + R[0][j]) % MOD;
    cout << ans;
    return 0;
}
import sys
MOD = 10**9 + 7

def mul(A, B):
    cols = list(zip(*B))
    return [[sum(x * y for x, y in zip(row, col)) % MOD for col in cols] for row in A]

def mat_pow(M, e):
    k = len(M)
    R = [[int(i == j) for j in range(k)] for i in range(k)]
    while e > 0:
        if e & 1:
            R = mul(R, M)
        M = mul(M, M)
        e >>= 1
    return R

data = sys.stdin.read().split()
n, sigma, P = int(data[0]), int(data[1]), data[2]
L = len(P)
T = [[0] * L for _ in range(L)]
for j in range(L):                                # j = độ dài tiền tố của P đang khớp ở cuối
    for c in range(sigma):
        s = P[:j] + chr(97 + c)
        nj = min(len(s), L)
        while nj > 0 and s[len(s) - nj:] != P[:nj]:
            nj -= 1
        if nj < L:                                # nj == L: vừa xuất hiện P, bỏ
            T[j][nj] += 1
print(sum(mat_pow(T, n)[0]) % MOD)
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét