Chương 1·Bài 18

Matrix Multiplication

Tính số Fibonacci thứ nn theo modulo 109+710^9 + 7, với F0=0F_0 = 0, F1=1F_1 = 1 và Fi=Fi−1+Fi−2F_i = F_{i-1} + F_{i-2}. Nếu n≤107n \le 10^7 thì vòng lặp là đủ. Nhưng nn có thể là 101810^{18}: với 10910^9 phép cộng mỗi giây, vòng lặp mất khoảng ba mươi hai năm.

Lối ra nằm ở chỗ bước từ cặp (Fi+1,Fi)(F_{i+1}, F_i) sang cặp (Fi+2,Fi+1)(F_{i+2}, F_{i+1}) luôn là cùng một phép biến đổi tuyến tính. Gói nó vào một ma trận QQ thì đi nn bước là nhân với QnQ^n, và QnQ^n tính được bằng bình phương liên tiếp trong O(log⁡n)O(\log n) phép nhân ma trận. Khuôn này dùng cho mọi truy hồi tuyến tính, cho bài đếm đường đi dài 10910^9 cạnh trên đồ thị cỡ một trăm đỉnh, và cho quy hoạch động theo lớp có ít trạng thái mà cách chuyển giống hệt nhau ở mọi bước.

Truy hồi là ma trận

Gom hai số liền nhau thành một cột hai số, gọi là vectơ trạng thái, wi=(Fi+1Fi)w_i = \begin{pmatrix} F_{i+1} \\ F_i \end{pmatrix}. Một bước truy hồi cho Fi+2=1⋅Fi+1+1⋅FiF_{i+2} = 1 \cdot F_{i+1} + 1 \cdot F_i và Fi+1=1⋅Fi+1+0⋅FiF_{i+1} = 1 \cdot F_{i+1} + 0 \cdot F_i: mỗi tọa độ mới là một tổ hợp tuyến tính của các tọa độ cũ, tức tổng của các tọa độ cũ, mỗi tọa độ nhân với một hệ số cố định. Viết hệ số của từng tổ hợp thành một hàng:

Q=(1110),wi+1=Q wi.Q = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}, \qquad w_{i+1} = Q\, w_i .

Sau nn bước wn=Qnw0w_n = Q^n w_0. Chỗ lợi nằm ở một quan sát: hai bước liên tiếp cũng là một phép biến đổi cố định, gói được thành Q⋅Q=Q2Q \cdot Q = Q^2; gói được Q2Q^2 thì gói tiếp Q4Q^4, Q8Q^8, và để tới QnQ^n chỉ cần chừng log⁡2n\log_2 n lần gói.

Với hai ma trận vuông cỡ k×kk \times k, tích C=ABC = AB có Cij=∑tAitBtjC_{ij} = \sum_{t} A_{it} B_{tj}: ô (i,j)(i, j) là hàng ii của AA nhân từng ô với cột jj của BB rồi cộng lại. Định nghĩa này được chọn để (AB) v=A (B v)(AB)\,v = A\,(B\,v). Ba tính chất cần dùng: có ma trận đơn vị II (đường chéo bằng 1, các ô khác bằng 0), chính là A0A^0; kết hợp, (AB)C=A(BC)(AB)C = A(BC), nên lũy thừa bằng bình phương chạy đúng như với số; và không giao hoán, ABAB thường khác BABA. Các lũy thừa của cùng một ma trận thì giao hoán, nhưng khi nhân nhiều ma trận khác nhau, thứ tự là một phần của đáp án.

Viết nn trong hệ nhị phân: AnA^n là tích của các A2bA^{2^b} ứng với những bit bb bằng 1, và mỗi A2b+1A^{2^{b+1}} có từ A2bA^{2^b} bằng một lần bình phương. Với n=10=10102n = 10 = 1010_2: Q2=(2111)Q^2 = \begin{pmatrix} 2 & 1 \\ 1 & 1 \end{pmatrix}, Q4=(5332)Q^4 = \begin{pmatrix} 5 & 3 \\ 3 & 2 \end{pmatrix}, Q8=(34212113)Q^8 = \begin{pmatrix} 34 & 21 \\ 21 & 13 \end{pmatrix}, rồi Q10=Q8Q2=(89555534)Q^{10} = Q^8 Q^2 = \begin{pmatrix} 89 & 55 \\ 55 & 34 \end{pmatrix}.

Trong hình, ô cam là lúc res được nhân thêm; base bình phương sau mỗi bit. Thử ma trận khác hoặc nn khác.

Lũy thừa ma trận bằng bình phương liên tiếp.

Số 55 nằm ngoài đường chéo không phải ngẫu nhiên: quy nạp cho Qn=(Fn+1FnFnFn−1)Q^n = \begin{pmatrix} F_{n+1} & F_n \\ F_n & F_{n-1} \end{pmatrix} với mọi n≥1n \ge 1, vì nhân (abcd)\begin{pmatrix} a & b \\ c & d \end{pmatrix} với QQ bên phải cho (a+bac+dc)\begin{pmatrix} a + b & a \\ c + d & c \end{pmatrix}. Với n=0n = 0, Q0=IQ^0 = I có ô ngoài đường chéo bằng 0=F00 = F_0.

