有限制的随机数(矩阵状态机)
作者:
| 更新日期:之前分享了动态规划的递归和递推解法,这里分享下矩阵状态机优化解法。
本文首发于公众号:天空的代码世界,微信号:tiankonguse
一、背景
之前在《Leetcode 第 158 场比赛回顾》文章中,第三题介绍了动态规划的递归解法。
后来在《有限制的掷骰子(递推版)》分享了递推的实现,然后使用预处理进行时间优化,使用滚动数组进行空间优化。
在递推的解法中,提出了一个很重要的结论:状态转移方程的状态n
只与状态n-1
有关系。
就是这样一个结论,里面暗含着另外一个更高级的做法:矩阵状态机优化法。
二、回顾
题意是有n
次机会来随机生成数字,每次随机出的数字是1~6
。
但是有个限制,相同的数字连续出现的次数有个限制。
问最终生成的合法的数字序列有多少种。
我们也推理出了一个递归状态:f(k, index, n)
,代表数字index
在前面出现k
次时,再掷n
次骰子会出现多少序列。
这个状态可以使用递归实现非常简单。
其实,上篇文章我少说一句,那就是使用递推实现的时候,状态f(k, index, n)
的含义是掷n
次骰子后,以k
个index
为后缀的序列个数。
由于是后缀的个数,所以我们需要求和统计所有后缀的个数。
三、矩阵状态机
现在我们聚焦在n
与n-1
两组状态上,则发现状态的关系是固定的。
如上图,状态f(1,1)
可能来自所有的f(x, ~1)
,而状态f(2, 1)
只可能来自于f(1, 1)
。
使用几何意义来表示就是:
第n+1
轮的f(1,1)
,只能来自第n
轮的不以1
为后缀的所有状态答案之和。
第n+1
轮的f(2,1)
,只能来自第n
轮的f(1,1)
的状态答案。
这些状态之间的关系实际上可以使用0/1
矩阵表示,然后通过矩阵运算就可以得到下一轮所有状态的答案了。
为了简单起见,这里假设只有2
个数字,最多都只能出现2
次。
则有四个状态,分别编号为0~3
。
0: f(1, 1)
1: f(2, 1)
2: f(1, 2)
3: f(2, 2)
则状态机的关系是下图
| 0 1 2 3
- - - - - -
0 | 0 1 1 0
1 | 0 0 1 0
2 | 1 0 0 1
3 | 1 0 0 0
矩阵(i, j)
为1
时含义为状态i
能够到达状态j
。
这个矩阵竖着按列看比较容易理解。
比如状态0
和状态2
,可以看出来相同数字后缀的状态都是0
,不同数字后缀的状态都是1
而对于状态1
和状态3
,则只有一个值是1
,那就是状态i
的下一个状态,即满足j=i+1
的时候才为1
。
有了这个矩阵状态机,我们就可以快速求出任意次数后,各状态的个数了。
例如起始状态中,后缀只出现一个数字的状态值为1
。
原因也很简单,只随机出一个值。
| 0 1 2 3
- - - - - -
0 | 1 0 1 0
随机第二次的时候,起始状态乘以状态矩阵,就可以得出第二次的结果了。
如上图,随机第三次,第四次都可以很快的计算出结果。
所以,想求第n
次的结果,起始状态乘n-1
次这个矩阵状态机就行了。
四、矩阵幂优化
我们知道,矩阵满足结合律,即(A * B) * C
等价于A * (B * C)
。
所以起始矩阵乘以n-1
个矩阵状态机,等价于乘以矩阵状态机的幂,即先求出矩阵状态机的幂。
对于幂的运算,可以通过二进制优化来加速运算,这样n
个矩阵相乘,我们就可以优化为log(n)
个矩阵相乘了。
理论大概如下:
2 ^ 15
= 2 ^ (1 + 2 + 4 + 8)
= 2^1 * 2^2 * 2^4 * 2^8
2^8 = 2^4 * 2^4
2^4 = 2^2 * 2^2
2^2 = 2^1 * 2^1
本来需要乘15
次矩阵的,这里通过乘2*log(15)
次计算出结果来了。
空间复杂度:(6*15) ^ 2
时间复杂度:log(n) * (6*15) ^ 3
五、最后
根据时间复杂度可以发现,这个矩阵状态机的方法并不比前面的递归和递推更优。
原因是次数n
太小,log
效果不明显,而转化为矩阵后,状态比较多,矩阵运算复杂度也比较高。
这样就导致这道题的耗时比较高。
但是这个方法可以学习一下,如果次数n
改成一亿,前两个方法都不行了,但是这个方法可以轻松计算出答案来。
总结下就是满足三个条件时可以使用矩阵状态机:
1)当前状态只与上一个状态有关系
2)状态的维度很小
3)状态转移的次数很大时,递推会超时
关于矩阵状态机,你学会了吗?
-EOF-
本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号ID:tiankonguse-code
本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。