【精選】矩陣加速

Weekoder發表於2024-06-07

大家好,我是Weekoder!

今天要講的內容是矩陣加速!

這時候就有人說了:

\(\tiny{\texttt{Weekoder 這麼蒻,怎麼會矩陣啊。還給我們講,真是十惡不赦!}}\)

不不不,容我解釋。在經過我的研究後,我發現基本的矩陣運算和矩陣加速都並沒有那麼難。只要繼續往下看,相信你也能學會!

注意:以下內容的學習難度將會用顏色表示,與洛谷題目難度順序一致,即 \(\color{#FE4C61}\texttt{紅}\color{#000000}<\color{#F39C11}\texttt{橙}\color{#000000}<\color{#FFC116}\texttt{黃}\color{#000000}<\color{#52C41A}\texttt{綠}\color{#000000}\)。(並不對標洛谷題目難度,只作為學習難易度參考)

\[\huge\texttt{Part 1}\small\texttt{ Definition} \]

\[\color{#FE4C61}\texttt{定義} \]

矩陣和二維陣列很像,是由 \(m\times n\) 個數排列成 \(m\)\(n\) 列的一張表,由於排列出來的表是一個矩形,故稱其為矩陣。矩陣長這個樣子:

\[\begin{pmatrix} a_{11} & a_{12} & a_{13} & \cdots & a_{1n} \\ a_{21} & a_{22} & a_{23} & \cdots & a_{2n} \\ a_{31} & a_{32} & a_{33} & \cdots & a_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & a_{m3} & \cdots & a_{mn} \end{pmatrix} \]

可以看到,矩陣中的每個元素都有著對應的行和列,我們把一個矩陣記作 \(A\),第 \(i\)\(j\) 列的元素即為 \(a_{ij}\)。更形式化的,寫作:

\[A=(a_{ij})\in \mathbb{F^{m\times n}} \]

其中 \(\mathbb{F}\)數域,一般取為實數域 \(\mathbb{R}\) 或複數域 \(\mathbb{C}\)。(看不懂沒事,蒟蒻自行走開QWQ)

\[\huge\texttt{Part 2}\small\texttt{ Special matrices} \]

\[\color{#FE4C61}\texttt{特殊矩陣} \]

\(\texttt{1.零矩陣}\)

元素全部為 \(0\) 的矩陣稱為零矩陣。像這樣:

\[\begin{pmatrix} 0 & 0 & \cdots & 0 \\ 0 & 0 & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & 0 \end{pmatrix} \]

零矩陣記作 \(0_{m\times n}\),就是在 \(0\) 下面加上矩陣的大小 \(m\times n\)。你可以把零矩陣看做數字 \(0\),任何數乘以 \(0\) 都得 \(0\)

\(\texttt{2.對角矩陣}\)

只有主對角線上的元素有值,其餘元素為 \(0\) 的矩陣稱為對角矩陣

注:主對角線為矩陣中從左上角到右下角的一條對角線。

\[\begin{pmatrix} a_1 & 0 & \cdots & 0 \\ 0 & a_2 & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & a_n \end{pmatrix} \]

對角矩陣根據主對角線上的值,記作 \(\text{diag(}\text{a}_1,\text{a}_2,\ldots,\text{a}_n\text{)}\)

\(\texttt{3.單位矩陣}\)

主對角線上的元素均為 \(1\),其餘元素為 \(0\) 的矩陣稱為單位矩陣

\[\begin{pmatrix} 1 & 0 & \cdots & 0 \\ 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & 1 \end{pmatrix} \]

單位矩陣記作 \(I\)

記得分數中的概念分數單位嗎?矩陣單位和分數單位的“地位”差不多,代表的都是最基礎的,最小的獨立個體。你可以把單位矩陣看做數字 \(1\),任何數乘以 \(1\) 都等於它本身。

最基礎的,常見的特殊矩陣就是這些了。當然,還有很多的特殊矩陣,不過我們暫時用不到。

\[\huge\texttt{Part 3}\small\texttt{ Matrix operations} \]

\[\color{#F39C11}\texttt{矩陣運算} \]

\(\texttt{1.相等}\)

若對於矩陣 \(A,B\),所有的 \(i,j\) 都有 \(a_{ij}=b_{ij}\) 且矩陣的行和列相等,則稱矩陣 \(A,B\) 相等。

其實就是兩個矩陣長得一模一樣

\[\begin{pmatrix} a_{11} & a_{12} & a_{13} & \cdots & a_{1n} \\ a_{21} & a_{22} & a_{23} & \cdots & a_{2n} \\ a_{31} & a_{32} & a_{33} & \cdots & a_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & a_{m3} & \cdots & a_{mn} \end{pmatrix} = \begin{pmatrix} b_{11} & b_{12} & b_{13} & \cdots & b_{1n} \\ b_{21} & b_{22} & b_{23} & \cdots & b_{2n} \\ b_{31} & b_{32} & b_{33} & \cdots & b_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ b_{m1} & b_{m2} & b_{m3} & \cdots & b_{mn} \end{pmatrix} \]

\(\texttt{2.矩陣加(減)法}\)

若要求 \(A,B\) 兩個矩陣之和,即 \(C=A+B\),則對於任意 \(i,j\),滿足 \(c_{ij}=a_{ij}+b_{ij}\)。要求矩陣行列相等。

總結一句話:對應位置相加。

\[\begin{aligned} {} & \begin{pmatrix} a_{11} & a_{12} & a_{13} & \cdots & a_{1n} \\ a_{21} & a_{22} & a_{23} & \cdots & a_{2n} \\ a_{31} & a_{32} & a_{33} & \cdots & a_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & a_{m3} & \cdots & a_{mn} \end{pmatrix} + \begin{pmatrix} b_{11} & b_{12} & b_{13} & \cdots & b_{1n} \\ b_{21} & b_{22} & b_{23} & \cdots & b_{2n} \\ b_{31} & b_{32} & b_{33} & \cdots & b_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ b_{m1} & b_{m2} & b_{m3} & \cdots & b_{mn} \end{pmatrix} \\ = & \begin{pmatrix} a_{11}+b_{11} & a_{12}+b_{12} & a_{13}+b_{13} & \cdots & a_{1n}+b_{1n} \\ a_{21}+b_{21} & a_{22}+b_{22} & a_{23}+b_{23} & \cdots & a_{2n}+b_{2n} \\ a_{31}+b_{31} & a_{32}+b_{32} & a_{33}+b_{33} & \cdots & a_{3n}+b_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{m1}+b_{m1} & a_{m2}+b_{m2} & a_{m3}+b_{m3} & \cdots & a_{mn}+b_{mn} \end{pmatrix} \end{aligned} \]

矩陣加法滿足交換律和結合律:

\[A+B=B+A \]

\[(A+B)+C=A+(B+C) \]

減法同理,對應位置相減。

\[\begin{aligned} {} & \begin{pmatrix} a_{11} & a_{12} & a_{13} & \cdots & a_{1n} \\ a_{21} & a_{22} & a_{23} & \cdots & a_{2n} \\ a_{31} & a_{32} & a_{33} & \cdots & a_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & a_{m3} & \cdots & a_{mn} \end{pmatrix} - \begin{pmatrix} b_{11} & b_{12} & b_{13} & \cdots & b_{1n} \\ b_{21} & b_{22} & b_{23} & \cdots & b_{2n} \\ b_{31} & b_{32} & b_{33} & \cdots & b_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ b_{m1} & b_{m2} & b_{m3} & \cdots & b_{mn} \end{pmatrix} \\ = & \begin{pmatrix} a_{11}-b_{11} & a_{12}-b_{12} & a_{13}-b_{13} & \cdots & a_{1n}-b_{1n} \\ a_{21}-b_{21} & a_{22}-b_{22} & a_{23}-b_{23} & \cdots & a_{2n}-b_{2n} \\ a_{31}-b_{31} & a_{32}-b_{32} & a_{33}-b_{33} & \cdots & a_{3n}-b_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{m1}-b_{m1} & a_{m2}-b_{m2} & a_{m3}-b_{m3} & \cdots & a_{mn}-b_{mn} \end{pmatrix} \end{aligned} \]

\(\texttt{3.矩陣數乘}\)

\(\lambda\)(一個數字) 乘以矩陣 \(A\),記作 \(\lambda A\),即為矩陣數乘運算。若有 \(B=\lambda A\),則對於任意 \(i,j\) 都滿足 \(b_{ij}=\lambda a_{ij}\)

還是一句話:對應位置相乘。

\[\lambda\begin{aligned} {} & \begin{pmatrix} a_{11} & a_{12} & a_{13} & \cdots & a_{1n} \\ a_{21} & a_{22} & a_{23} & \cdots & a_{2n} \\ a_{31} & a_{32} & a_{33} & \cdots & a_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & a_{m3} & \cdots & a_{mn} \end{pmatrix} = \begin{pmatrix} \lambda a_{11} & \lambda a_{12} & \lambda a_{13} & \cdots & \lambda a_{1n} \\ \lambda a_{21} & \lambda a_{22} & \lambda a_{23} & \cdots & \lambda a_{2n} \\ \lambda a_{31} & \lambda a_{32} & \lambda a_{33} & \cdots & \lambda a_{3n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ \lambda a_{m1} & \lambda a_{m2} & \lambda a_{m3} & \cdots & \lambda a_{mn} \end{pmatrix} \end{aligned} \]

\[\huge\texttt{Part 3.5}\small\texttt{ Matrix multiplication} \]

\[\color{#FFC116}\texttt{矩陣乘法} \]

雖然矩陣乘法也屬於矩陣運算,但難度比前面的都高,而且是今天的重點內容,所以單獨放出來講,故記為 \(\texttt{Part 3.5}\)。(話說你們沒有發現難度變成黃了嗎)

例題!(雖然難度是橙)

先看矩陣乘法的定義:若有 \(n\)\(m\) 列矩陣 \(A\)\(m\)\(k\) 列的矩陣 \(B\)\(A\) 的行與 \(B\) 的列相等),則 \(n\)\(k\) 列的矩陣 \(C=A\times B\) 滿足

\[c_{ij}=\sum_{l=1}^m a_{il}\times b_{lj} \]

只要列舉 \(i,j\)(範圍是 \(n,k\)),並套用公式就能用 \(O(n^3)\) 的時間複雜度解決這個問題。

我知道,這看起來根本不是新手蒟蒻能看懂的。那我就用人話來講講矩陣乘法。

矩陣乘法並不是一個一個乘,而是行對應列乘。怎麼個乘法呢?我們來看看下面兩個矩陣相乘的例子。

\[\begin{pmatrix} 5 & 2 & 3 \\ 7 & 9 & 4 \\ \end{pmatrix} \times \begin{pmatrix} 2 & 6 & 8 & 1 \\ 0 & 9 & 1 & 3 \\ 2 & 4 & 4 & 1 \end{pmatrix} \]

第一個矩陣為 \(A\),第二個矩陣為 \(B\)

我們先取出 \(A\)第一行。像這樣:

\[\begin{pmatrix} 5 & 2 & 3 \end{pmatrix} \]

再取出 \(B\)第一列。像這樣:

\[\begin{pmatrix} 2 \\ 0 \\ 2 \end{pmatrix} \]

不對,你給我轉過來。

\[\begin{pmatrix} 2 & 0 & 2 \end{pmatrix} \]

現在終於可以相乘了。逐位相乘得出結果:

\[\begin{pmatrix} 5 & 2 & 3 \end{pmatrix} \times \begin{pmatrix} 2 & 0 & 2 \end{pmatrix} = \begin{pmatrix} 5\times2 & 2\times0 & 3\times2 \end{pmatrix} = \begin{pmatrix} 10 & 0 & 6 \end{pmatrix} \]

得出了結果 \(\begin{pmatrix} 10 & 0 & 6 \end{pmatrix}\)。再將每一位相加:

\[\begin{pmatrix} 10 & 0 & 6 \end{pmatrix} \to 10+0+6=16 \]

還記得我們之前是怎麼取的嗎?我們取了 \(A\)第一行\(B\)第一列(注意加粗部分),所以答案就儲存在 \(C\)第一行第一列。還沒搞懂?更通用一點:我們取了 \(A\)\(x\)\(B\)\(y\)(注意加粗部分),所以答案就儲存在 \(C\)\(x\) 行第 \(y\)。也就是說,當我們想要獲取矩陣 \(C\) 的第 \(x\)\(y\) 列的時候,就需要取 \(A\) 的第 \(x\) 行和 \(B\) 的第 \(y\) 列,相乘再相加。由於 \(A\) 的行數與 \(B\) 的列數相等,取出來的數列才可以逐位相乘(不然元素個數不一樣)。而取出來的數列長度就是 \(m\),所以可以用 \(O(m)\) 求和,總時間複雜度 \(O(nmk)=O(n^3)\)

最後,可以看看程式碼輔助理解。

#include <bits/stdc++.h>
using namespace std;

const int N = 105;

int n, m, k, a[N][N], b[N][N]; // 用二維陣列存矩陣 A,B

int main() {
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> a[i][j]; // 輸入矩陣 A
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= k; j++)
            cin >> b[i][j]; // 輸入矩陣 B
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= k; j++) { // 列舉 C 矩陣 n 行 k 列的每個元素
  	        // 以下部分為模擬剛剛講的矩陣乘法
            int sum = 0; // 求和,sum 即為 C_ij
            for (int l = 1; l <= m; l++)
                sum += a[i][l] * b[l][j]; // 求和,A 的行和 B 的列,建議模擬一下過程加強理解
            cout << sum << " "; // 輸出 sum(C_ij)
        }
        cout << "\n"; // 記得換行!
    }
    return 0; // 完美的結束
} 

這樣就能愉快地切掉這道題了。請完成這道題再繼續!

矩陣乘法滿足以下性質:

結合律:\((AB)C=A(BC)\)

分配律:\((A+B)C=AC+BC\)

矩陣乘法不滿足交換律。(這是重點!)

有了矩陣乘法,我們還可以結合上面的特殊矩陣得到一些性質:

\[A\times I=A \]

\[A\times 0_{m\times n}=0_{m\times n} \]

\[\huge\texttt{Part 4}\small\texttt{ Matrix fast power} \]

\[\color{#52C41A}\texttt{矩陣封裝 & 矩陣快速冪} \]

快到今天的主題了!上例題

點開題目後的你 be like:

這是啥呀?

我來讓題目描述“縮點水”:

給定一個 \(n\)\(n\) 列的矩陣 \(A\),求 \(A^k\),即 \(\underbrace{A\times A\times A\times\cdots\times A\times A}_{k\texttt{ 次}}\)

第一思路:暴力!直接做 \(k\) 次矩陣乘法,時間複雜度 \(O(kn^3)\)。看看資料範圍:

\(0\le k\le10^{12}\)

考慮放棄做題。

那我們該怎麼最佳化呢?看到需要計算 \(A^k\),我突然想到了一個演算法:快速冪!但是矩陣快速冪該怎麼寫呢?答案是:和正常的快速冪一樣,矩陣也能使用快速冪,只不過快速冪中的乘法變成了矩陣乘法。但是矩陣乘法太難寫,有沒有什麼辦法能讓矩陣乘法也像普通的乘法一樣,只要寫一個 * 乘號就行了呢?

注意:不會快速冪的話可以先簡單看看我寫的文章

回到主題,有沒有什麼辦法能只要寫一個 * 乘號就能進行矩陣乘法呢?其實我們可以用結構體把矩陣封裝起來,再用過載運算子就行了。關於過載運算子,可以參考這些資料

定義一個矩陣型別的結構體可以寫成這樣:

struct Matrix {
	
};

我們需要在裡面用一個二維陣列儲存矩陣。我們還可以寫一個結構體初始化函式,只要定義了一個矩陣,就自動清零,免去清零的麻煩。

struct Matrix {
	int a[N][N]; // N 為矩陣大小
	Matrix() {
		memset(a, 0, sizeof a);
	}
};

最後,把矩陣乘法寫進去。

struct Matrix {
    ll a[N][N];
    Matrix() {
        memset(a, 0, sizeof a);
    }
    Matrix operator*(const Matrix &x)const {
        Matrix res;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                for (int k = 1; k <= n; k++)
                    res.a[i][j] = (res.a[i][j] % MOD + a[i][k] % MOD * x.a[k][j] % MOD) % MOD;
        return res;
    }
}; 

注意,這裡一定要寫成 a[i][k] * x.a[k][j],不能寫成 x.a[i][k] * a[k][j],因為矩陣乘法不滿足交換律!

這樣,結構體封裝部分就完成了。

我們要定義兩個矩陣:\(a\)\(base\)\(a\) 是輸入的矩陣,\(base\) 是答案矩陣,所以 \(base\) 需要初始化成 \(I\)(單位矩陣),寫一個初始化函式 \(\operatorname{init}\),如下:

void init() {
    for (int i = 1; i <= n; i++) base.a[i][i] =1;
}

初始化完以後,就可以執行快速冪了,計算 \(A^k\) 了,讓 \(base\)\(A\)。矩陣快速冪核心程式碼如下:

void expow(ll b) {
    while (b) {
        if (b & 1) base = base * a;
        a = a * a, b >>= 1;
    }  
}

有一點需要注意的就是,不能寫成 base *= a 等形式,因為過載運算子定義的是 *,沒有定義 *=,所以需要將 *= 展開。

最後,就可以輸出 \(base\) 了。展示全部程式碼:

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int N = 105, MOD = 1e9 + 7;

int n;
ll k;

struct Matrix {
    ll a[N][N];
    Matrix() {
        memset(a, 0, sizeof a);
    }
    Matrix operator*(const Matrix &x)const {
        Matrix res;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                for (int k = 1; k <= n; k++)
                    res.a[i][j] = (res.a[i][j] % MOD + a[i][k] % MOD * x.a[k][j] % MOD) % MOD;
        return res;
    }
}a, base; 

void init() {
    for (int i = 1; i <= n; i++) base.a[i][i] =1;
}

void expow(ll b) {
    while (b) {
        if (b & 1) base = base * a;
        a = a * a, b >>= 1;
    }  
}

int main() {
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            cin >> a.a[i][j];
    init();
    expow(k);
    for (int i = 1; i <= n; putchar('\n'), i++)
        for (int j = 1; j <= n; j++)
            cout << base.a[i][j] << " ";
    return 0;
} 

\[\huge\texttt{Part 5}\small\texttt{ Matrix acceleration} \]

\[\color{#52C41A}\texttt{矩陣加速} \]

終於到了最後的 \(\color{red}\texttt{BOSS 關卡}\) 了!你們有信心嗎?加油!

點選此處進入 \(\color{red}\texttt{BOSS 關卡}\) ......

點開題目 \(\color{red}\texttt{BOSS 關卡}\) 後的你 be like(梅開二度):

這和矩陣有什麼關係嗎???

我直接一個遞推!

  • 對於 \(100\%\) 的資料 \(1 \leq T \leq 100\)\(1 \leq n \leq 2 \times 10^9\)

\(O(Tn)\)\(2\times10^{11}\) 的複雜度實在無法接受。

(嗚嗚嗚我再也不學 c艹 了)

沒關係,先看看思路!

因為發現當 \(x\le3\) 時答案為 \(1\),所以這是最基礎的情況。我們可以構造一個只有一列的矩陣:

\[\begin{pmatrix} a_3 & a_2 & a_1 \end{pmatrix} = \begin{pmatrix} 1 & 1 & 1 \end{pmatrix} \]

顯然,這三個元素都是 \(1\)

那麼,假設我想要得到 \(a_4\),該怎麼辦呢?所以,我們需要進行一種運算,讓上面的矩陣變化一下,像這樣:

\[\begin{pmatrix} a_3 & a_2 & a_1 \end{pmatrix} \to \begin{pmatrix} a_4 & a_3 & a_2 \end{pmatrix} \]

更加通用一點:

\[\begin{pmatrix} a_x & a_{x-1} & a_{x-2} \end{pmatrix} \to \begin{pmatrix} a_{x+1} & a_{x} & a_{x-1} \end{pmatrix} \]

可以發現,矩陣中的每個元素的項數都向前推進了 \(1\)。那麼,我們大概可以寫出虛擬碼:


如果 \(x\le3\)

輸出 \(1\)

否則

執行運算 \(n-3\) 次(重要!)

並輸出答案矩陣 \(1\)\(1\)


特判(對於特殊情況的判斷)和輸出應該沒什麼問題,主要是為什麼運算恰好要執行 \(n-3\) 次呢?稍微畫個圖模擬一下就好了。

還是假設要獲取 \(a_4\),則執行運算 \(4-3=1\) 次。在執行 \(1\) 次運算後,

\[\begin{pmatrix} a_3 & a_2 & a_1 \end{pmatrix} \]

變為

\[\begin{pmatrix} a_4 & a_3 & a_2 \end{pmatrix} \]

這樣就剛好在第 \(1\)\(1\) 列得到 \(a_4\) 啦!

那麼,說了這麼久,這個神秘的運算是什麼呢?噹噹噹當~,他就是我們的——矩陣乘法!

沒錯,所謂的變換,其實就是乘上了一個特殊的矩陣!那麼,這個矩陣長什麼樣呢?讓我們一起來推理吧。

(此處應配上推理の小曲)

我們可以先列一個表格,表格的行代表矩陣 \(\begin{pmatrix}a_3 & a_2 & a_1 \end{pmatrix}\) 的元素,列代表遞推時與這些元素相關的元素。像這樣:(表格可能在部落格裡渲染不出來,湊合著看吧,抱歉)

\(a_x\) \(a_{x-1}\) \(a_{x-2}\)
\(a_{x-1}\)
\(a_{x-2}\)
\(a_{x-3}\)

好了,對於 \(a_x\),我們該怎麼填他那一列呢?我們可以觀察到遞推式 \(a_x=a_{x-1}+a_{x-3}\),所以有:

\[a_x=a_{x-1}\times1+a_{x-2}\times0+a_{x-3}\times1 \]

觀察係數 \(1,0,1\),把這些係數填入表格中:

\(a_x\) \(a_{x-1}\) \(a_{x-2}\)
\(a_{x-1}\) \(1\)
\(a_{x-2}\) \(0\)
\(a_{x-3}\) \(1\)

後面的也以此類推:

\[a_{x-1}=a_{x-1}\times1+a_{x-2}\times0+a_{x-3}\times0 \]

\[a_{x-2}=a_{x-1}\times0+a_{x-2}\times1+a_{x-3}\times0 \]

\(a_x\) \(a_{x-1}\) \(a_{x-2}\)
\(a_{x-1}\) \(1\) \(1\) \(0\)
\(a_{x-2}\) \(0\) \(0\) \(1\)
\(a_{x-3}\) \(1\) \(0\) \(0\)

這樣,我們就可以推出這個神秘的矩陣了:

\[\begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix} \]

好了,現在我們終於知道了,一次神秘操作,就是將讓 \(\begin{pmatrix}a_3 & a_2 & a_1 \end{pmatrix}\) 這個矩陣乘上\( \begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix} \)。這時候就有人問了:

一次矩陣乘法的時間複雜度還沒有遞推快,這根本就沒有最佳化嘛。

等等!我們把這個式子展開:

\[\begin{aligned} {} & \begin{pmatrix} 1 & 1 & 1 \end{pmatrix} \times \begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix} \times \begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix} \cdots \times \begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix} \\ = & \begin{pmatrix} 1 & 1 & 1 \end{pmatrix} \times \begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix} ^{n-3} \end{aligned} \]

不是吧!這居然變成了一個矩陣快速冪?!!

也就是說,我們可以用快速冪計算 \(\begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix} ^{n-3}\),並乘上初始矩陣 \(\begin{pmatrix} 1 & 1 & 1 \end{pmatrix}\)。這樣,我們成功地把時間複雜度從 \(O(Tn)\) 最佳化到了 \(O(T\log n)\)!(矩陣快速冪是 \(O(\log n)\),因為矩陣很小,矩陣乘法只計算 \(9\) 次,是一個很小的常數)

下面奉上程式碼:(標準的矩陣加速思想)

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MOD = 1e9 + 7;

int T, n;

struct Matrix {
    ll a[5][5];
    Matrix() {
        memset(a, 0, sizeof a);
    }
    Matrix operator*(const Matrix &x)const { // 矩陣乘法
        Matrix res;
        for (int i = 1; i <= 3; i++)
            for (int j = 1; j <= 3; j++)
            	for (int k = 1; k <= 3; k++)
                    res.a[i][j] = (res.a[i][j] % MOD + a[i][k] % MOD * x.a[k][j] % MOD) % MOD;
        return res;
    }
    void mems() {
    	memset(a, 0, sizeof a);
	}
}ans, base; 

void init() { // 初始化兩個矩陣
	ans.mems(), base.mems(); // 記得清空!
	ans.a[1][1] = ans.a[1][2] = ans.a[1][3] = 1;
	base.a[1][1] = base.a[1][2] = base.a[2][3] = base.a[3][1] = 1;
}

void expow(int b) { // 矩陣快速冪,是在 ans 矩陣的基礎上乘的
    while (b) {
        if (b & 1) ans = ans * base;
        base = base * base, b >>= 1;
    }  
}

int main() {    
    cin >> T;
    while (T --) {
        cin >> n;
        init(); // 初始化不能忘
        if (n <= 3) { // 特判
            cout << "1\n";
            continue;
        } 
        expow(n - 3); // 計算特殊矩陣的 n - 3 次方,已經乘到了 ans 裡
        cout << ans.a[1][1] << "\n"; // 輸出答案!蕪湖!
    }
    return 0; // 快樂結束
} 

就這樣,我們完成了矩陣加速遞推。

再次宣告矩陣快速冪(矩陣加速)時間複雜度:\(O(N^3\log n)\),其中 \(N\) 為矩陣的行數(列數),\(n\) 為快速冪的規模 \(a^n\)

小提示:關於 \(base\) 矩陣的構造

就是這個 \(\begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix}\) 矩陣。

可以這樣:我們要推匯出 \(\begin{pmatrix}a_x & a_{x-1} & a_{x-2}\end{pmatrix}\),那麼這個矩陣從哪裡來?當然是從 \(\begin{pmatrix}a_{x-1} & a_{x-2} & a_{x-3}\end{pmatrix}\) 來。所以,表格才長這樣:

\(a_x\) \(a_{x-1}\) \(a_{x-2}\)
\(a_{x-1}\)
\(a_{x-2}\)
\(a_{x-3}\)

那麼,能不能構造一個行列數各不相同的矩陣,而不是一個 \(n\times n\) 的矩陣呢?答案是不可以,因為我們要計算 \(\begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{pmatrix}\) 這種矩陣的冪,那如果行和列不相等,相乘的兩個矩陣的行列也不相等,就無法進行矩陣乘法。比如這個:

\[\begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix} \times \begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix} \]

可以看到,左邊 \(2\) 行,右邊 \(3\) 列,顯然不相等,無法進行矩陣乘法。

\[\huge\texttt{Part 6}\small\texttt{ Thank you!} \]

\[\color{#FE4C6E}\texttt{你}\color{#F39C11}\texttt{居}\color{#FFC116}\texttt{然}\color{#52C41A}\texttt{看}\color{#3498DB}\texttt{完}\color{#9D3DCF}\texttt{了}\color{#0E1D69}\texttt{!} \]

這篇文章花費了我很多時間,希望你喜歡!

對了,你學會了嗎?是不是,矩陣也並沒有那麼難?

這應該是我的【精選】文章中的第一篇,沒想到寫的是矩陣方面的。

總之,很感謝你的閱讀!希望你能從我這學到點東西!

再見!

相關文章