Mỗi phép nhân hai ma trận k×kk \times k tốn k3k^3 phép nhân-cộng; lũy thừa số mũ nn làm khoảng log⁡2n\log_2 n lần bình phương và tối đa chừng ấy lần nhân vào kết quả, tổng O(k3log⁡n)O(k^3 \log n). Về số học: mọi ô nằm trong [0,109+6][0, 10^9 + 6], nên tích hai ô nhỏ hơn 2602^{60} và lấy % MOD ngay sau mỗi phép nhân-cộng là an toàn. Modulo lớn hơn cỡ 3⋅1093 \cdot 10^9 thì cần __int128.

Hỏi nhanh

Với Q=(1110)Q = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}, ma trận Q2=Q⋅QQ^2 = Q \cdot Q bằng gì?

Ví dụChỉ Số Fibonacci

Đề. Cho 0≤n≤10180 \le n \le 10^{18}, in FnF_n theo modulo 109+710^9 + 7.

Lời giải. Như đã thấy, Qn=(Fn+1FnFnFn−1)Q^n = \begin{pmatrix} F_{n+1} & F_n \\ F_n & F_{n-1} \end{pmatrix} với Q=(1110)Q = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}, nên chỉ cần lũy thừa QQ lên nn rồi đọc ô ngoài đường chéo.

Bước 1: nhân hai ma trận theo modulo. Ba vòng lặp, mỗi tích lấy dư ngay; hai số dưới 109+710^9 + 7 nhân với nhau còn vừa ll.

C++
mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++)
            for (ll j = 0; j < k; j++) c[i][j] = (c[i][j] + a[i][t] * b[t][j]) % MOD;
    return c;
}

Bước 2: lũy thừa nhanh. Kết quả bắt đầu bằng ma trận đơn vị. Duyệt các bit của ee từ thấp lên cao: a lần lượt là A1,A2,A4,…A^1, A^2, A^4, \dots, bit nào bằng 1 thì nhân a vào kết quả. Đây là lũy thừa nhanh của bài 08, áp cho ma trận.

C++
mat power(mat a, ll e) {
    ll k = a.size();
    mat r(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++) r[i][i] = 1;
    for (; e > 0; e >>= 1) {
        if (e & 1) r = mul(r, a);
        a = mul(a, a);
    }
    return r;
}

Bước 3: đọc đáp án. Ô [1][0] của QnQ^n là FnF_n. Với n=0n = 0 kết quả là ma trận đơn vị và ô đó cho đúng 0.

C++
mat q = {{1, 1}, {1, 0}};
cout << power(q, n)[1][0] << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using mat = vector<vector<ll>>;

const ll MOD = 1e9 + 7;

mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++)
            for (ll j = 0; j < k; j++) c[i][j] = (c[i][j] + a[i][t] * b[t][j]) % MOD;
    return c;
}

mat power(mat a, ll e) {
    ll k = a.size();
    mat r(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++) r[i][i] = 1;
    for (; e > 0; e >>= 1) {
        if (e & 1) r = mul(r, a);
        a = mul(a, a);
    }
    return r;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll n;
    cin >> n;
    mat q = {{1, 1}, {1, 0}};
    cout << power(q, n)[1][0] << '\n';
}
10

Ước lượng phép tính: khoảng 60 lần nhân ma trận 2×22 \times 2, cỡ 10310^3 phép tính.

Truy hồi bậc k

Một truy hồi tuyến tính bậc kk cho kk số hạng đầu a0,…,ak−1a_0, \dots, a_{k-1} và ai=c1ai−1+c2ai−2+⋯+ckai−ka_i = c_1 a_{i-1} + c_2 a_{i-2} + \dots + c_k a_{i-k} với i≥ki \ge k. Lấy trạng thái là cửa sổ gồm kk số hạng liên tiếp, số mới nhất ở trên cùng, wi=(ai+k−1,…,ai)Tw_i = (a_{i+k-1}, \dots, a_{i})^T. Tọa độ đầu của wi+1w_{i+1} là ai+k=c1ai+k−1+⋯+ckaia_{i+k} = c_1 a_{i+k-1} + \dots + c_k a_i, một tổ hợp với hệ số c1,…,ckc_1, \dots, c_k; mỗi tọa độ còn lại chỉ là tọa độ liền trên của wiw_i dời xuống một ô. Vậy

M=(c1c2⋯ck−1ck10⋯0001⋯00⋮⋱⋮00⋯10),wi+1=M wi,M = \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}, \qquad w_{i+1} = M\, w_i,

và ana_n là tọa độ cuối của Mnw0M^n w_0 với w0=(ak−1,…,a0)Tw_0 = (a_{k-1}, \dots, a_0)^T. Với ai=ai−1+2ai−3a_i = a_{i-1} + 2 a_{i-3} và a0=a1=a2=1a_0 = a_1 = a_2 = 1: (c1,c2,c3)=(1,0,2)(c_1, c_2, c_3) = (1, 0, 2), hệ số bằng 0 vẫn chiếm đúng một ô trong hàng đầu. Một bước cho w1=(3,1,1)Tw_1 = (3, 1, 1)^T, bước nữa cho w2=(5,3,1)Tw_2 = (5, 3, 1)^T; đó là a3=3a_3 = 3 và a4=5a_4 = 5.

Trong hình, hàng đầu (cam) tính số hạng mới, các hàng dưới dời cửa sổ xuống một ô.

Ma trận đồng hành của truy hồi.

