D矩阵 DP

矩阵快速幂加速

递推→矩阵幂·O(k³log n)

本课摘要

矩阵快速幂加速课程回答“线性递推怎样写成矩阵并用快速幂加速”。内容以递推→矩阵幂·O(k³log n)为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断矩阵快速幂加速的适用条件与状态边界
  • 围绕“递推→矩阵幂·O(k³log n)”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

当递推项数大到没法一格一格填

回到最朴素的斐波那契:f[i]=f[i−1]+f[i−2]f[i]=f[i-1]+f[i-2],从 f[1]=f[2]=1f[1]=f[2]=1 起逐项往上加。 只要 nn 不大,一个 O(n)O(n) 的循环就够——这正是 B 部分计数 DP 里数楼梯那一套。 可一旦题目把 nn 抬到 n<263n<2^{63}(例题 P1962 就是),逐项递推要走近 九百亿亿 步,任何机器都算不完。

f[0]1f[1]1f[2]2f[3]3f[4]5f[5]8· · ·f[n]n ≈ 2⁶³逐项递推要走 n 步——n 达 2⁶³ 时 O(n) 必然超时
逐项递推每次只前进一格,要走 n 步才到 f[n];n 达 2⁶³ 时 O(n) 彻底超时,逐格填表的思路在这里失效。

瓶颈很清楚:递推一步只跨一项,代价被死死锁在 O(n)O(n)。要提速,就得想办法一次跨过很多项。 突破口在于——斐波那契这类递推是线性的(新项是旧几项的线性组合,没有平方、没有取最值)。线性变换恰好可以写成矩阵乘法,而「重复施加同一个线性变换 nn 次」就是求矩阵的 nn 次幂—— 幂运算有快速幂(二进制倍增),能把 nn 次压成 log⁡n\log n 次。这一节就把这条「递推 → 矩阵 → 快速幂」的加速链讲透。

把递推写成矩阵乘法

关键一步:把「当前需要记住的几项」打包成一个状态向量。斐波那契的新项只用到前两项,于是取行向量 [ F(n−1), F(n−2) ][\,F(n-1),\ F(n-2)\,] 作状态。 我们要找一个固定的矩阵 MM,让它右乘这个向量后,正好整体前移一步,得到 [ F(n), F(n−1) ][\,F(n),\ F(n-1)\,]:

[ F(n), F(n−1) ]=[ F(n−1), F(n−2) ]⋅M[\,F(n),\ F(n-1)\,]=[\,F(n-1),\ F(n-2)\,]\cdot M
旧状态(行向量)[ F(n−1) F(n−2) ]×转移矩阵 M1110=新状态[ F(n) F(n−1) ]第一列做 F(n−1)+F(n−2)=F(n);第二列把 F(n−1) 原样移下来一次乘法 = 递推走一步;乘 M 的 n 次方 = 一步跨过 n 项
状态向量右乘转移矩阵 M=[[1,1],[1,0]]:结果第一列算出 F(n-1)+F(n-2)=F(n),第二列把 F(n-1) 原样移下——一次乘法 = 递推走一步。

怎么定出 MM 的每个数?逐个输出分量看它由旧分量怎样线性组合:新向量第一个分量要等于 F(n)=F(n−1)+F(n−2)F(n)=F(n-1)+F(n-2), 即旧的两个分量各取一份相加 → 对应 MM 的第一列是 (11)\binom{1}{1};新向量第二个分量要等于 F(n−1)F(n-1), 即只取旧的第一个分量 → 第二列是 (10)\binom{1}{0}。合起来:

M=[1110]M=\begin{bmatrix}1 & 1\\ 1 & 0\end{bmatrix}

于是从初始向量 [ F(2),F(1) ]=[ 1,1 ][\,F(2),F(1)\,]=[\,1,1\,] 出发,右乘一次 MM 前进一项,乘 kk 次就前进 kk 项。 把 kk 次乘法凑成一个整体,就是 MkM^{k}——「跨过一大段递推」被压缩成「求一个矩阵的高次幂」。可以验证 Mn−1M^{n-1} 的左上角恰是 F(n)F(n)。

本质 · 递推 ↔ 矩阵,一次乘法 = 一步递推

线性递推与矩阵乘法是同一件事的两种写法:把「本步要用到的历史项」摆成状态向量,转移的每个系数就落进矩阵的一列;右乘一次 MM = 递推推进一步,右乘 MnM^{n} = 一口气推进 nn 步。 原本 O(n)O(n) 的逐项递推,就此转化为「求矩阵幂 MnM^{n}」这个能被快速幂加速的问题。

