数学/数论专题-专项训练:矩阵相关#1

1. 前言

本篇文章是作者学习矩阵的时候的一些相关训练。

注意作者是个 OIer,因此并不会涉及到线性代数知识(或者说是很少)。

前置知识:矩阵快速幂

2. 题单

题单:

P5343 【XR-1】分块

显然这道题并不是分块

首先我们需要预处理出来有哪些块长是 xht37 可以分的,这里记作 \(a\) 数组,其大小为 \(cnt\)注意去重


那么接下来考虑 60pts 的部分分。

\(f_i\) 表示将前 \(i\) 个数按照所给块长分块的方案数,那么我们有转移方程:

\[f_{i}=\sum_{1 \leq j \leq cnt,i-a_{j} \geq 0}f_{i-a_j} \]

初值:\(f_0=1\)

这个转移方程是比较容易推的,有 CSP-J 一等的实力就能推出来。

发现这个复杂度是 \(O(n)\),60pts 到手。


那么如何处理 \(n \leq 10^{18}\) 的数据呢?

仔细看看这个转移方程,我们发现实际上该方程是线性转移的,因此可以采用矩阵来优化。

什么是线性转移?线性转移就是满足 \(f_i=\sum_{j满足某条件}f_j\) 这样的式子,换言之就是有个通式。

由于 \(x \leq 100\),所以首先考虑暴力求出 \(f_{1...100}\)

然后构造一个矩阵 \(\begin{bmatrix}f_1\\f_2\\\dots\\f_{100}\end{bmatrix}\)

那么如何转移呢?

我们需要从 \(\begin{bmatrix}f_k\\f_{k+1}\\\dots\\f_{k+99}\end{bmatrix}\) 转移到 \(\begin{bmatrix}f_{k+1}\\f_{k+2}\\\dots\\f_{k+100}\end{bmatrix}\),那么前 99 行需要原封不动的转移,对于第 100 行的转移我们需要根据 \(a_i\) 构造线性递推式转移。

具体而言就是这样(转移矩阵为 \(Base\)):

对于 \(i \in[1,99]\)\(Base_{i+1,i}=1\)

对于 \(i=100\)\(Base_{100,100-a_j+1}=1|j \in[1,cnt]\)

其余均为 0。

这样就可以转移啦~

不理解的读者可以通过样例手造矩阵理解一下qwq

我们的最终目的是 \(f_{n}\),那么我们只需要计算以下结果:

\[\begin{bmatrix}f_1\\f_2\\\dots\\f_{100}\end{bmatrix} \times Base^{n-99} \]

然后取出第 100 行的项就可以了。

注意对于 \(n \leq 100\) 的时候直接输出 \(f_i\) 即可。

复杂度为 \(O(100^3 \log n)\)


Code:

/*
========= Plozia =========
    Author:Plozia
    Problem:P5343 【XR-1】分块
    Date:2021/6/10
========= Plozia =========
*/

#include <bits/stdc++.h>
#define int long long

typedef long long LL;
const int MAXN = 100 + 10, P = 1e9 + 7;
int a[MAXN], PR, aPR[MAXN], NF, aNF[MAXN], cnt, f[MAXN];
LL n;
struct Matrix
{
    int a[MAXN][MAXN], r, c;
    void init()
    {
        memset(a, 0, sizeof(a));
        for (int i = 1; i <= r; ++i)
            a[i][i] = 1;
    }
    Matrix operator *(const Matrix &fir)
    {
        Matrix tmp; memset(tmp.a, 0, sizeof(tmp.a));
        tmp.r = r; tmp.c = fir.c;
        for (int i = 1; i <= r; ++i)
            for (int k = 1; k <= c; ++k)
            {
                int t = a[i][k];
                for (int j = 1; j <= fir.c; ++j)
                    { tmp.a[i][j] += t * fir.a[k][j]; tmp.a[i][j] %= P; }
            }
        return tmp;
    }
}Base;

int Read()
{
    int sum = 0, fh = 1; char ch = getchar();
    for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
    for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = sum * 10 + ch - '0';
    return sum * fh;
}