Chương trình tổng quát đọc kk, các hệ số c1,…,ckc_1, \dots, c_k, các số hạng đầu a0,…,ak−1a_0, \dots, a_{k-1} rồi nn; tọa độ cuối của Mnw0M^n w_0 là tích của hàng cuối của MnM^n với w0w_0.

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using mat = vector<vector<ll>>;

const ll MOD = 1e9 + 7;

mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++)
            for (ll j = 0; j < k; j++) c[i][j] = (c[i][j] + a[i][t] * b[t][j]) % MOD;
    return c;
}

mat power(mat a, ll e) {
    ll k = a.size();
    mat r(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++) r[i][i] = 1;
    for (; e > 0; e >>= 1) {
        if (e & 1) r = mul(r, a);
        a = mul(a, a);
    }
    return r;
}

ll k, n, c[105], a[105];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> k;
    for (ll i = 0; i < k; i++) cin >> c[i];
    for (ll i = 0; i < k; i++) cin >> a[i];
    cin >> n;
    mat m(k, vector<ll>(k, 0));
    for (ll j = 0; j < k; j++) m[0][j] = c[j] % MOD;
    for (ll i = 1; i < k; i++) m[i][i - 1] = 1;
    mat p = power(m, n);
    ll ans = 0;
    for (ll j = 0; j < k; j++) ans = (ans + p[k - 1][j] * (a[k - 1 - j] % MOD)) % MOD;
    cout << ans << '\n';
}
3
1 0 2
1 1 1
6

Dãy là 1,1,1,3,5,7,13,…1, 1, 1, 3, 5, 7, 13, \dots nên a6=13a_6 = 13. Chi phí O(k3log⁡n)O(k^3 \log n): với n=1018n = 10^{18} và k=10k = 10 chỉ khoảng 1,2⋅1051{,}2 \cdot 10^5 phép, với k=100k = 100 khoảng 1,2⋅1081{,}2 \cdot 10^8, còn k=300k = 300 đã cỡ 3⋅1093 \cdot 10^9. Khi nn chỉ cỡ 10710^7 mà kk nhỏ, cứ tính từng số hạng bằng O(nk)O(nk) cho gọn. Chỗ hay sai nhất là thứ tự: hàng đầu của MM phải khớp với thứ tự các tọa độ của wiw_i, và các hệ số bằng 0 không được “nén” khỏi hàng.

Hỏi nhanh

Truy hồi ai=3ai−1+4ai−3a_i = 3 a_{i-1} + 4 a_{i-3} có k=3k = 3, trạng thái wi=(ai+2,ai+1,ai)Tw_i = (a_{i+2}, a_{i+1}, a_i)^T. Hàng đầu của ma trận chuyển là gì?

Chạy nhanh hơn. Chi phí tăng theo lập phương của kk, nên việc đầu tiên là giữ kk nhỏ. Ngoài ra có mấy cách tiết kiệm. Thường ta chỉ cần MnwM^n w cho một vectơ ww, không cần cả MnM^n: vì các M2bM^{2^b} giao hoán với nhau, nhân dần từng M2bM^{2^b} vào vectơ, mỗi lần O(k2)O(k^2) thay vì O(k3)O(k^3), và số phép nhân ma trận với ma trận chỉ còn các lần bình phương. Khi có nhiều truy vấn trên cùng một ma trận, dựng sẵn các M2bM^{2^b} một lần rồi mỗi truy vấn tốn O(k2log⁡n)O(k^2 \log n). Ma trận chuyển thường rất thưa; một dòng if (!a[i][t]) continue; trong vòng lặp ngoài của phép nhân bỏ qua cả vòng trong. Riêng Fibonacci còn có fast doubling: F2m+1=Fm+12+Fm2F_{2m+1} = F_{m+1}^2 + F_m^2 và F2m=Fm(2Fm+1−Fm)F_{2m} = F_m(2F_{m+1} - F_m), chỉ theo dõi một cặp số. Khi kk lên vài trăm, người ta tính xnx^n modulo đa thức đặc trưng (kỹ thuật Kitamasa) với O(k2log⁡n)O(k^2 \log n); đó là chủ đề nâng cao hơn.

Cách viết hàm nhân ma trận cũng quyết định tốc độ:

Nhân hai ma trận 300 × 300 theo modulo, 20 lần, trung vị 5 lần chạy, i7-10850H, g++ -O2.i, j, k, lấy dư mỗi tích1780 ms · gấp 4,2 lầni, k, j, lấy dư mỗi tích870 ms · gấp 2 lầni, k, j, lấy dư muộn426 ms
Nhân hai ma trận 300 × 300 theo modulo, lặp 20 lần, ba cách viết, đo trên i7-10850H, g++ -O2.

Đổi thứ tự vòng lặp từ i,j,ki, j, k sang i,k,ji, k, j nhanh hơn khoảng 2 lần, vì vòng trong đọc liên tiếp từng hàng của cả hai ma trận. Lấy dư muộn (cộng tối đa 8 tích rồi mới % MOD) nhanh thêm khoảng 2 lần nữa, tổng cộng khoảng 4 lần. Quy tắc: chỉ số cuối của mảng nằm ở vòng lặp trong cùng, và % đắt hơn phép cộng nên đừng gọi nó cho từng tích.

Ví dụNhững Viên Ngọc Ma Thuật

Đề. Xếp một hàng gồm số viên ngọc tùy ý, mỗi viên dạng thường dài 1 hoặc dạng mở rộng dài mm (2≤m≤1002 \le m \le 100); thứ tự các dạng được phân biệt. Đếm số cấu hình có tổng độ dài đúng n≤1018n \le 10^{18}, theo modulo 109+710^9 + 7.