快速幂:把 Mⁿ 的 n 次拆成 log n 层

剩下的问题是怎样快速求 MnM^{n}。若老老实实 M⋅M⋅M⋯M\cdot M\cdot M\cdots 连乘,还是 n−1n-1 次矩阵乘法,白忙一场。快速幂(二进制倍增)的思路:把指数 nn 写成二进制,例如 13=11012=8+4+113=1101_2=8+4+1,于是

M13=M8⋅M4⋅M1M^{13}=M^{8}\cdot M^{4}\cdot M^{1}

而 M1,M2,M4,M8,…M^{1},M^{2},M^{4},M^{8},\dots 这串倍增幂,每个都是前一个平方得来(M2k=(Mk)2M^{2k}=(M^{k})^{2}),只需 log⁡n\log n 次平方就能全部算出。 再按 nn 的二进制里哪几位是 1,把对应的倍增幂累乘进结果即可。

n = 13 = 1101₂ → 平方倍增,再挑「位为 1」的幂相乘M位0=1M²位1=0M⁴位2=1M⁸位3=1²²²M⁸ · M⁴ · M¹ = M¹³
n=13=1101₂:上排 M,M²,M⁴,M⁸ 靠平方逐个倍增;n 的二进制里为 1 的位(第 0、2、3 位)对应的幂被挑出,相乘得到 M¹³。

总代价:log⁡n\log n 次平方 + 至多 log⁡n\log n 次累乘,共约 2log⁡n2\log n 次矩阵乘法;每次矩阵乘法是 O(k3)O(k^{3})(kk 为矩阵阶数)。 合起来 O(k3log⁡n)O(k^{3}\log n)。斐波那契 k=2k=2、n=263n=2^{63} 时也不过约 6363 层倍增、百余次 2×22\times2 乘法,瞬间出解。把这套骨架写成中文伪代码:

# 矩阵快速幂:求 base^n(base 是 k×k 矩阵)
res = 单位阵 I                 # 任何矩阵乘 I 不变,作累乘的起点
while n > 0:
    if n & 1 == 1:            # 当前二进制最低位为 1
        res = res × base      # ★把这一位对应的倍增幂累乘进结果
    base = base × base        # 平方 → 得到下一个倍增幂 M^(2^k)
    n >>= 1                   # 右移一位,看下一位
return res                    # 循环 ⌊log₂ n⌋+1 次,每次一到两回矩阵乘法
下面的演示把 nn 写成二进制,逐位展示 M,M2,M4,…M,M^2,M^4,\dots 的平方倍增,并把「位为 1」的幂累乘成 MnM^{n}。改指数 nn,看矩阵数值、当前操作与步数对比实时重算。

看 Mⁿ 在倍增里累乘出来

指数 n(要算 Mⁿ,即斐波那契第 n+1 项)
n
5
n 的二进制(低位驱动倍增)
101= 510
倍增序列 M, M², M⁴, M⁸ …:每行处理 n 的一个二进制位(低位在上)位为 1 才累乘
起点 acc = I(单位阵)
1001
从单位阵出发,逐位累乘
M1位0=1
M^1(平方倍增得到)
1110
累乘
累乘后 acc
1110
M2位1=0
M^2(平方倍增得到)
2111
跳过
acc 不变
1110
M4位2=1
M^4(平方倍增得到)
5332
累乘
累乘后 acc
8553
Mⁿ = M^5
8553
左上角 = F(6) = 8
斐波那契 F(1)=F(2)=1;Mⁿ 左上恒为 F(n+1)(n=5→F(6)=8,n=10→F(11)=89)。
暴力:连乘 M
4次乘法
M¹→Mⁿ 逐个乘,共 n−1 次 · O(n)
快速幂:倍增
4次乘法
平方 2 + 累乘 2 · O(log n)
n=5 时,暴力要做 4 次矩阵乘法,快速幂只用 4 次—— 把线性的 O(n) 压成对数的 O(log n)。 真正题目里 n 可达 2⁶³,暴力 O(n) 必然超时, 快速幂却只需约 3 层倍增即可。

深化 · 非标准递推:怎么构造转移矩阵