void init()
{
    int l = 1, r = 1;
    while (l <= PR && r <= NF)
    {
        if (aPR[l] == aNF[r]) { ++l; ++r; ++cnt; a[cnt] = aPR[l - 1]; }
        else if (aPR[l] > aNF[r]) ++r;
        else ++l;
    }
    cnt = std::unique(a + 1, a + cnt + 1) - (a + 1);
    f[0] = 1;
    for (int i = 1; i <= 100; ++i)
        for (int j = 1; j <= cnt; ++j)
        {
            if (i < a[j]) continue ;
            f[i] += f[i - a[j]]; f[i] %= P;
        }
}

Matrix ksm(Matrix fir, LL sec, LL P)
{
    Matrix ans; ans.r = ans.c = fir.r; ans.init();
    for (; sec; sec >>= 1, fir = fir * fir)
        if (sec & 1) ans = ans * fir;
    return ans;
}

signed main()
{
    scanf("%lld", &n); PR = Read();
    for (int i = 1; i <= PR; ++i) aPR[i] = Read();
    NF = Read();
    for (int i = 1; i <= NF; ++i) aNF[i] = Read();
    std::sort(aPR + 1, aPR + PR + 1);
    std::sort(aNF + 1, aNF + NF + 1);
    init(); Base.r = Base.c = 100;
    memset(Base.a, 0, sizeof(Base.a));
    for (int i = 1; i <= 99; ++i) Base.a[i + 1][i] = 1;
    for (int i = 1; i <= cnt; ++i) Base.a[100 - a[i] + 1][100] = 1;
    if (n <= 100) { printf("%d\n", f[n]); return 0; }
    n -= 100; Matrix ans = ksm(Base, n, P);
    Matrix d; memset(d.a, 0, sizeof(d.a));
    d.r = 1; d.c = 100;
    for (int i = 1; i <= 100; ++i) d.a[1][i] = f[i];
    d = d * ans; printf("%d\n", d.a[1][100]); return 0;
}

P5789 [TJOI2017]可乐(数据加强版)

这道题有一道数据弱化版,切了这道题的各位可以顺便把弱化版给切了。

做这道题首先需要知道一个结论:

设一张无向图 \(G=<V,E>\)\(A\) 是其邻接矩阵表示是否连通,设 \(k \in N_+\),那么做矩阵快速幂 \(A^k\) 之后 \(A_{i,j}\) 表示 \(k\) 步后 \(i \to j\) 有多少种方案。

证明可以考虑设 \(f_{i,j,k}\)\(k\) 步后 \(i \to j\) 的方案数,那么有转移方程:

\[f_{i,j,k}=\sum f_{i,v,k-1} \times f_{v,j,k-1} \]

我们发现这就是矩阵乘法的定义式,而 \(f_{i,j,1}\) 就是邻接矩阵。

于是这就等价于 \(A^k\)


考虑题目中的三种走法:

  • 移动到相邻的城市。

这个直接在邻接矩阵里面存好了。

  • 停在原地。

每个点向自己连边。

  • 自爆。

建立一个自爆节点 \(n+1\)\([1,n]\) 内的所有点往 \(n+1\) 连边,相当于走到这个点就不能走了,也就是自爆。

注意:\(n+1\) 不能向自己连边。

连边之后求出 \(A^t\),答案就是 \(\sum_{j \in[1,n+1]} A^t_{1,j}\)


Code:

/*
========= Plozia =========
    Author:Plozia
    Problem:P5789 [TJOI2017]可乐(数据加强版)
    Date:2021/6/11
========= Plozia =========
*/

#include <bits/stdc++.h>

typedef long long LL;
const int MAXN = 100 + 5, P = 2017;
int n, m, t;

struct Matrix
{
    LL a[MAXN][MAXN];
    int r, c;
    void init()
    {
        memset(a, 0, sizeof(a));
        for (int i = 1; i <= r; ++i) a[i][i] = 1;
    }
    Matrix operator *(const Matrix &fir)
    {
        Matrix tmp; memset(tmp.a, 0, sizeof(tmp.a));
        tmp.r = r; tmp.c = fir.c;
        for (int i = 1; i <= r; ++i)
            for (int k = 1; k <= c; ++k)
            {
                LL t = a[i][k];
                for (int j = 1; j <= fir.c; ++j)
                    { ((tmp.a[i][j] += t * fir.a[k][j]) >= P) ? (tmp.a[i][j] %= P) : 0; }
            }
        return tmp;
    }
}Base;