Lời giải. Gọi f(i)f(i) là số cấu hình dài ii, f(0)=1f(0) = 1. Viên đầu là dạng thường thì phần còn lại dài i−1i - 1, là dạng mở rộng thì dài i−mi - m; hai trường hợp rời nhau và phủ hết, nên f(i)=f(i−1)+f(i−m)f(i) = f(i - 1) + f(i - m). Đây là truy hồi bậc mm, và f(i)=1f(i) = 1 với 0≤i<m0 \le i < m (chỉ xếp được toàn viên thường). Với n=4n = 4, m=3m = 3: các cấu hình là 1+1+1+11+1+1+1, 1+31+3, 3+13+1.

Bước 1: ma trận dời cửa sổ. Trạng thái là cửa sổ mm giá trị liên tiếp (f(i),f(i−1),…,f(i−m+1))(f(i), f(i - 1), \dots, f(i - m + 1)). Hàng đầu của ma trận tính giá trị mới f(i+1)=f(i)+f(i−m+1)f(i + 1) = f(i) + f(i - m + 1), nên có số 1 ở cột 0 và cột m−1m - 1; các hàng sau dời cửa sổ xuống một ô.

C++
mat t(m, vector<ll>(m, 0));
t[0][0] = t[0][m - 1] = 1;
for (ll i = 1; i < m; i++) t[i][i - 1] = 1;
vector<ll> w(m, 1);

Cửa sổ đầu là (f(m−1),…,f(0))(f(m - 1), \dots, f(0)), toàn số 1.

Bước 2: nhân vectơ thay vì nhân hai ma trận. Ta chỉ cần TnwT^n w, nên giữ vectơ w và duyệt bit của nn: bit 1 thì w = T * w (chỉ m2m^2 phép), luôn bình phương T. Hàm mul bỏ qua các ô 0 của a, giúp ma trận thưa chạy nhanh hơn.

C++
for (; n > 0; n >>= 1) {
    if (n & 1) w = mulvec(t, w);
    t = mul(t, t);
}

Bước 3: đọc đáp án. Sau nn bước, cửa sổ là (f(n+m−1),…,f(n))(f(n + m - 1), \dots, f(n)), nên f(n)f(n) nằm ở tọa độ cuối w[m - 1].

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using mat = vector<vector<ll>>;

const ll MOD = 1e9 + 7;

mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++) {
            if (!a[i][t]) continue;
            for (ll j = 0; j < k; j++) c[i][j] = (c[i][j] + a[i][t] * b[t][j]) % MOD;
        }
    return c;
}

vector<ll> mulvec(mat a, vector<ll> v) {
    ll k = a.size();
    vector<ll> w(k, 0);
    for (ll i = 0; i < k; i++)
        for (ll j = 0; j < k; j++) w[i] = (w[i] + a[i][j] * v[j]) % MOD;
    return w;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll n, m;
    cin >> n >> m;
    mat t(m, vector<ll>(m, 0));
    t[0][0] = t[0][m - 1] = 1;
    for (ll i = 1; i < m; i++) t[i][i - 1] = 1;
    vector<ll> w(m, 1);
    for (; n > 0; n >>= 1) {
        if (n & 1) w = mulvec(t, w);
        t = mul(t, t);
    }
    cout << w[m - 1] << '\n';
}
4 3

Ước lượng phép tính: ma trận 100×100100 \times 100: 60 lần bình phương, mỗi lần 10610^6 phép, khoảng 6⋅1076 \cdot 10^7, vừa một giây; phần nhân vectơ chỉ 6⋅1056 \cdot 10^5.

Tọa độ phụ

Trạng thái không nhất thiết chỉ gồm các số hạng của dãy. Tọa độ nào cũng được đưa vào, miễn giá trị mới của nó tính được từ các tọa độ hiện có bằng một tổ hợp tuyến tính. Ba trường hợp dùng nhiều nhất:

Hằng số. Xét ai=ai−1+ai−2+1a_i = a_{i-1} + a_{i-2} + 1. Số +1+1 không phải là tổ hợp của aa, nên ta thêm một tọa độ luôn bằng 1 với hàng (0,0,1)(0, 0, 1) để nó tự giữ nguyên: trạng thái (ai+1,ai,1)T(a_{i+1}, a_i, 1)^T và ma trận có các hàng (1,1,1)(1, 1, 1), (1,0,0)(1, 0, 0), (0,0,1)(0, 0, 1). Cũng cách đó biến hàm bậc nhất t↦At+Bt \mapsto At + B (biến tt thành At+BAt + B) thành ma trận (AB01)\begin{pmatrix} A & B \\ 0 & 1 \end{pmatrix} tác động lên (t,1)T(t, 1)^T.

Tổng tiền tố. Muốn có si=F0+⋯+Fi−1s_i = F_0 + \dots + F_{i-1}, để ý si+1=si+Fis_{i+1} = s_i + F_i. Thêm sis_i vào trạng thái: (Fi+1,Fi,si)T(F_{i+1}, F_i, s_i)^T với ma trận có các hàng (1,1,0)(1, 1, 0), (1,0,0)(1, 0, 0), (0,1,1)(0, 1, 1). Tổng trên đoạn Fl+⋯+FrF_l + \dots + F_r là sr+1−sls_{r+1} - s_l, đúng ý của tổng tiền tố ở bài 02 nhưng bây giờ rr có thể tới 101810^{18}.