斐波那契的 MM 太经典,容易让人以为矩阵是「背下来」的。真正的功夫在于面对一个陌生递推,自己把矩阵拼出来。 来看一个非标准递推(正是例题 P1939 的模型):a[x]=a[x−1]+a[x−3]a[x]=a[x-1]+a[x-3]——新项跳过了 a[x−2]a[x-2],只由 a[x−1]a[x-1] 和 a[x−3]a[x-3] 决定。

第一步,定状态向量。新项用到往前三项(最远到 a[x−3]a[x-3]),所以状态要装下最近三项:[ a[x−1], a[x−2], a[x−3] ][\,a[x-1],\ a[x-2],\ a[x-3]\,],是 3 维向量,矩阵就是 3×33\times3。 推进一步后,新状态应当是 [ a[x], a[x−1], a[x−2] ][\,a[x],\ a[x-1],\ a[x-2]\,]。

第二步,逐行确定系数——盯住新状态的每个分量由旧分量怎样组合:

M=[101100010]M=\begin{bmatrix}1 & 0 & 1\\ 1 & 0 & 0\\ 0 & 1 & 0\end{bmatrix}
a[x] = a[x-1] + a[x-3] → 状态 3 维 → 3×3 矩阵,逐行填旧状态a[x-1]a[x-2]a[x-3]101→ a[x]递推系数:1·a[x-1] + 0·a[x-2] + 1·a[x-3]100→ a[x-1]位移:搬来旧的 a[x-1]010→ a[x-2]位移:搬来旧的 a[x-2]
a[x]=a[x-1]+a[x-3] 的 3×3 转移矩阵:首行是递推系数 [1,0,1](真正做加法的一行);其余两行是「位移行」,把旧的 a[x-1]、a[x-2] 原样搬下来,各只有一个 1。

读这三行——第一行(算 a[x]a[x]):a[x]=1⋅a[x−1]+0⋅a[x−2]+1⋅a[x−3]a[x]=1\cdot a[x-1]+0\cdot a[x-2]+1\cdot a[x-3],系数就是递推式里的系数,写成 [ 1,0,1 ][\,1,0,1\,]——这是唯一「真正做递推加法」的一行。第二行(算 a[x−1]a[x-1]):新状态里的 a[x−1]a[x-1] 不过是旧状态里现成的 a[x−1]a[x-1],原样搬过来 → [ 1,0,0 ][\,1,0,0\,]。第三行(算 a[x−2]a[x-2]):同理搬旧的 a[x−2]a[x-2] → [ 0,1,0 ][\,0,1,0\,]。 后两行统称位移行,作用只是把向量整体「下移一格」,好腾出位置给新项——它们对任何这类递推都长得差不多,套路固定。

构造要诀 · 一行递推系数,其余全是位移

面对形如 a[x]=∑t≥1ct a[x−t]a[x]=\sum_{t\ge1} c_t\,a[x-t] 的常系数线性递推:状态向量取最近的若干项(维数 = 递推用到的最远回溯步数); 转移矩阵第一行直接填递推系数 [ c1,c2,… ][\,c_1,c_2,\dots\,],其余每一行是把旧分量原样下移的位移行(一个 1,其余 0)。 矩阵一旦搭好,求第 xx 项就是算 MxM^{x} 乘初始向量,复杂度 O(k3log⁡x)O(k^{3}\log x)——递推越是「奇形怪状」,这套矩阵化越显威力。

下面的演示给你两个递推预设,把状态向量、转移矩阵、输出向量并排摆好;点矩阵任意一行,它会点亮对应的输出分量并讲清这一行系数从哪来。切换预设,看维数与矩阵一起变。

看转移矩阵怎么从递推拼出来

选一个线性递推
状态向量 3 维 → 转移矩阵 3×3。 点矩阵任意一行,看这行系数从哪来。
此处按列向量左乘 M · 旧状态 = 新状态 摆放(矩阵在左、状态是列), 矩阵第 r 行正好算出新状态的第 r 个分量——与正文的行向量右乘 […]·M 只是转置写法,结果等价。
转移矩阵 M
×
旧状态
a[x-1]a[x-2]a[x-3]
=
新状态
a[x]a[x-1]a[x-2]
第 0 行a[x] = 1·a[x-1] + 0·a[x-2] + 1·a[x-3]

a[x] 由递推给出 = a[x-1] + a[x-3],故本行系数就是递推系数。

递推系数行(本步真正做加法) 位移行(把旧分量原样下移,只一个 1)有了 M,求第 x 项就是算 Mˣ 乘初始向量——用矩阵快速幂 O(k³log x)。

例题

