Cho , hãy tính theo modulo , với , và . Vòng lặp quen thuộc làm đúng phép cộng; với phép cộng mỗi giây, 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 sang cặp 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 thì đi bước là nhân với , và tính được bằng bình phương liên tiếp trong 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 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, . Một bước truy hồi cho và : 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:
Sau bước . 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 ; gói được thì gói tiếp , , và để tới chỉ cần chừng lần gói.
Với hai ma trận vuông cỡ , tích có : ô là hàng của nhân từng ô với cột của rồi cộng lại. Định nghĩa này được chọn để . Ba tính chất cần dùng: có ma trận đơn vị (đường chéo bằng 1, các ô khác bằng 0), chính là ; kết hợp, , 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, thường khác . 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 trong hệ nhị phân: là tích của các ứng với những bit bằng 1, và mỗi có từ bằng một lần bình phương. Với : , , , rồi .
Số 55 nằm ngoài đường chéo không phải ngẫu nhiên: quy nạp cho với mọi , vì nhân với bên phải cho . Với , có ô ngoài đường chéo bằng .
Mỗi phép nhân hai ma trận tốn phép nhân-cộng; lũy thừa số mũ làm khoảng lần bình phương và tối đa chừng ấy lần nhân vào kết quả, tổng . Về số học: mọi ô nằm trong , nên tích hai ô nhỏ hơn và lấy % MOD ngay sau mỗi phép nhân-cộng là an toàn. Modulo lớn hơn cỡ thì cần __int128.
Hỏi nhanh
Với , ma trận bằng gì?
Ô là hàng 1 nhân cột 1: ; ô là ; ô là ; ô là .
Đáp án đầu hấp dẫn vì các ô chỉ là 0 hoặc 1, và bình phương từng ô cho lại đúng ma trận ấy; nhưng lũy thừa ma trận không phải lũy thừa từng ô. [[2, 2], [2, 0]] là , nhầm phép nhân thành phép cộng. [[3, 2], [2, 1]] là , nhầm số mũ lên một.
Ví dụChỉ Số Fibonacci
Đề. Cho , in theo modulo .
Lời giải. Như đã thấy, với , nên chỉ cần lũy thừa lên 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 nhân với nhau còn vừa ll.
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 từ thấp lên cao: a lần lượt là , 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.
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 là . Với kết quả là ma trận đơn vị và ô đó cho đúng 0.
mat q = {{1, 1}, {1, 0}};
cout << power(q, n)[1][0] << '\n';
#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
55
Khoảng 60 lần nhân ma trận , cỡ 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 cho số hạng đầu và với . Lấy trạng thái là cửa sổ gồm số hạng liên tiếp, số mới nhất ở trên cùng, . Tọa độ đầu của là , một tổ hợp với hệ số ; mỗi tọa độ còn lại chỉ là tọa độ liền trên của dời xuống một ô. Vậy
và là tọa độ cuối của với . Với và : , hệ số bằng 0 vẫn chiếm đúng một ô trong hàng đầu. Một bước cho , bước nữa cho ; đó là và .
Chương trình tổng quát đọc , các hệ số , các số hạng đầu rồi ; tọa độ cuối của là tích của hàng cuối của với .
#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
13
Dãy là nên . Chi phí : với và chỉ khoảng phép, với khoảng , còn đã cỡ . Khi chỉ cỡ mà nhỏ, cứ tính từng số hạng bằng cho gọn. Chỗ hay sai nhất là thứ tự: hàng đầu của phải khớp với thứ tự các tọa độ của , và các hệ số bằng 0 không được “nén” khỏi hàng.
Hỏi nhanh
Truy hồi có , trạng thái . Hàng đầu của ma trận chuyển là gì?
Số hạng mới là ; các hệ số đứng đúng theo thứ tự các tọa độ của , nên hàng đầu là .
dồn hệ số 4 vào cột của , tức là đã bỏ ô của hệ số 0 và làm lệch mọi thứ sau nó. đảo thứ tự: đó là hàng hệ số nếu trạng thái viết ngược , nhưng khi ấy nó nằm ở hàng cuối chứ không phải hàng đầu. lệch các hệ số sang phải một cột nên bỏ mất . Chọn một thứ tự cho trạng thái rồi đọc hàng đầu từ chính định nghĩa là cách chắc chắn nhất.
Chạy nhanh hơn. Chi phí tăng theo lập phương của , nên việc đầu tiên là giữ nhỏ. Ngoài ra có mấy cách tiết kiệm. Thường ta chỉ cần cho một vectơ , không cần cả : vì các giao hoán với nhau, nhân dần từng vào vectơ, mỗi lần thay vì , 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 một lần rồi mỗi truy vấn tố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: và , chỉ theo dõi một cặp số. Khi lên vài trăm, người ta tính modulo đa thức đặc trưng (kỹ thuật Kitamasa) với ; đó 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 (); thứ tự các dạng được phân biệt. Đếm số cấu hình có tổng độ dài đúng , theo modulo .
Lời giải. Gọi là số cấu hình dài , . Viên đầu là dạng thường thì phần còn lại dài , là dạng mở rộng thì dài ; hai trường hợp rời nhau và phủ hết, nên . Đây là truy hồi bậc , và với (chỉ xếp được toàn viên thường). Với , : các cấu hình là , , .
Bước 1: ma trận dời cửa sổ. Trạng thái là cửa sổ giá trị liên tiếp . Hàng đầu của ma trận tính giá trị mới , nên có số 1 ở cột 0 và cột ; các hàng sau dời cửa sổ xuống một ô.
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à , toàn số 1.
Bước 2: nhân vectơ thay vì nhân hai ma trận. Ta chỉ cần , nên giữ vectơ w và duyệt bit của : bit 1 thì w = T * w (chỉ 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.
for (; n > 0; n >>= 1) {
if (n & 1) w = mulvec(t, w);
t = mul(t, t);
}
Bước 3: đọc đáp án. Sau bước, cửa sổ là , nên nằm ở tọa độ cuối w[m - 1].
#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
3
Ma trận : 60 lần bình phương, mỗi lần phép, khoảng , vừa một giây; phần nhân vectơ chỉ .
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 . Số không phải là tổ hợp của , nên ta thêm một tọa độ luôn bằng 1 với hàng để nó tự giữ nguyên: trạng thái và ma trận có các hàng , , . Cũng cách đó biến hàm bậc nhất (biến thành ) thành ma trận tác động lên .
Tổng tiền tố. Muốn có , để ý . Thêm vào trạng thái: với ma trận có các hàng , , . Tổng trên đoạn là , đúng ý của tổng tiền tố ở bài 02 nhưng bây giờ có thể tới .
Chỉ số và các lũy thừa của nó. Nếu truy hồi có số hạng dạng hay , thêm các tọa độ , và hằng : khi tăng một, và đều là tổ hợp tuyến tính của .
Điều ta không làm được là các số hạng phi tuyến như . Khi truy hồi chỉ gồm các phép nhân, ta chuyển sang theo dõi số mũ: nếu thì mỗi là tích các lũy thừa của với số mũ theo một truy hồi cộng kiểu Fibonacci.
Hỏi nhanh
Xét . Trạng thái đã có và . Để một ma trận cố định tính được ở mọi bước, cần thêm những tọa độ nào?
Ma trận chuyển là một khối cố định dùng chung cho mọi bước, nên nó không biết mình đang ở bước nào; muốn cộng vào thì phải là một tọa độ của trạng thái. Nhưng tọa độ cũng phải tự tăng được: là phép cộng một hằng, nên cần thêm tọa độ hằng . Trạng thái có ma trận với các hàng , , , .
Đáp án đầu đòi một ma trận khác nhau ở mỗi bước, thứ mà lũy thừa không cho phép. Chỉ thêm thì không có cách tăng nó lên một. không xuất hiện trong truy hồi nên chưa cần.
Ví dụLặp Hàm Tuyến Tính
Đề. Cho với . Bắt đầu từ , áp đúng lần; in kết quả theo modulo .
Lời giải. Thêm tọa độ hằng 1: trạng thái và một lần áp là nhân với , vì là tọa độ trên và tự giữ nguyên. Áp lần là nhân với ma trận ấy mũ . Có công thức đóng nhưng nó phải chia, và vỡ khi ; ma trận thì không phải bận tâm. Với ví dụ .
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 , , theo modulo trước khi nhân để tích không tràn. Gọi là ma trận mũ , thì có tọa độ trên .
mat r = power({{A % MOD, B % MOD}, {0, 1}}, n);
cout << (r[0][0] * (x % MOD) + r[0][1]) % MOD << '\n';
#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
123
Khoảng 60 lần nhân ma trận .
Đế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ố ) 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 đỉnh. Ma trận kề có bằng số cạnh từ tới . Một đường đi (walk) có cạnh là dãy đỉnh mà mỗi là một cạnh; đỉnh và cạnh được lặp lại. Điều cần nhớ: là số đường đi đúng cạnh từ tới . Quy nạp theo : phân loại đường đi cạnh theo đỉnh ngay trước đỉnh cuối, phần đầu có cách, rồi cạnh có cách; cộng theo ra đúng công thức nhân ma trận.
Trong hình, ô hàng 1, cột 4 của bằng , ứng với hai đường đi và .
Đừ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 tới : lấy ô . Tổng số đường đi cạnh: cộng mọi ô của . Đường đi không quá cạnh: thêm một đỉnh mới có khuyên và cạnh từ mọi đỉnh ; mỗi đường đi cạnh khớp với đúng một đường đi cạnh kết thúc ở .
Hỏi nhanh
Đồ thị có ba đỉnh và ba cạnh , , (một chu trình). Ô , số đường đi đúng 7 cạnh từ 1 tới 2, bằng bao nhiêu?
Mỗi đỉnh chỉ có một cạnh đi ra, nên từ đỉnh 1 có đúng một đường đi cho mỗi độ dài. Sau 6 cạnh ta đã đi đúng hai vòng và quay về đỉnh 1; cạnh thứ 7 đưa tới đỉnh 2. Vậy .
Đáp án 0 nhầm đường đi với đường đi đơn (không lặp đỉnh) trong khi đề hỏi đường đi tự do. Đáp án 2 và 7 đếm theo số vòng hoặc số cạnh, không theo số đường đi phân biệt.
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 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 : thêm 0 đưa mọi trạng thái về 0, thêm 1 đưa lên nếu . Với vectơ cột số xâu ở từng trạng thái,
với cạnh nằm ở hàng , cột . Các vectơ có tổng ; tám xâu độ dài 3 trừ xâu đú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 đỉnh. Đếm tổng số đường đi (walk) đúng cạnh với đầu, cuối tùy ý, theo modulo .
Lời giải. Ô của là số đường đi cạnh từ tới , nên đáp án là tổng mọi ô của . Ta không cần cả ma trận: với là vectơ toàn số 1, là số đường đi cạnh bắt đầu tại , và đáp án là tổng các tọa độ. Quy hoạch động theo từng độ dài thì tốn .
Bước 1: ma trận kề. Cạnh đặt ở hàng , cột (đổi về đánh số từ 0). Khi đó (A * w)[u] cộng w[v] qua mọi cạnh : số đường đi bắt đầu tại bằng tổng qua đỉnh kế tiếp.
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 . w bắt đầu là ; 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.
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 độ.
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.
#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
2
30 lần bình phương ma trận , mỗi lần phép.
Đổi phép cộng và phép nhân
Trong chứng minh 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:
- : thay “cộng” bằng và “nhân” bằng , . Khi là trọng số cạnh (vô cực nếu không có cạnh), là độ dài đường đi ngắn nhất đúng cạnh. Cùng khuôn với cho đường đi dài nhất.
- : “có hay không” thay cho “bao nhiêu”, để biết có tới được từ bằng đúng cạnh không.
- : 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 là , phần tử đơn vị của là 0, nên ma trận đơn vị của có trên đường chéo và ở 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 cạnh từ đỉnh 1 tới đỉnh (hoặc ). Bỏ qua các ô trước khi cộng để không bị nhầm thành giá trị hữu hạn.
#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
9
Đường đi có đúng 4 cạnh và độ dài 9; đường 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 để tìm đường đi ngắn nhất đúng cạnh. Ma trận kết quả lúc bắt đầu, tương ứng , cần là ma trận nào?
Ma trận đơn vị phải thỏa , tức . Chọn (phần tử đơn vị của phép ) và với (phần tử không của ) thì chỉ số hạng còn sống và cho đúng .
Ma trận 1/0 quen thuộc, đọc theo , là một khuyên dài 1 ở mỗi đỉnh cùng một cạnh dài 0 giữa mọi cặp đỉnh khác nhau, nên làm sai cả độ dài lẫn số cạnh. Toàn 0 cho mọi cặp đỉnh một cạnh dài 0, rút ngắn đường đi một cách ảo. Toàn khiến mọi tích đều bằng .
Ví dụTruy Hồi Bit
Đề. Cho số hạng đầu và hệ số , đều là số không dấu 32 bit. Với , . In với .
Lời giải. Đúng truy hồi bậc , chỉ đổi cặp phép toán sang : “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.
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ổ . Hàng đầu chứa các hệ số ; 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.
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 bước. Nếu thì in thẳng . Ngược lại nhân vectơ theo các bit của , rồi đọc tọa độ trên w[0].
for (ll e = m - k; e > 0; e >>= 1) {
if (e & 1) w = mulvec(t, w);
t = mul(t, t);
}
#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
1
30 lần bình phương ma trận , mỗi lần phép bit.
Khi đi thi
- Nhận ra bài: số bước cỡ đến , 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 phép tính cho (khoảng khi ); chỉ cần 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 .
- Trước khi nộp, so kết quả với vòng lặp ngây thơ cho : 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 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 : trạng thái là số hạng gần nhất, một bước là nhân với ma trận cố định (hàng đầu là hệ số, các hàng sau dời xuống), nên nằm trong .
- Nhân ma trận , 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 trong , 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ố, và 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ó.
- là số đường đi đúng cạnh từ tới ; 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 thì nhân vectơ thay vì nhân ma trận; đổi cặp phép toán sang hay dùng lại được code, nhưng ma trận đơn vị phải đổi theo.
Bài tập
unsigned cho tự tràn.Đọ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 và truy hồi tổng quát (SPOJ SEQ).
- Matrix ExponentiationUSACO GuideChứng minh bằng quy nạp, ma trận bậc 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 , đường đi ngắn nhất đúng cạnh bằng , 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, , thứ tự vòng lặp -- 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.