Chương 1·Bài 18

Matrix Multiplication

Cho n≤1018n \le 10^{18}, hãy tính FnF_n 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}. Vòng lặp quen thuộc làm đúng nn phép cộng; với 10910^9 phép cộng mỗi giây, n=1018n = 10^{18} mất khoảng ba mươi hai năm. Không có đường tắt hiển nhiên, vì mỗi số chỉ biết hai số liền trước.

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 phép biến đổi ấy 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 các bài đếm đường đi dài 10910^9 cạnh trên đồ thị cỡ một trăm đỉnh, và cho các quy hoạch động theo lớp có ít trạng thái mà cách chuyển trạng thái giống hệt nhau ở mọi bước.

Một bước truy hồi là một phép nhân 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}.

Ô 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 n khác.

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;
using 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

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

Truy hồi tuyến tính 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.

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 ô.

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;
using 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.

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;
using 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

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.

Thêm tọa độ phụ: hằng số, tổng và chỉ số

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;
using 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

Khoảng 60 lần nhân ma trận 2×22 \times 2.

Đếm đường đi và quy hoạch động bằng 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.

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ó.

Đừ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;
using 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

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 cộng và phép nhâ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;
using 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;
using 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

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

  • Nhận ra bài: 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 một trạng thái nhỏ. Viết rõ từng 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ơ trực tiếp, nhiều truy vấn cùng ma trận thì dựng sẵn các M2bM^{2^b}.
  • Trước khi nộp, so kết quả với vòng lặp ngây thơ cho n≤20n \le 20: sai thứ tự tọa độ trong vectơ 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, và lấy modulo sau mỗi phép nhân.

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

1
Tâm Trạng Sau N Ngàycanuc80k1801Hai trạng thái vui, buồn với ma trận chuyển 2×22 \times 2 số thực, 1−p1 - p trên đường chéo; cũng có công thức đóng 1+(1−2p)n2\frac{1 + (1 - 2p)^n}{2} để đối chiếu.
LQDOJ
2
Lũy Thừa Ma Trậncanuc80k1810Chính là mẫu mul và power của bài; cỡ tới 50, in cả ma trận AnA^n.
LQDOJ
3
Lát Bảng Hai Hàngcanuc80k1815Xét cột ngoài cùng: một domino dọc hoặc hai domino ngang, ra truy hồi Fibonacci lệch chỉ số.
LQDOJ
4
Tổng Điểm Xúc Xắccanuc80k1804f(s)=f(s−1)+⋯+f(s−6)f(s) = f(s-1) + \dots + f(s-6): ma trận 6×66 \times 6 dạng cửa sổ; bắt đầu từ f(0)=1f(0) = 1 và f(s)=0f(s) = 0 với s<0s < 0.
LQDOJ
5
Số Hạng Với Hằng Sốcanuc80k1807Trạng thái (fi,fi−1,fi−2,1)(f_i, f_{i-1}, f_{i-2}, 1) chứa được phần +c+c; để ý các nn nhỏ và modulo 104+710^4 + 7.
LQDOJ
6
Tổng Cấp Số Nhâncanuc80k1805St+1=A St+1S_{t+1} = A\, S_t + 1 là phép affine; modulo MM bất kỳ nên không được chia cho A−1A - 1.
LQDOJ
7
Tổng Fibonacci Trên Đoạncanuc80k1806Thêm tọa độ tổng tiền tố vào trạng thái hoặc dùng đẳng thức F0+⋯+Ft=Ft+2−1F_0 + \dots + F_t = F_{t+2} - 1; đoạn là hiệu hai tiền tố.
LQDOJ
8
Đường Đi Của Quân Mãcanuc80k182064 ô là 64 đỉnh, cạnh là nước mã; “không quá kk nước” cần thêm một đỉnh hút có khuyên như mục đếm đường đi; modulo 2322^{32} thì dùng unsigned cho tự tràn.
LQDOJ
9
Dãy XOR Hợp Lệcanuc80k1823Mỗi lần xuất hiện là một đỉnh, có cạnh u→vu \to v khi popcount của XOR chia hết cho 3; dãy độ dài kk là đường đi k−1k - 1 cạnh.
LQDOJ
10
Ghép Các Khối Chữ Sốcanuc80k1826Đỉnh là phần dư theo xx; thêm chữ số dd đổi rr thành (10r+d) mod x(10r + d) \bmod x, cạnh mang trọng số bằng số lần dd xuất hiện.
LQDOJ
11
Đường Đi Trên Ba Làncanuc80k1838Giữa hai mốc chướng ngại ma trận chuyển 3×33 \times 3 không đổi; nâng lũy thừa theo độ dài từng khúc rồi nhân nối tiếp.
LQDOJ
12
Tổng Đoạn Của Truy Hồicanuc80k1817Thêm tọa độ tổng tiền tố vào truy hồi bậc kk; đáp án là S(n)−S(m−1)S(n) - S(m-1) theo modulo pp bất kỳ.
LQDOJ
13
Truy Hồi Tích Có Hệ Sốcanuc80k1836Đặt gx=cxfxg_x = c^x f_x để chỉ còn tích; số mũ của từng giá trị đầu theo truy hồi cộng nên tính theo modulo p−1p - 1; cuối cùng chia lại cho cnc^n.
LQDOJ
14
Làm Mượt Đồ Thịcanuc80k1846Kỳ vọng là tuyến tính nên Et+1=TEtE_{t+1} = T E_t với TT là trung bình các ma trận của từng cạnh; chia bằng nghịch đảo modulo.
LQDOJ
15
Fibonacci Trên Đoạncanuc80k1845Mỗi aia_i ứng với vectơ (Fai+1,Fai)(F_{a_i+1}, F_{a_i}); cộng xx là nhân với QxQ^x; cây phân đoạn có cập nhật lười lưu tổng vectơ và ma trận chờ nhân.
LQDOJ

Đọc thêm

  • Nhân ma trậnVNOI WikiBài tiếng Việt: định nghĩa, lũy thừa ma trận, bài lát gạch 2×N2 \times N và truy hồi tổng quát (SPOJ SEQ).
  • Matrix ExponentiationUSACO GuideChứng minh QnQ^n bằng quy nạp, ma trận bậc kk và danh sách bài CSES, Codeforces xếp từ dễ tới khó.
  • Binary Exponentiationcp-algorithmsPhần ứng dụng: Fibonacci, đếm đường đi độ dài kk, đường đi ngắn nhất đúng kk cạnh bằng (min⁡,+)(\min, +), biến đổi affine.
  • Fibonacci Numberscp-algorithmsFast doubling, đẳng thức Cassini và tính tuần hoàn của Fibonacci theo modulo.
  • 矩阵OI WikiMục ứng dụng: tăng tốc truy hồi, đếm đường đi, (min⁡,+)(\min, +), thứ tự vòng lặp ii-kk-jj và bớt phép lấy modulo.
  • Competitive Programmer’s Handbook, chương 23Truy hồi tuyến tính, đếm đường đi và đường đi ngắn nhất bằng ma trận, viết rất gọn.