• 【矩阵性质】矩阵加速递推


    本文介绍线性代数中一个非常重要的内容——矩阵(Matrix)的一个重要性质:矩阵加速递推

    同时本文已经更新至:矩阵(Matrix)系统介绍篇


    斐波那契数列(Fibonacci Sequence)大家应该都非常的熟悉了。在斐波那契数列当中,(F_1 = F_2 = 1)(F_i = F_{i - 1} + F_{i - 2}(i geq 3))

    如果有一道题目让你求斐波那契数列第 (n) 项的值,最简单的方法莫过于直接递推了。但是如果 (n) 的范围达到了 (10^{18}) 级别,递推就不行了,稳 TLE。考虑矩阵加速递推。

    (Fib(n)) 表示一个 (1 imes 2) 的矩阵 (left[ egin{array}{ccc}F_n & F_{n-1} end{array} ight])。我们希望根据 (Fib(n-1)=left[ egin{array}{ccc}F_{n-1} & F_{n-2} end{array} ight]) 推出 (Fib(n))

    试推导一个矩阵 ( ext{base}),使 (Fib(n-1) imes ext{base} = Fib(n)),即 (left[egin{array}{ccc}F_{n-1} & F_{n-2}end{array} ight] imes ext{base} = left[ egin{array}{ccc}F_n & F_{n-1} end{array} ight])

    怎么推呢?因为 (F_n=F_{n-1}+F_{n-2}),所以 ( ext{base}) 矩阵第一列应该是 (left[egin{array}{ccc} 1 \ 1 end{array} ight]),这样在进行矩阵乘法运算的时候才能令 (F_{n-1})(F_{n-2}) 相加,从而得出 (F_n)。同理,为了得出 (F_{n-1}),矩阵 ( ext{base}) 的第二列应该为 (left[egin{array}{ccc} 1 \ 0 end{array} ight])

    综上所述:( ext{base} = left[egin{array}{ccc} 1 & 1 \ 1 & 0 end{array} ight]) 原式化为 (left[egin{array}{ccc}F_{n-1} & F_{n-2}end{array} ight] imes left[egin{array}{ccc} 1 & 1 \ 1 & 0 end{array} ight] = left[ egin{array}{ccc}F_n & F_{n-1} end{array} ight])

    转化为代码,应该怎么求呢?

    定义初始矩阵 ( ext{ans} = left[egin{array}{ccc}F_2 & F_1end{array} ight] = left[egin{array}{ccc}1 & 1end{array} ight], ext{base} = left[egin{array}{ccc} 1 & 1 \ 1 & 0 end{array} ight])。那么,(F_n) 就等于 ( ext{ans} imes ext{base}^{n-2}) 这个矩阵的第一行第一列元素,也就是 (left[egin{array}{ccc}1 & 1end{array} ight] imes left[egin{array}{ccc} 1 & 1 \ 1 & 0 end{array} ight]^{n-2}) 的第一行第一列元素。

    注意,矩阵乘法不满足交换律,所以一定不能写成 (left[egin{array}{ccc} 1 & 1 \ 1 & 0 end{array} ight]^{n-2} imes left[egin{array}{ccc}1 & 1end{array} ight]) 的第一行第一列元素。另外,对于 (n leq 2) 的情况,直接输出 (1) 即可,不需要执行矩阵快速幂。

    为什么要乘上 ( ext{base}) 矩阵的 (n-2) 次方而不是 (n) 次方呢?因为 (F_1, F_2) 是不需要进行矩阵乘法就能求的。也就是说,如果只进行一次乘法,就已经求出 (F_3) 了。如果还不是很理解为什么幂是 (n-2),建议手算一下。

    下面是求斐波那契数列第 (n) 项对 (10^9+7) 取模的示例代码(核心部分)。

    const int mod = 1000000007;
    
    struct Matrix {
        int a[3][3];
        Matrix() { memset(a, 0, sizeof a); }
        Matrix operator*(const Matrix &b) const {
            Matrix res;
            for (int i = 1; i <= 2; ++i)
                for (int j = 1; j <= 2; ++j)
                    for (int k = 1; k <= 2; ++k)
                        res.a[i][j] = (res.a[i][j] + a[i][k] * b.a[k][j]) % mod;
            return res;
        }
    } ans, base;
    
    void init() {
        base.a[1][1] = base.a[1][2] = base.a[2][1] = 1;
        ans.a[1][1] = ans.a[1][2] = 1;
    }
    
    void qpow(int b) {
        while (b) {
            if (b & 1) ans = ans * base;
            base = base * base;
            b >>= 1;
        }
    }
    
    int main() {
        int n = read();
        if (n <= 2) return puts("1"), 0;
        init();
        qpow(n - 2);
        println(ans.a[1][1] % mod);
    }
    

    这是一个稍微复杂一些的例子。

    [f_{1} = f_{2} = 0\ f_{n} = 7f_{n-1}+6f_{n-2}+5n+4 imes 3^n ]

    我们发现,(f_n)(f_{n-1}, f_{n-2}, n) 有关,于是考虑构造一个矩阵描述状态。

    但是发现如果矩阵仅有这三个元素 (egin{bmatrix}f_n& f_{n-1}& nend{bmatrix}) 是难以构造出转移方程的,因为乘方运算和 (+1) 无法用矩阵描述。

    于是考虑构造一个更大的矩阵。

    [egin{bmatrix}f_n& f_{n-1}& n& 3^n & 1end{bmatrix} ]

    我们希望构造一个递推矩阵可以转移到

    [egin{bmatrix} f_{n+1}& f_{n}& n+1& 3^{n+1} & 1 end{bmatrix} ]

    转移矩阵即为

    [egin{bmatrix} 7 & 1 & 0 & 0 & 0\ 6 & 0 & 0 & 0 & 0\ 5 & 0 & 1 & 0 & 0\ 12 & 0 & 0 & 3 & 0\ 5 & 0 & 1 & 0 & 1 end{bmatrix} ]


    有了上面的基础能很快做出 又见斐波那契

    图源来自网络上一位博主

    这个相比普通的斐波那契数列多了后面四项,看一眼数据范围,使用普通的 (O(n)) 的算法肯定会超时

    因此这里需要使用矩阵快速幂(斐波那契数列的项数n一旦过大,就要考虑快速幂,普通算法时间空间都开销太大)。

    使用矩阵快速幂的一个关键问题就是矩阵递推式。

    参考普通快速幂那一片博客最后面的那个扩展式,就可以得到下面这个递推式了:

    img

    然后通过计算等价替换可得出该矩阵A:

    img

    下面只需要把普通斐波那契数列的构造由2*2的矩阵换为6*6的即可。

    using ll = long long;
    const int mod = 1e9 + 7;
    const int N = 6;
    ll tmp[N][N], res[N][N];
    ll n;
    void mul(ll a[][N], ll b[][N]) {
        memset(tmp, 0, sizeof(tmp));
        for (int i = 0; i < N; ++i)
            for (int j = 0; j < N; ++j)
                for (int k = 0; k < N; ++k) {
                    tmp[i][j] += a[i][k] * b[k][j] % mod;
                    if (tmp[i][j] >= mod) tmp[i][j] -= mod;
                }
        for (int i = 0; i < N; ++i)
            for (int j = 0; j < N; ++j)a[i][j] = tmp[i][j];
    }
    void pow(ll a[][N]) {
        memset(res, 0, sizeof(res));
        for (int i = 0; i < N; ++i)res[i][i] = 1;
        for (; n; n >>= 1) {
            if (n & 1)mul(res, a);
            mul(a, a);
        }
    }
    void solve() {
        cin >> n, n--;
        ll ans[N][N] = { 1, 1, 1, 1, 1, 1,
                         1, 0, 0, 0, 0, 0,
                         0, 0, 1, 3, 3, 1,
                         0, 0, 0, 1, 2, 1,
                         0, 0, 0, 0, 1, 1,
                         0, 0, 0, 0, 0, 1
                       };
        pow(ans);
        ll sum = 0;
        ll x[N] = {1, 0, 8, 4, 2, 1};
        for (int i = 0; i < N; ++i) {
            sum += (res[0][i] * x[i]) % mod;
            if (sum >= mod) sum -= mod;
        }
        cout << sum << "
    ";
    }
    

    The desire of his soul is the prophecy of his fate
    你灵魂的欲望,是你命运的先知。

  • 相关阅读:
    js函数的属性和方法
    php中str_repeat函数
    html5中的空格符
    php实现简单算法3
    php intval函数
    什么是全栈工程师
    配置Log4j(非常具体)
    【解决】/usr/bin/ld: cannot find -lc
    Java的位运算符具体解释实例——与(&amp;)、非(~)、或(|)、异或(^)
    【小白的java成长系列】——顶级类Object源代码分析
  • 原文地址:https://www.cnblogs.com/RioTian/p/14772150.html
Copyright © 2020-2023  润新知