数学/数论专题-专项训练:矩阵相关#1
1. 前言
本篇文章是作者学习矩阵的时候的一些相关训练。
注意作者是个 OIer,因此并不会涉及到线性代数知识(或者说是很少)。
前置知识:矩阵快速幂
2. 题单
题单:
P5343 【XR-1】分块
显然这道题并不是分块
首先我们需要预处理出来有哪些块长是 xht37 可以分的,这里记作 \(a\) 数组,其大小为 \(cnt\),注意去重。
那么接下来考虑 60pts 的部分分。
设 \(f_i\) 表示将前 \(i\) 个数按照所给块长分块的方案数,那么我们有转移方程:
初值:\(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}\),那么我们只需要计算以下结果:
然后取出第 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,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 方程然后矩阵优化。