Chỉ số ii và các lũy thừa của nó. Nếu truy hồi có số hạng dạng ii hay i2i^2, thêm các tọa độ ii, i2i^2 và hằng 11: khi ii tăng một, i+1i + 1 và (i+1)2=i2+2i+1(i+1)^2 = i^2 + 2i + 1 đều là tổ hợp tuyến tính của (i2,i,1)(i^2, i, 1).

Điều ta không làm được là các số hạng phi tuyến như ai−1⋅ai−2a_{i-1} \cdot a_{i-2}. Khi truy hồi chỉ gồm các phép nhân, ta chuyển sang theo dõi số mũ: nếu bi=bi−1bi−2b_i = b_{i-1} b_{i-2} thì mỗi bib_i là tích các lũy thừa của b0,b1b_0, b_1 với số mũ theo một truy hồi cộng kiểu Fibonacci.

Hỏi nhanh

Xét ai=ai−1+ai−2+ia_i = a_{i-1} + a_{i-2} + i. Trạng thái đã có ai+1a_{i+1} và aia_i. Để một ma trận cố định tính được ai+2a_{i+2} ở mọi bước, cần thêm những tọa độ nào?

Ví dụLặp Hàm Tuyến Tính

Đề. Cho f(t)=At+Bf(t) = At + B với 1≤A,B,x≤1091 \le A, B, x \le 10^9. Bắt đầu từ xx, áp ff đúng n≤1018n \le 10^{18} lần; in kết quả theo modulo 109+710^9 + 7.

Lời giải. Thêm tọa độ hằng 1: trạng thái (t,1)T(t, 1)^T và một lần áp ff là nhân với (AB01)\begin{pmatrix} A & B \\ 0 & 1 \end{pmatrix}, vì At+B⋅1At + B \cdot 1 là tọa độ trên và 11 tự giữ nguyên. Áp nn lần là nhân với ma trận ấy mũ nn. Có công thức đóng Anx+BAn−1A−1A^n x + B \frac{A^n - 1}{A - 1} nhưng nó phải chia, và vỡ khi A≡1A \equiv 1; ma trận thì không phải bận tâm. Với ví dụ 3→11→27→59→1233 \to 11 \to 27 \to 59 \to 123.

Bước 1: nhân và lũy thừa ma trận. Hai hàm mul và power giống hệt ví dụ Fibonacci.

Bước 2: dựng ma trận rồi đọc tọa độ trên. Lấy AA, BB, xx theo modulo trước khi nhân để tích không tràn. Gọi RR là ma trận mũ nn, thì R⋅(x,1)TR \cdot (x, 1)^T có tọa độ trên R00x+R01R_{00} x + R_{01}.

C++
mat r = power({{A % MOD, B % MOD}, {0, 1}}, n);
cout << (r[0][0] * (x % MOD) + r[0][1]) % MOD << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using mat = vector<vector<ll>>;

const ll MOD = 1e9 + 7;

mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++)
            for (ll j = 0; j < k; j++) c[i][j] = (c[i][j] + a[i][t] * b[t][j]) % MOD;
    return c;
}

mat power(mat a, ll e) {
    ll k = a.size();
    mat r(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++) r[i][i] = 1;
    for (; e > 0; e >>= 1) {
        if (e & 1) r = mul(r, a);
        a = mul(a, a);
    }
    return r;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll A, B, n, x;
    cin >> A >> B >> n >> x;
    mat r = power({{A % MOD, B % MOD}, {0, 1}}, n);
    cout << (r[0][0] * (x % MOD) + r[0][1]) % MOD << '\n';
}
2 5 4 3

Ước lượng phép tính: khoảng 60 lần nhân ma trận 2×22 \times 2.

Ma trận kề

Đồ thị có hướng gồm các đỉnh (vẽ thành chấm tròn, đánh số 1..n1..n) và các cạnh (mũi tên đi từ một đỉnh tới một đỉnh); chương 3 học kỹ về đồ thị, ở đây chỉ cần chừng đó. Xét đồ thị có hướng với nn đỉnh. Ma trận kề AA có AuvA_{uv} bằng số cạnh từ uu tới vv. Một đường đi (walk) có kk cạnh là dãy đỉnh u0,u1,…,uku_0, u_1, \dots, u_k mà mỗi uj−1→uju_{j-1} \to u_j là một cạnh; đỉnh và cạnh được lặp lại. Điều cần nhớ: (Ak)uv(A^k)_{uv} là số đường đi đúng kk cạnh từ uu tới vv. Quy nạp theo kk: phân loại đường đi k+1k + 1 cạnh theo đỉnh xx ngay trước đỉnh cuối, phần đầu có (Ak)ux(A^k)_{ux} cách, rồi cạnh x→vx \to v có AxvA_{xv} cách; cộng theo xx ra đúng công thức nhân ma trận.

Trong hình, ô hàng 1, cột 4 của A2A^2 bằng A12A24+A13A34=2A_{12} A_{24} + A_{13} A_{34} = 2, ứng với hai đường đi 1→2→41 \to 2 \to 4 và 1→3→41 \to 3 \to 4.

Hình dưới cho gõ danh sách cạnh để dựng đồ thị; bấm một ô của ma trận để xem các đường đi ứng với nó.

