线性代数的知识还可以这么用
原题链接
解题思路
KMP
在 n < = 20 n<=20 n<=20的情况下,可以直接模拟求出每一秒的字符串,然后进行字符串匹配即KMP算法,但是只能拿到32分,n再大一些这么长的字符串就存不下了。
DP
不管怎么变,字符串都是由1,2,4,6这四个数字够成的,并且其实我们只需要统计各个数字的个数,而不是某一秒字符串的具体样子,那各个数字的数量的变化是否有规律,仔细观察变换规则:
- 1 -> 2
- 2 -> 4
- 4 -> 16
- 6 -> 64
可以想到下一秒各个数字的数目是和上一秒字符串中各数字的个数有关的,比如要求下一秒中数字4的个数,我们可以通过统计上一秒中数字2以及6的个数之和来得到。
将四个数字的个数放在一个向量A中,初始时字符串为1,那么该向量为: A = [ 1 , 0 , 0 , 0 ] A = [1,0,0,0] A=[1,0,0,0],下一秒将变为: A = [ 0 , 1 , 0 , 0 ] A = [0,1,0,0] A=[0,1,0,0]。
写得更一般些,假设某一秒字符串中
1
,
2
,
4
,
6
1,2,4,6
1,2,4,6的个数分别为
a
,
b
,
c
,
d
a, b, c, d
a,b,c,d,即:
A
=
[
a
,
b
,
c
,
d
]
A = [a,b,c,d]
A=[a,b,c,d]那么下一秒该向量将变换为:
A
=
[
c
,
a
,
b
+
d
,
c
+
d
]
A = [c, a, b+d, c+d]
A=[c,a,b+d,c+d]
显然这是一道动态规划,我们只需要根据该规律变换n次,就可以得到最终字符串中各个数字的个数。
神奇的dp矩阵
之前做动态规划都是构造dp数组,然后逐步填充,这次掌握了一个新的方法,太巧妙了,原来线性代数的用处这么大。
对于任何的线性变化,上面总结的就是一种线性关系,可以构造矩阵,将一次变换转化为一次矩阵相乘。
上面的线性变换就可以通过与下面的矩阵相乘完成:
[
a
,
b
,
c
,
d
]
⋅
[
0
1
0
0
0
0
1
0
1
0
0
1
0
0
1
1
]
=
[
c
,
a
,
b
+
d
,
c
+
d
]
[a,b,c,d] \cdot \left[ \begin{matrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0\\ 1 & 0 & 0 & 1\\ 0 & 0 & 1 & 1 \end{matrix} \right] = [c,a,b+d,c+d]
[a,b,c,d]⋅⎣⎢⎢⎡0010100001010011⎦⎥⎥⎤=[c,a,b+d,c+d]
那么用向量A乘DP矩阵就相当于对字符串进行了一次以2为底的幂运算(即一次变换),则结果就是向量A与该矩阵相乘n次。
A
⋅
d
p
⋅
d
p
.
.
.
⋅
d
p
=
A
⋅
d
p
n
A \cdot dp \cdot dp... \cdot dp = A\cdot dp^n
A⋅dp⋅dp...⋅dp=A⋅dpn
显然,我们可以先计算
d
p
n
dp^n
dpn,n次变换可以通过n次矩阵相乘完成!这其实是这道题给我的最大的收获,任何的线性变换都可以转换为矩阵相乘。
∣ S ∣ = 2 |S|=2 ∣S∣=2,两位数的情况
这是一段误入歧途浪费时间和经历的过程。
上面那种变换规则(
[
a
,
b
,
c
,
d
]
−
>
[
c
,
a
,
b
+
d
,
c
+
d
]
[a,b,c,d] -> [c,a,b+d,c+d]
[a,b,c,d]−>[c,a,b+d,c+d])最终只能得到1,2,4,6这四个数字的个数,如果要计算最终字符串中两位数的个数,就需要考虑两位数的变换。
1, 2, 4, 6总共能组成16个不同的两位数,对于每个两位数,写出其下一秒可能的变化形态。
可能的组合形式 | |||
---|---|---|---|
下一秒能变换出的数字 | |||
11 | 12 | 14 | 16 |
22 | 24 | 21, 16 | 26, 64 |
21 | 22 | 24 | 26 |
42 | 44 | 41, 16 | 46, 64 |
41 | 42 | 44 | 46 |
16, 62 | 16, 64 | 16, 61 | 16, 66, 64 |
61 | 62 | 64 | 66 |
64, 42 | 64, 44 | 64, 41, 16 | 64, 46 |
刚开始我认为总共是需要考虑16个数字的状态转移的,但是仔细观察发现其实并不是都要考虑的,因为有些组合是无法通过其他组合得到的,例如11这个组合的两位数,其他任何组合都无法变换出11,因此如果输入的串S是11,大可直接输出0。
将能够由其他组合变换出的(即真正需要考虑的组合)圈出来,也就是出现在白色行中的那些数字:
其中有些数字比如说21会比较独特,按理说它可以由14变换得到,但是14并不能通过其他数字变换得到,因此也不需要考虑。
∣ S ∣ < = 2 |S| <= 2 ∣S∣<=2,合并起来
当同时考虑一位和两位数的组合时DP矩阵应该做出改变,因为如果按照 ∣ S ∣ = 2 |S|=2 ∣S∣=2的表,是考虑了很多重复的变换(所以说是没什么用的考虑),比如:
- 判断16的数量,会认为其和41、42、44、46、64的数量有关系,但其实这都是因为其中有数字4才产生的,因此事实上16的数量只和上一个字符串中4的个数有关;
- 此外对于1来说,同样只需要统计上一个字符串中4的个数,其他有4的组合都不算能够直接且独立产生1;
- 还有64的产生情况比较特殊,6可以直接产生64,42也是可以直接产生64的,除了统计上一个字符串中6的个数,还应该统计其中42的个数;
即只考虑那些能够独立直接产生的变形,否则会造成重复,并且需要考虑的数字个数从10个变成了14个,(一位数的4个,以及两位数的10个):
可能的组合形式 | ||||||
---|---|---|---|---|---|---|
直接变换出的数字 | ||||||
1 | 2 | 4 | 6 | 16 | 26 | 41 |
2 | 4 | 16 | 64 | 26 | 46 | 62 |
42 | 44 | 46 | 61 | 62 | 64 | 66 |
64 | 61 | 66 | 42 | 44 | 41 | 46 |
对这14个数字分别以0~13编号,写出DP矩阵:(竖着纵向看,例如1的个数只和4的个数有关)
\ | 0(1) | 1(2) | 2(4) | 3(6) | 4(16) | 5(26) | 6(41) | 7(42) | 8(44) | 9(46) | 10(61) | 11(62) | 12(64) | 13(66) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0(1) | 1 | |||||||||||||
1(2) | 1 | |||||||||||||
2(4) | 1 | 1 | 1 | |||||||||||
3(6) | 1 | 1 | 1 | |||||||||||
4(16) | 1 | |||||||||||||
5(26) | 1 | |||||||||||||
6(41) | 1 | |||||||||||||
7(42) | 1 | |||||||||||||
8(44) | 1 | |||||||||||||
9(46) | 1 | |||||||||||||
10(61) | 1 | |||||||||||||
11(62) | 1 | |||||||||||||
12(64) | 1 | |||||||||||||
13(66) | 1 |
那么根据上表构造出DP矩阵:(matrix是一个成员函数只有二维数组的结构体,至于它为什么可以这样直接赋值,我也很迷茫
matrix dp =
{{
{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0}
}};
初始时字符串从1开始,即只有1(映射为0了)的位置为1,其余数的个数都为0,即: A 1 ∗ 14 = [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] A_{1*14} = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] A1∗14=[1,0,0,0,0,0,0,0,0,0,0,0,0,0]
用向量A乘DP矩阵就相当于对字符串进行了一次以2为底的幂运算(即一次变换),那么其实结果就是原来这14个数的个数够成的向量与该矩阵相乘n次。
A
⋅
d
p
⋅
d
p
.
.
.
⋅
d
p
=
A
⋅
d
p
n
A \cdot dp \cdot dp... \cdot dp = A\cdot dp^n
A⋅dp⋅dp...⋅dp=A⋅dpn
显然,我们可以先计算
d
p
n
dp^n
dpn,老师有提到这里可以利用线性代数的知识,然而我都忘完了,问张张他也不是很想搭理我的样子 ,但是总归是思路嘛:
于是我当然是选择直接进行矩阵相乘,和快速幂一样的写快速矩阵幂。
最后再用向量A乘该结果就是题目要求解答的了,由于A向量中只有下标为0处为1,则假设要统计的数字被映射为idx,那么结果自然就是 d p n [ 0 ] [ i d x ] dp^n[0][idx] dpn[0][idx]。
∣ S ∣ > 2 时 |S|>2时 ∣S∣>2时
当|S| > 2时,变换就太多了,无法通过构造dp矩阵来求解,此时老师有给过思路,然而我并没有明白这是什么意思:
源代码(dp, |S|<2, 96分)
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<map>
using namespace std;
const int MOD = 998244353;
const int maxn = 14; // 只考虑|S|<=2时的情况
struct matrix{
long long data[maxn][maxn];
};
// 初始化时将d构造为对角矩阵,充当“1”的作用,任何矩阵与它相乘结果为矩阵自身
matrix d;
map<int, int> mp;
int nums[maxn] = {1, 2, 4, 6, 16, 26, 41, 42, 44, 46, 61, 62, 64, 66};
void init(){
for(int i=0; i<maxn; i++){
// 主对角线上的元素均为1,其余元素均为0
d.data[i][i] = 1;
}
// 建立映射
for(int i=0; i<maxn; i++){
mp[nums[i]] = i;
}
}
matrix dot(matrix a, matrix b){
// 返回两个maxn*maxn的矩阵相乘的结果
matrix res;
for(int i=0; i<maxn; i++){
for(int j=0; j<maxn; j++){
long long tmp = 0;
for(int k=0; k<maxn; k++){
tmp += (a.data[i][k]*b.data[k][j])%MOD;
}
res.data[i][j] = tmp;
}
}
return res;
}
matrix quickPow(matrix m, int n){
// 返回矩阵幂方m^n的结果
matrix res = d;
while(n){
// 3 = 011
if(n&1){
res = dot(res, m);
}
m = dot(m, m);
n >>= 1;
}
return res;
}
void print(matrix m){
for(int i=0; i<maxn; i++){
for(int j=0; j<maxn; j++){
printf("%lld ", m.data[i][j]);
}
printf("\n");
}
}
int main(){
init();
int n;
scanf("%d", &n);
int str;
scanf("%d", &str);
/*
// 基本的函数测试
matrix test;
for(int i=0; i<maxn; i++){
for(int j=0; j<maxn; j++){
test.data[i][j] = rand()%3;
}
}
printf("test:\n");
print(test);
test = dot(d, test);
printf("d*test:\n");
print(test);
printf("test^2:\n");
test = quickPow(test, 2);
print(test);
*/
matrix dp =
{{
{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0}
}};
matrix dp_n = quickPow(dp, n);
// 返回1*maxn的矩阵和maxn*maxn矩阵乘积的结果
int idx = mp[str];
// [1, 0, ...]*dp_n = dp[0][idx]
printf("%lld\n", dp_n.data[0][idx]%MOD);
return 0;
}
心得体会
感觉我已经激动到说了好几次了,“线性变化就是一次矩阵相乘”,这大概也是线性代数在计算机中非常重要非常基础的原因吧,之前在Bilibili上看到3Blue1Brown的线性代数的本质–系列合集,就很受触动,它把矩阵相乘看成是一次空间变换,线性代数让我们能够通过几个简单的数字拥有操纵空间,进行线性变换的能力,绝对不单单是学会计算矩阵的运算,线性代数看起来☺️~