矩阵类优化 dp——矩阵加速优化
Idea
了解矩阵优化之前,我们不妨先来说说什么是矩阵。
如其名,矩阵就是按照长方形阵列排列的实数(也可以是复数)集合,类似于这样:
而我们在优化 dp 转移时主要用到的就是矩阵的乘法。
设我们有两个矩阵 ,若 和 之间可以作乘法,首先需要保证矩阵 的列数和矩阵 的行数相同。所以不妨设 的规模为 , 的规模为 ,若有 ,则 的规模为 ,且对于 有:
那么问题来了,为什么我们要把好端端的一个递推式变成一个矩阵进行转移呢?答案很简单,因为矩阵乘法可以用矩阵快速幂快速求解——矩阵乘法是满足结合律的!
也就是说,对于三个矩阵 ,是有 的。
这个其实很好证明,可以试着自己证明一下。(基本原理其实是加法满足对于乘法的分配律,且乘法满足结合律)
所以当转移递推式固定时,我们可以通过类似于快速幂的方式,设置好初始矩阵和转移矩阵后,通过矩阵快速幂将转移时关于阶段的一维复杂度优化为 log 级别。具体设置方式可以看下面例题。
值得扩展的一点时,注意只要我们的矩阵运算满足结合律就可以进行快速幂,而乘法和加法相结合的矩阵运算之所以会满足结合律,其实就是因为乘法本身具有结合律且加法对于乘法具有分配律。所以广义地来讲,只要我们的矩阵运算法则中,内层的运算具有结合律,外层的运算对于内层的运算具有分配律,且状态转移递推式恒定,再结合数据范围发现递推方程涉及到底前缀状态极少,转移阶段又十分大时,就可以考虑通过矩阵快速幂进行优化转移。(例如 min 和 +,or 和 and)
设矩阵规模为 ,转移阶段为 ,则一般时间复杂度为 。
Extension
1. 图上 dp 优化
考虑有经典问题:
给定一个图,边权均为 或 ,求从起点到终点的最短路径。
首先显然可以考虑通过 来正常做,复杂度应为 .
但由于边权的特殊性,我们完全可以建立一个双端队列,遇到 加队尾, 加队首,跑一遍 ,即可得到答案,从而去掉一个 ,时间复杂度 .
扩展成一个计数问题:
给定一个图,对于两个点之间要么不连边,要么边权为 ,求从起点出发长度为 到达终点的路径方案数( 极大,)。
考虑如果没有括号里的限制,显然可以用动态规划来做。
设 表示路径长度为 ,当前达到结点 的路径数目。则初始设 为 ,因为边权的特殊性,通过相邻边进行转移即可。时间复杂度 ,空间复杂度 .
而 是极大的, 的范围却是允许立方级别复杂度,自然就想到了矩阵加速优化。通过相临边的关系构造转移矩阵,用矩阵快速幂转移即可。时间复杂度 ,空间复杂度 。
若起点终点不固定,则改变初始化方式和最终统计方式即可,算法流程不变。
Problem
1. P1962 斐波那契数列
一道很基础的矩阵加速幂优化,就拿这道题来谈谈实现细节。
首先我们有一个固定的递推式,即:
也就是说,我们的转移需要用到前面两项的值,我们也只关心这两项。于是我们可以将初始矩阵状态设为最前面的两项,即 .
首先来考虑这个矩阵怎么变成 。设 ,则我们都知道 的哪些信息呢?
首先 的规模一定是 的,这个毋庸置疑。故设 ,考虑转移。
由矩阵乘法的规则可知,,所以 ;又由于 ,所以 。
即我们有:
扩展一下,若我们想得到向量 ,则可以有递推式:
此时输出向量的第一个元素即可。
矩阵加速幂函数如下:
void ksm(int k){//矩阵a的k次方
while (k){
if (k&1) ans=ans*a;
a=a*a;
k>>=1;
}
return ans;
}
重载矩阵运算符代码如下:
struct node{
ll s[N][N];//每一个结构体都代表一个矩阵
node(){
memset(s,0,sizeof(s));//自动初始化
}
}a,ans;
node operator *(const node&a,const node&b){//重载运算符板子
node zc;
for (int i=1;i<=3;i++)
for (int j=1;j<=3;j++)
for (int k=1;k<=3;k++)
zc.s[i][j]=(zc.s[i][j]+a.s[i][k]*b.s[k][j]%mod)%mod;
return zc;
}
一种更好理解的方式是,我们可以将转移矩阵 中第 行第 列的元素 看作是要将向量的第 个元素乘上 后赋值给新向量的第 个元素。注意矩阵的运算位置不能乱改。
2. P1707 刷题比赛
这个递推式和数据范围一看就很矩阵加速。
不过递推式涉及到的东西有点多,让我们来好好捋一捋。
首先 在转移过程中会互相涉及,所以我们显然是要同时转移这些变量的。
那么当我们的转移阶段为 时,都有哪些东西需要知道呢?
首先 肯定都是需要知道的,常数 也需要用到,还需要一个自增的 (每次转移时都从 那里给他一个 ),以及一个自增的 , 和 也是需要维护的。一共十一个变量。
所以我们的初始矩阵就为:
转移一步之后的矩阵为:
可以推出转移矩阵为:
简单来说就是,将变量设进向量里,将常量放进转移矩阵里,推出前两步后的初始矩阵向量后,通过他们之间的递推关系推出转移矩阵即可。
注意到本题模数最大为 ,直接做乘法应该会爆 long long,所以需要使用龟速乘。
时间复杂度 .
3. P4159 [SCOI2009] 迷路
给定一个有向图,边权为 到 ,问从起点到终点长度为 的路径数(,)。
注意到 极小, 极大,考虑转化为上面的经典问题进行解决。
发现和上面经典问题唯一的区别就是边权不再只有 了,而是变成了 到 ,但依然很小。于是我们很容易就想到,将边拆开,拆成边权 个点,对于每条边从起点到终点依次串联起拆成的点,如此以来,可以使得新图中边权均为 ,就转化为了上述经典问题。
可惜如此以来, 相当于最大变为了 左右,由于时间复杂度为 ,显然是过不了大数据的。
考虑优化。
首先肯定是要拆的,拆边肯定是不行的,而图上只有点和边,所以就考虑拆点。
考虑对于每一个点,向周围连的边边权均为 到 ,所以我们可以将每一个点拆为 个点,分别表示周围边权为 到周围边权为 。对于拆的点与外界的点,若与外界连的边边权为 ,则从代表边权为 的点与其代表边权为 的点连边;对于拆点内部,代表边权为 的点向代表边权为 的点连边。如此,可以保证当前图与原图是等价的。于是愉快地跑一边矩阵加速即可!
由于拆的节点最多只有 个,显然可以通过此题。
由此我们可以发现,拆点其实就是将原图改成一个与之等价的图,使得题目中的特殊限制被满足,从而达到可以套板子解决原问题的目的。而对于拆点的方式,可以考虑将边拆为点,也可以考虑将原点拆为若干个点,但应在满足题意的情况下,尽量拆更少的点,使得新图更加简单,从而降低复杂度。
4. P2151 [SDOI2009]HH去散步
给定一个无向图,边权均为 ,求起点到终点长度为 的路径数目(,,)。特殊限制是上一步走过的路下一步不能重复走。
经典问题的痕迹很明显,通过我们上一题的经验之谈,要做的就是通过拆点使得新图满足特殊限制。
由于边数其实也不多,就可以考虑将每个点拆成相邻边数个点,第 个点代表从第 条相邻的边走过来的点,然后向该点除了第 条边之外的边所连的点连一条单向边即可构造出新图。
但还没完。考虑对于起始点,可以向周围任意一个点前进,所以我们还可以额外设置一个 号点,向起始点周围的所有点连边,然后将 的初始值设为 即可跑矩阵加速。
对于每一条边,都会割出两个点,最多割出 个点,时间复杂度 ,实测效率较为优秀(单题最优解前五)。
由这道题其实也可以看出,割点其实就是将每个点的不同状态放大化,使其“人格分裂”,从而将【节点的转移】转化为【状态的转移】,从而达到实现满足特殊条件约束的目的。
5. P3176 [HAOI2015]数字串拆分
首先我们发现 数组是可以通过 dp 求出的。
不妨将拆分看作是给 个物品插板,两个木板之间的物品个数就是拆分出来的一个数。设 为在第 个物品后面插板之后所能得到的拆分数目,则显然,上一个木板可以放在往前的 个空隙当中的任何一个。即有转移方程:
开始时将 的初值设为 即可,表示开头一定会放上一块木板。
而观察到 很小,转移阶段又很大( 过于魔鬼),所以可以考虑矩阵加速优化转移。
但是显然还不够,因为 次方显然过于庞大了,甚至还需要用到高精,且 的拆分数目足足可以达到 种,这是不好的。考虑到 数组的拆分方式十分奇怪,思考一下题目中是否有什么厉害的性质可以让我们优化。
注意我们转移时用的是矩阵,而矩阵所满足的一个性质是:
正确性是很显然的。所以例如 ,我们就可以将它拆解为:
考虑递推求解 ,设 表示 前 位数字组成的数字串的答案(我们最终要求出 ),则显然当我们算到 时, 和 都已经算过了。于是可以将上面的式子进一步化简为:
所以扩展地来看,只要我们知道每一个后缀数字串的 值,就可以转移 ,得到 的答案了!
而为了避免高精,设 表示 第 位数字到第 位数字组成的 的转移矩阵,则通过 即可推出 ,所以预处理出所有 即可。其中 表示代表 中第 个数字的矩阵。
最后输出 即可。
6. CF576D Flights for Regular Customers
题目关心的只有走过的边数,相当于每条边的边权都为 。很熟悉的感觉。
又观察到数据范围。更熟悉了。矩阵加速的味道十分浓重。
如果将当前走过的边数看作时间戳,可以先思考一下若每条边的时间戳都为 ,即每条边一开始就存在时,该如何做。
显然,跑一边 bfs 即可解决。无解的情况很好判断,只要 号节点在边全都存在的情况下不能到达 号节点,则在任意时间戳上皆无法达到。
那么有解的时候呢?我们设 表示走到节点 时,长度为 的路径是否存在,则显然 可以通过 号节点的所有相邻节点 的 或运算过来。由于递推式子恒定,所以可以考虑矩阵乘法,并且用 与和或 代替 乘和加。
那有 该怎么做呢?考虑到将所有的 排序,则两个 之间的转移方程显然是不变的,我们在每到一个时间戳的时候停止转移,然后从目前所有能到的地方跑一边向终点跑一边多源 bfs,将 bfs 到的最短距离与目前已经走过的边数相加即为当前的答案,修改转移矩阵之后再继续转移即可。注意由于每个时间戳边数不同,所以先到达终点时的答案不一定最优,需要将每个时间戳上的答案全部算出来,取最小值就是答案。
时间复杂度 ,好像过不了。
观察到 值只有 和 ,所以用 bitset 优化转移即可,时间复杂度会乘上 ,差不多能过。
注意用 bitset 的写法需要将两个矩阵对齐,大概可以这么写:
node operator *(const node&x,const node&y){
node z;
for (int k=1;k<=n;k++)
for (int i=1;i<=n;i++)
if (x.a[i][k]) z.a[i]|=y.a[k];
return z;
}
7. P6569 [NOI Online #3 提高组] 魔法值
甚至状态和转移方程题目中都直接给出来了:
将 之外的节点与 的连边状态显式的写出来,设 表示 与 之间是否连边,则有:
所以我们可以设置一个 xor 和 乘法 的广义矩阵乘法。但是这个广义矩阵乘法是否满足条件了?
前面提到,广义矩阵乘法的设置需要满足两个条件:
-
内层运算要满足结合律——乘法显然满足 √
-
外层运算要对于内层运算满足分配律——异或对于乘法满足吗?
答案显然是否定的,反例随处可见,直觉上也并不符合。
那怎么办??这可不好转化啊!
别着急,别忘了我们的 只有 两种取值!在 状态下的矩阵乘法是否满足呢? 就消失, 就保留,当然满足。
此时时间复杂度为 ,还是不太行。
注意到一个细节,向量乘矩阵的时间复杂度其实可以是 的,而矩阵乘矩阵则需要 。我们在求解矩阵快速幂的时候就是因为要边算向量乘矩阵、边算矩阵乘矩阵,所以矩阵乘的时间复杂度才是 。如果我们将两者分开呢?
观察到我们是可以快速计算一个矩阵的 的整数次幂的,如果我们将所有的整数次幂矩阵都预处理出来,那么在询问的时候,我们就可以通过经典套路二进制拆分,只通过向量乘矩阵得到最终的答案向量。
故最终时间复杂度 。
启发性地讲,对于类似于 凑 的多次询问或需要多次用到 凑 ,且 求解二进制整数幂答案的时间复杂度较低,则可以考虑倍增+二进制拆分 求解。
8. P5059 中国象棋
注意到每一行都答案其实是相对独立且相同的,所以只需要算出一行的答案,即可得到整个棋盘的答案。
我们都关注哪些信息呢?阶段显然是设置成当前处理到了前 个格点,除此之外,我们显然还关心当前格点有没有放置卒且当前行一共有几个卒,这样才能使我们转移出的方案满足题目中给定的两个条件。
那么现在其实就有两种设置状态的选择:
- 设 表示处理完前 个格点的方案数,且第 个格点强制放置时的方案数,此时我们只需要枚举上一次放置的格点,注意一些细节即可同时满足两个条件。
- 设 表示处理完了前 个格点,且第 个格点 没有放置/放置棋子,本行目前 没有棋子/有一个棋子/棋子个数达到要求 时的方案数。则转移时考虑上一个格子的状态即可。
两个 dp 状态设置的区别在于一个强制选择而另一个不强制选择,正常来说应该是第一个 dp 更方便一些的,但是观察到此题的数据范围,,果断考虑矩阵加速优化,那么由于矩阵转移的特殊性,转移时决策显然是越少越好的,第一种的决策涉及到了前面的所有状态,显然无法写成矩阵,于是我们便采取了第二种状态设计方式。
对于每一个阶段,一共有六种不同的状态,所以我们设计一个 的矩阵即可优化转移。最后将答案 作一个 次幂即可。
注意到模数的取值范围,记得使用龟速乘。
时间复杂度 .
9. P3597 [POI2015] WYC
看到特殊边权,按照套路,先考虑拆点,但是我们显然不能将起点或者终点为拆得的点的路径算进答案,于是考虑换种思路。
由于 实在是太小了,所以不妨将其直接放入矩阵,设 表示的是到达节点 的长度为 的路径的条数,而 是可以由 转移过来的,于是我们将三维都设进矩阵,根据边权转移矩阵即可。除此之外,为了快速的计算小于等于当前矩阵阶段的路径个数,我们可以再开一维矩阵表示当前的总和,每次转移时将新产生的路径条数全部加到这一维度当中即可。
最后计算时考虑到矩阵快速幂倍增的特殊形式,可以考虑用倍增代替二分,算出最终的答案。
不过注意此题十分毒瘤,个别数据最后可能会爆 long long,所以建议使用 int128 解决本题。
10. P6772 [NOI2020] 美食家
有了前面几道题的铺垫,这道题显得就不是那么难了。先考虑没有美食节该怎么做。
首先依然采用上一题的套路,按照边权写成一个 规模的矩阵。若设 表示在当前阶段到达 的最大预约值, 为合法决策,则有:
而 max 满足结合律,加法具有对于 max 的分配律,所以可以由此定义广义矩阵乘法。
那么加上美食节该怎么做呢?
考虑对于每次美食节中断一次转移,特殊转移一遍美食节矩阵之后,再继续正常转移,但这样每两次美食节之间的转移都是 的,总时间复杂度就是 。
所以考虑使用 T7 的套路预处理出每一个二进制整数次幂的矩阵,两次美食节之间转移时直接调用即可,由于向量乘矩阵的优秀复杂度,总时间复杂度就被我们优化到了 。
但是注意转换矩阵乘法规则之后不要思维定势,矩阵赋值项设为 而非 ,无用项设为不可能值(-INF)而非 ,矩阵的初始化设为不可能值(-INF)而非 ,以及一些细节问题。
Exercise
P2886 [USACO07NOV]Cow Relays G 将最短路和矩阵加速相结合,用 min + 替换 + *
P1397 [NOI2013] 矩阵游戏 行内转移,行间转移,预处理出一位的 次方矩阵,按照十进制拼接起来即可。
AT2371 [AGC013E] Placing Squares
Ending
总结下来,矩阵优化 dp 的特征其实非常鲜明,只要看完数据范围基本就可以确定此题是否可以矩阵优化。
具体来说,当我们观察数据范围时,发现:
- 如果 dp 式子可以写成矩阵,且矩阵规模较小。
- 转移阶段过大,正常复杂度无法承担。
那么我们就完全可以考虑矩阵优化 dp。
而矩阵优化 dp 的主要用途无非两种:
- 优化转移递推式。
- 优化边权较小图上路径统计问题。
列矩阵时,基本步骤如下:
-
详细列出题目中的 dp 式子,判断广义矩阵乘法的形式。
-
找出 dp 式子中的变量与常量,并溯源每个变量的求解递推式,将变量列进初始矩阵,常量列进转移矩阵,矩阵加速幂优化转移即可。
以及转移过程中的一些 trick:
-
若题目中有一些关键节点会改变转移矩阵,考虑到向量乘矩阵和矩阵乘矩阵的不同复杂度,可以考虑先预处理出转移矩阵的二进制整数幂,再对于每个关键节点“暂停转移”,依次处理。
-
若发现模数超过 long long 范围,记得使用龟速乘。
-
如果转移矩阵都为 构成,则可以考虑使用 bitset 优化。
-
若图上的转移涉及多个时间戳,则可以类似于拆点将每一个时间戳的该点都列出转移矩阵。
-
阶段较大时,十进制转二进制会非常麻烦,则可以考虑预处理十进制下 的转移矩阵,在十进制下直接转移即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】