Đếm đường đi bằng lũy thừa ma trận kề.

Đừng nhầm đường đi này với đường đi đơn: nó cho đi lại đỉnh cũ, nên một chu trình chỉ có ba đỉnh vẫn có đường đi dài 100 cạnh. Từ đỉnh ss tới tt: lấy ô (Ak)st(A^k)_{st}. Tổng số đường đi kk cạnh: cộng mọi ô của AkA^k. Đường đi không quá kk cạnh: thêm một đỉnh mới zz có khuyên z→zz \to z và cạnh v→zv \to z từ mọi đỉnh vv; mỗi đường đi j≤kj \le k cạnh khớp với đúng một đường đi k+1k + 1 cạnh kết thúc ở zz.

Hỏi nhanh

Đồ thị có ba đỉnh và ba cạnh 1→21 \to 2, 2→32 \to 3, 3→13 \to 1 (một chu trình). Ô (A7)12(A^7)_{12}, số đường đi đúng 7 cạnh từ 1 tới 2, bằng bao nhiêu?

Quy hoạch động theo lớp. Nhiều bài đếm xâu quy về đếm đường đi trong một đồ thị trạng thái nhỏ, trong đó đỉnh là thông tin cần nhớ về phần xâu đã viết và cạnh là ký tự được thêm vào. Đếm xâu nhị phân độ dài nn không có ba chữ số 1 liên tiếp chỉ cần nhớ số chữ số 1 ở cuối xâu, tức ba trạng thái 0,1,20, 1, 2: thêm 0 đưa mọi trạng thái về 0, thêm 1 đưa ss lên s+1s + 1 nếu s<2s < 2. Với vectơ cột số xâu ở từng trạng thái,

wt+1=(111100010)wt,w0=(1,0,0)T,w_{t+1} = \begin{pmatrix} 1 & 1 & 1 \\ 1 & 0 & 0 \\ 0 & 1 & 0 \end{pmatrix} w_t, \qquad w_0 = (1, 0, 0)^T ,

với cạnh s→s′s \to s' nằm ở hàng s′s', cột ss. Các vectơ w1,…,w4w_1, \dots, w_4 có tổng 2,4,7,132, 4, 7, 13; tám xâu độ dài 3 trừ xâu 111111 đúng bằng 7. Mọi quy hoạch động theo lớp mà bảng chuyển giống nhau ở mọi lớp và chỉ cộng các giá trị của lớp trước đều lũy thừa hóa được như vậy.

Ví dụĐếm Hành Trình Đúng K Cạnh

Đề. Đồ thị có hướng đơn n≤100n \le 100 đỉnh. Đếm tổng số đường đi (walk) đúng k≤109k \le 10^9 cạnh với đầu, cuối tùy ý, theo modulo 109+710^9 + 7.

Lời giải. Ô (u,v)(u, v) của AkA^k là số đường đi kk cạnh từ uu tới vv, nên đáp án là tổng mọi ô của AkA^k. Ta không cần cả ma trận: với 1\mathbf{1} là vectơ toàn số 1, (Ak1)u=∑v(Ak)uv(A^k \mathbf{1})_u = \sum_v (A^k)_{uv} là số đường đi kk cạnh bắt đầu tại uu, và đáp án là tổng các tọa độ. Quy hoạch động theo từng độ dài thì tốn k⋅n2=1013k \cdot n^2 = 10^{13}.

Bước 1: ma trận kề. Cạnh u→vu \to v đặt ở hàng uu, cột vv (đổi về đánh số từ 0). Khi đó (A * w)[u] cộng w[v] qua mọi cạnh u→vu \to v: số đường đi bắt đầu tại uu bằng tổng qua đỉnh kế tiếp.

C++
mat a(n, vector<ll>(n, 0));
while (m--) {
    ll u, v;
    cin >> u >> v;
    a[u - 1][v - 1] = 1;
}

Bước 2: nhân vectơ theo các bit của kk. w bắt đầu là 1\mathbf{1}; bit 1 thì w = a * w, luôn bình phương a. Thứ tự nhân không quan trọng vì mọi lũy thừa của cùng một ma trận giao hoán với nhau.

C++
vector<ll> w(n, 1);
for (; k > 0; k >>= 1) {
    if (k & 1) w = mulvec(a, w);
    a = mul(a, a);
}

Bước 3: cộng các tọa độ.

C++
ll ans = 0;
for (ll x : w) ans = (ans + x) % MOD;

Với ví dụ, từ mỗi đỉnh chỉ có một đường đi 3 cạnh, tổng 2.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using mat = vector<vector<ll>>;

const ll MOD = 1e9 + 7;

mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<ll>(k, 0));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++) {
            if (!a[i][t]) continue;
            for (ll j = 0; j < k; j++) c[i][j] = (c[i][j] + a[i][t] * b[t][j]) % MOD;
        }
    return c;
}

vector<ll> mulvec(mat a, vector<ll> v) {
    ll k = a.size();
    vector<ll> w(k, 0);
    for (ll i = 0; i < k; i++)
        for (ll j = 0; j < k; j++) w[i] = (w[i] + a[i][j] * v[j]) % MOD;
    return w;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll n, m, k;
    cin >> n >> m >> k;
    mat a(n, vector<ll>(n, 0));
    while (m--) {
        ll u, v;
        cin >> u >> v;
        a[u - 1][v - 1] = 1;
    }
    vector<ll> w(n, 1);
    for (; k > 0; k >>= 1) {
        if (k & 1) w = mulvec(a, w);
        a = mul(a, a);
    }
    ll ans = 0;
    for (ll x : w) ans = (ans + x) % MOD;
    cout << ans << '\n';
}
2 2 3
1 2
2 1