int Read()
{
    int sum = 0, fh = 1; char ch = getchar();
    for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
    for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
    return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }

Matrix ksm(Matrix fir, LL sec)
{
    Matrix ans; ans.r = ans.c = fir.r; ans.init();
    for (; sec; sec >>= 1, fir = fir * fir)
        if (sec & 1) ans = ans * fir;
    return ans;
}

int main()
{
    n = Read(), m = Read();
    Base.r = Base.c = n + 1; Base.init();
    for (int i = 1; i <= m; ++i)
    {
        int x = Read(), y = Read();
        Base.a[x][y] = Base.a[y][x] = 1;
    }
    for (int i = 1; i <= n; ++i) Base.a[i][n + 1] = 1;
    t = Read(); Base = ksm(Base, t);
    LL ans = 0;
    for (int i = 1; i <= n + 1; ++i) ans += Base.a[1][i];
    ans %= P; printf("%lld\n", ans); return 0;
}

P5337 [TJOI2019]甲苯先生的字符串

这道题跟上一道题是一样的。

考虑将 \([a,z]\) 内的字母看作 26 个节点,建立邻接矩阵,题目中说的不能相邻就是不能连边,求出 \(A^{k-1}\),然后求出 \(\sum A^{k-1}_{i,j}\) 即可。

注意指数是 \(k-1\),因为实际上我们只需要走 \(k-1\) 次。

Code:

/*
========= Plozia =========
    Author:Plozia
    Problem:P5337 [TJOI2019]甲苯先生的字符串
    Date:2021/6/11
    Remarks:字符串长度为 n 意思是走 n - 1 步而不是 n 步
========= Plozia =========
*/

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

typedef long long LL;
const int MAXN = 26 + 5, P = 1e9 + 7;
LL n;
string str;
struct Matrix
{
    LL a[MAXN][MAXN];
    int r, c;
    void init()
    {
        memset(a, 0, sizeof(a));
        for (int i = 1; i <= r; ++i) a[i][i] = 1;
    }
    Matrix operator *(const Matrix &fir)
    {
        Matrix ans; ans.r = r; ans.c = fir.c; memset(ans.a, 0, sizeof(ans.a));
        for (int i = 1; i <= r; ++i)
            for (int k = 1; k <= c; ++k)
            {
                LL t = a[i][k];
                for (int j = 1; j <= fir.c; ++j)
                { ((ans.a[i][j] += t * fir.a[k][j]) >= P) ? (ans.a[i][j] %= P) : 0; }
            }
        return ans;
    }
}Base;

LL Read()
{
    LL sum = 0, fh = 1; char ch = getchar();
    for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
    for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
    return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }

Matrix ksm(Matrix a, LL b)
{
    Matrix ans; ans.r = ans.c = a.r; ans.init();
    for (; b; b >>= 1, a = a * a)
        if (b & 1) ans = ans * a;
    return ans;
}

int main()
{
    n = Read(); std::cin >> str;
    Base.r = Base.c = 26;
    for (int i = 1; i <= 26; ++i)
        for (int j = 1; j <= 26; ++j)
            Base.a[i][j] = 1;
    for (int i = 0; i + 1 < str.size(); ++i)
    {
        if (str[i] == '\r' || str[i] == '\n') continue ;
        Base.a[str[i] - 'a' + 1][str[i + 1] - 'a' + 1] = 0;
    }
    Base = ksm(Base, n - 1);
    LL ans = 0;
    for (int i = 1; i <= 26; ++i)
        for (int j = 1; j <= 26; ++j)
            ((ans += Base.a[i][j]) >= P) ? (ans -= P) : 0;
    printf("%lld\n", ans); return 0;
}

3. 总结

本篇博文的题都比较套路,就是推 DP 方程然后矩阵优化。

posted @ 2022-04-17 17:48  Plozia  阅读(66)  评论(0编辑  收藏  举报