P1962斐波那契数列洛谷原生普及+/提高
题意
求斐波那契数列第 nn 项对 109+710^9+7 取模的值,F(1)=F(2)=1F(1)=F(2)=1,n<263n<2^{63}。
为什么选它
nn 大到 2632^{63},逐项递推的 O(n)O(n) 无论如何都超时——逼你把递推矩阵化再快速幂。是「递推 → 矩阵快速幂」这条加速链最干净的入门题,一切从这个 2×22\times2 的 MM 开始。
转移 · 复杂度
M=[[1,1],[1,0]]M=[[1,1],[1,0]],答案取 (Mn−1)[0][0](M^{n-1})[0][0];每次矩阵乘法 O(23)O(2^{3}),快速幂 O(log⁡n)O(\log n) 层,总 O(8log⁡n)O(8\log n)。
参考代码(2×2 矩阵快速幂)
#include <iostream>
using namespace std;

typedef long long ll;
const ll MOD = 1000000007;

// 2×2 矩阵,封装乘法(每步取模,防溢出)。斐波那契转移 M = {{1,1},{1,0}}。
struct Mat
{
    ll a[2][2];
};

Mat mul(const Mat &x, const Mat &y)     // 矩阵乘法:C[i][j] = Σ x[i][k]·y[k][j]
{
    Mat c;
    for (int i = 0; i < 2; i++)
        for (int j = 0; j < 2; j++)
        {
            ll s = 0;
            for (int k = 0; k < 2; k++)
            {
                s = (s + x.a[i][k] * y.a[k][j]) % MOD; // ★显式 long long + 逐步取模
            }
            c.a[i][j] = s;
        }
    return c;
}

Mat power(Mat base, ll n)               // 矩阵快速幂:base^n
{
    Mat res = {{{1, 0}, {0, 1}}};       // 单位阵起步
    while (n > 0)
    {
        if (n & 1)                      // 当前二进制位为 1 → 累乘
        {
            res = mul(res, base);
        }
        base = mul(base, base);         // 平方倍增
        n >>= 1;
    }
    return res;
}

int main()
{
    ll n;
    cin >> n;
    Mat M = {{{1, 1}, {1, 0}}};
    Mat r = power(M, n - 1);            // F(n) = (M^{n-1})[0][0],F(1)=F(2)=1
    cout << r.a[0][0] << endl;
    return 0;
}
// TAG: 矩阵DP 矩阵快速幂 斐波那契 取模
P3390【模板】矩阵快速幂洛谷原生普及+/提高
题意
给定 n×nn\times n 矩阵 AA 与指数 kk,求 AkA^{k} 的每个元素对 109+710^9+7 取模。
为什么选它
把「矩阵乘法 + 快速幂」这副骨架单独拎出来夯实——没有递推包装,纯粹练 O(n3)O(n^3) 通用矩阵乘法和二进制倍增的写法。写熟了它,所有矩阵加速题的底座就通了。
转移 · 复杂度
单位阵起步,kk 按二进制逐位:为 1 则累乘、每步平方;矩阵乘法 O(n3)O(n^3),共 O(n3log⁡k)O(n^{3}\log k)。
参考代码(通用 n×n 快速幂)
#include <iostream>
using namespace std;

typedef long long ll;
const ll MOD = 1000000007;

int n;                                   // 矩阵阶数(n×n)

struct Mat
{
    ll a[105][105];
};

Mat mul(const Mat &x, const Mat &y)      // 通用 n×n 矩阵乘法,O(n³)
{
    Mat c;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
        {
            ll s = 0;
            for (int k = 1; k <= n; k++)
            {
                s = (s + x.a[i][k] * y.a[k][j]) % MOD;
            }
            c.a[i][j] = s;
        }
    return c;
}