Ước lượng phép tính: 30 lần bình phương ma trận 100×100100 \times 100, mỗi lần 10610^6 phép.

Đổi phép toán

Trong chứng minh (AB)C=A(BC)(AB)C = A(BC) ta chỉ dùng: phép cộng giao hoán, kết hợp và có phần tử không; phép nhân kết hợp, có phần tử đơn vị, và phân phối với phép cộng. Cấu trúc nào có đủ các tính chất ấy (nửa vành, semiring) thì phép nhân ma trận và lũy thừa bằng bình phương dùng nguyên được. Ba biến thể hay gặp:

  • (min⁡,+)(\min, +): thay “cộng” bằng min⁡\min và “nhân” bằng ++, Cij=min⁡t(Ait+Btj)C_{ij} = \min_t (A_{it} + B_{tj}). Khi AuvA_{uv} là trọng số cạnh (vô cực nếu không có cạnh), (Ak)uv(A^k)_{uv} là độ dài đường đi ngắn nhất đúng kk cạnh. Cùng khuôn với max⁡\max cho đường đi dài nhất.
  • (∨,∧)(\vee, \wedge): “có hay không” thay cho “bao nhiêu”, để biết vv có tới được từ uu bằng đúng kk cạnh không.
  • (⊕,∧)(\oplus, \wedge): XOR thay cộng, AND thay nhân, cho các truy hồi trên bit.

Hai phần tử đặc biệt đổi theo cấu trúc. Phần tử không của min⁡\min là +∞+\infty, phần tử đơn vị của ++ là 0, nên ma trận đơn vị của (min⁡,+)(\min, +) có 00 trên đường chéo và ∞\infty ở nơi còn lại, không phải ma trận 1/0 quen thuộc. Chương trình sau in độ dài đường đi ngắn nhất đúng kk cạnh từ đỉnh 1 tới đỉnh nn (hoặc −1-1). Bỏ qua các ô ∞\infty trước khi cộng để ∞+5\infty + 5 không bị nhầm thành giá trị hữu hạn.

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using mat = vector<vector<ll>>;

const ll INF = 1e18;

mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<ll>(k, INF));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++) {
            if (a[i][t] == INF) continue;
            for (ll j = 0; j < k; j++)
                if (b[t][j] < INF) c[i][j] = min(c[i][j], a[i][t] + b[t][j]);
        }
    return c;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll n, m, k;
    cin >> n >> m >> k;
    mat a(n, vector<ll>(n, INF));
    while (m--) {
        ll u, v, w;
        cin >> u >> v >> w;
        a[u - 1][v - 1] = min(a[u - 1][v - 1], w);
    }
    mat r(n, vector<ll>(n, INF));
    for (ll i = 0; i < n; i++) r[i][i] = 0;
    for (; k > 0; k >>= 1) {
        if (k & 1) r = mul(r, a);
        a = mul(a, a);
    }
    cout << (r[0][n - 1] == INF ? -1 : r[0][n - 1]) << '\n';
}
4 6 4
1 2 4
1 3 1
3 2 1
2 4 5
3 4 6
2 3 1

Đường đi 1→3→2→3→41 \to 3 \to 2 \to 3 \to 4 có đúng 4 cạnh và độ dài 9; đường 1→3→41 \to 3 \to 4 chỉ dài 7 nhưng có hai cạnh nên không được tính. Cũng vì phép nhân ma trận không giao hoán, khi nhân nhiều ma trận khác nhau (mỗi phần tử của dãy một ma trận, hỏi tích trên một đoạn) ta phải giữ đúng thứ tự; cây phân đoạn của bài 17 làm được việc này.

Hỏi nhanh

Dùng lũy thừa kiểu (min⁡,+)(\min, +) để tìm đường đi ngắn nhất đúng kk cạnh. Ma trận kết quả lúc bắt đầu, tương ứng A0A^0, cần là ma trận nào?

Ví dụTruy Hồi Bit

Đề. Cho K≤100K \le 100 số hạng đầu A1..AKA_1..A_K và KK hệ số C1..CKC_1..C_K, đều là số không dấu 32 bit. Với i>Ki > K, Ai=(C1∧Ai−1)⊕⋯⊕(CK∧Ai−K)A_i = (C_1 \wedge A_{i-1}) \oplus \dots \oplus (C_K \wedge A_{i-K}). In AMA_M với M≤109M \le 10^9.

Lời giải. Đúng truy hồi bậc KK, chỉ đổi cặp phép toán sang (⊕,∧)(\oplus, \wedge): “cộng” là XOR, “nhân” là AND. Lũy thừa ma trận chỉ cần phép cộng giao hoán, kết hợp, có phần tử không, và phép nhân kết hợp, phân phối với phép cộng. XOR và AND thỏa hết, theo từng bit: phần tử không của XOR là 0, phần tử đơn vị của AND là số toàn bit 1 (~0u). Kiểu unsigned 32 bit giữ đúng mọi phép bit.

Bước 1: nhân ma trận với cặp phép mới. Như hàm mul quen thuộc, thay + bằng ^ và * bằng &, không cần modulo.