int main()
{
    ll k;
    cin >> n >> k;

    Mat A, res;
    for (int i = 1; i <= n; i++)         // 读入待幂的矩阵
        for (int j = 1; j <= n; j++)
        {
            cin >> A.a[i][j];
        }
    for (int i = 1; i <= n; i++)         // res 初始化为单位阵
        for (int j = 1; j <= n; j++)
        {
            res.a[i][j] = (i == j) ? 1 : 0;
        }

    while (k > 0)                        // 快速幂:A^k
    {
        if (k & 1)
        {
            res = mul(res, A);
        }
        A = mul(A, A);
        k >>= 1;
    }

    for (int i = 1; i <= n; i++)         // 输出结果矩阵
    {
        for (int j = 1; j <= n; j++)
        {
            cout << res.a[i][j] << " \n"[j == n];
        }
    }
    return 0;
}
// TAG: 矩阵DP 矩阵快速幂 模板
P1939【模板】矩阵加速(数列)洛谷原生普及+/提高
题意
数列 a[1]=a[2]=a[3]=1a[1]=a[2]=a[3]=1,a[x]=a[x−1]+a[x−3]a[x]=a[x-1]+a[x-3](x≥4x\ge4),TT 组询问,每组求 a[n] mod (109+7)a[n]\bmod (10^9+7),n≤2×109n\le 2\times10^{9}。
为什么选它
典型的非标准递推自构转移矩阵:跳项的 a[x]=a[x−1]+a[x−3]a[x]=a[x-1]+a[x-3] 逼你亲手推出 3×33\times3 的 M=[[1,0,1],[1,0,0],[0,1,0]]M=[[1,0,1],[1,0,0],[0,1,0]](一行系数 + 两行位移),正是本页深化演示所讲。
转移 · 复杂度
状态 [a[x−1],a[x−2],a[x−3]][a[x-1],a[x-2],a[x-3]],求 Mn−3M^{n-3} 作用于初始向量;单组 O(33log⁡n)O(3^{3}\log n),多组累加。
参考代码(3×3 自构矩阵)
#include <iostream>
#include <cstring>
using namespace std;

typedef long long ll;
const ll MOD = 1000000007;               // 本题模 1e9+7

// a[x] = a[x-1] + a[x-3],a[1]=a[2]=a[3]=1。
// 状态向量 (a[x-1], a[x-2], a[x-3]) → 转移矩阵 {{1,0,1},{1,0,0},{0,1,0}}。
struct Mat
{
    ll a[3][3];
};

Mat mul(const Mat &x, const Mat &y)
{
    Mat c;
    memset(c.a, 0, sizeof(c.a));
    for (int i = 0; i < 3; i++)
        for (int k = 0; k < 3; k++)
        {
            if (x.a[i][k] == 0) continue;   // 稀疏小优化,可省
            for (int j = 0; j < 3; j++)
            {
                c.a[i][j] = (c.a[i][j] + x.a[i][k] * y.a[k][j]) % MOD;
            }
        }
    return c;
}

Mat power(Mat base, ll n)
{
    Mat res;
    memset(res.a, 0, sizeof(res.a));
    for (int i = 0; i < 3; i++) res.a[i][i] = 1;  // 单位阵
    while (n > 0)
    {
        if (n & 1) res = mul(res, base);
        base = mul(base, base);
        n >>= 1;
    }
    return res;
}

int main()
{
    int T;
    cin >> T;
    Mat M = {{{1, 0, 1}, {1, 0, 0}, {0, 1, 0}}};
    while (T--)
    {
        ll n;
        cin >> n;
        if (n <= 3)                       // 前三项直接答 1
        {
            cout << 1 << endl;
            continue;
        }
        Mat r = power(M, n - 3);          // 从 (a3,a2,a1) 推到第 n 项
        // 结果向量第 0 分量 = a[n] = r 作用在初始向量 (1,1,1) 上的首行之和
        ll ans = (r.a[0][0] + r.a[0][1] + r.a[0][2]) % MOD;
        cout << ans << endl;
    }
    return 0;
}
// TAG: 矩阵DP 矩阵加速 自构转移矩阵 取模

练习

P2233[HNOI2002] 公交车路线8 个站点排成环 = 邻接矩阵 A(相邻两站连边)。定长路径计数:A^k 的第 (i,j) 项 = 从 i 走恰好 k 步到 j 的方案数。用矩阵快速幂求 A^n,读出起点到终点的方案数(注意不能提前到终点,需按题意处理)。在洛谷打开
P4159[SCOI2009] 迷路带边权(1~9)的定长路径计数。把每条权为 w 的边拆成 w 段、中间加虚拟点,化为 0/1 邻接矩阵(规模 9n×9n),再对邻接矩阵做矩阵快速幂求 T 时刻从起点到终点的方案数 mod 2009。拆点是关键技巧。在洛谷打开
P1707刷题比赛多条数列相互耦合的递推。把所有相关量塞进一个大状态向量,按题目给的耦合关系写出一个大转移矩阵,矩阵快速幂一并推进。核心是「多个序列 → 合并成一个高维状态 → 一个矩阵统一转移」。在洛谷打开

已进入 矩阵快速幂加速 · 矩阵 DP · DP大师