C++
mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<unsigned>(k, 0));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++) {
            if (!a[i][t]) continue;
            for (ll j = 0; j < k; j++) c[i][j] ^= a[i][t] & b[t][j];
        }
    return c;
}

Bước 2: ma trận dời cửa sổ. Trạng thái là cửa sổ (AK,…,A1)(A_K, \dots, A_1). Hàng đầu chứa các hệ số C1..CKC_1..C_K; các ô “1” dời cửa sổ xuống phải là phần tử đơn vị của AND, tức ~0u, chứ không phải số 1.

C++
mat t(k, vector<unsigned>(k, 0));
for (ll j = 0; j < k; j++) t[0][j] = c[j];
for (ll i = 1; i < k; i++) t[i][i - 1] = ~0u;
vector<unsigned> w(k);
for (ll i = 0; i < k; i++) w[i] = a[k - 1 - i];

Bước 3: đi M−KM - K bước. Nếu M≤KM \le K thì in thẳng AMA_M. Ngược lại nhân vectơ theo các bit của M−KM - K, rồi đọc tọa độ trên w[0].

C++
for (ll e = m - k; e > 0; e >>= 1) {
    if (e & 1) w = mulvec(t, w);
    t = mul(t, t);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using mat = vector<vector<unsigned>>;

mat mul(mat a, mat b) {
    ll k = a.size();
    mat c(k, vector<unsigned>(k, 0));
    for (ll i = 0; i < k; i++)
        for (ll t = 0; t < k; t++) {
            if (!a[i][t]) continue;
            for (ll j = 0; j < k; j++) c[i][j] ^= a[i][t] & b[t][j];
        }
    return c;
}

vector<unsigned> mulvec(mat a, vector<unsigned> v) {
    ll k = a.size();
    vector<unsigned> w(k, 0);
    for (ll i = 0; i < k; i++)
        for (ll j = 0; j < k; j++) w[i] ^= a[i][j] & v[j];
    return w;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll k, m;
    cin >> k >> m;
    vector<unsigned> a(k), c(k);
    for (ll i = 0; i < k; i++) cin >> a[i];
    for (ll i = 0; i < k; i++) cin >> c[i];
    if (m <= k) {
        cout << a[m - 1] << '\n';
        return 0;
    }
    mat t(k, vector<unsigned>(k, 0));
    for (ll j = 0; j < k; j++) t[0][j] = c[j];
    for (ll i = 1; i < k; i++) t[i][i - 1] = ~0u;
    vector<unsigned> w(k);
    for (ll i = 0; i < k; i++) w[i] = a[k - 1 - i];
    for (ll e = m - k; e > 0; e >>= 1) {
        if (e & 1) w = mulvec(t, w);
        t = mul(t, t);
    }
    cout << w[0] << '\n';
}
2 4
1 2
4294967295 4294967295

Ước lượng phép tính: 30 lần bình phương ma trận 100×100100 \times 100, mỗi lần 10610^6 phép bit.

Khi đi thi

  • Số bước nn cỡ 10910^9 đến 101810^{18}, mọi bước áp cùng một công thức tuyến tính lên trạng thái nhỏ: lũy thừa ma trận. Viết rõ mỗi tọa độ mới là tổ hợp của những tọa độ cũ nào; thiếu hằng số, tổng tiền tố hay chỉ số bước thì thêm tọa độ phụ.
  • Ước lượng trước 120⋅k3120 \cdot k^3 phép tính cho n=1018n = 10^{18} (khoảng 1,2⋅1081{,}2 \cdot 10^8 khi k=100k = 100). Chỉ cần MnwM^n w thì nhân vectơ; nhiều truy vấn cùng ma trận thì dựng sẵn các M2bM^{2^b}.
  • So kết quả với vòng lặp ngây thơ cho n≤20n \le 20 trước khi nộp: sai thứ tự tọa độ hay đổi chỗ hàng và cột vẫn chạy ra số sai mà không báo lỗi. Với (min⁡,+)(\min, +) nhớ ma trận đơn vị riêng.

Tóm tắt

  • Truy hồi tuyến tính bậc kk: trạng thái là kk số hạng gần nhất, một bước là nhân với ma trận cố định MM (hàng đầu là hệ số, các hàng sau dời xuống), nên ana_n nằm trong Mnw0M^n w_0.
  • Nhân ma trận O(k3)O(k^3), kết hợp nhưng không giao hoán; lũy thừa bằng bình phương liên tiếp cho MnM^n trong O(k3log⁡n)O(k^3 \log n), khởi tạo bằng ma trận đơn vị và lấy modulo sau mỗi phép nhân.
  • Hằng số, tổng tiền tố, ii và i2i^2 thêm được làm tọa độ phụ, miễn nó tự cập nhật được bằng tổ hợp tuyến tính của các tọa độ hiện có.
  • (Ak)uv(A^k)_{uv} là số đường đi đúng kk cạnh từ uu tới vv; quy hoạch động theo lớp có bảng chuyển cố định là lũy thừa ma trận kề của đồ thị trạng thái.
  • Chỉ cần MnwM^n w thì nhân vectơ thay vì nhân ma trận; đổi cặp phép toán sang (min⁡,+)(\min, +) hay (⊕,∧)(\oplus, \wedge) dùng lại được code, nhưng ma trận đơn vị phải đổi theo.

Bài tập

Đọc thêm