快速幂运算是一种利用位运算和DP思想求的\(x^n\)的数值算法,它将时间复杂度\(O(n)\)降到\(O(log(n))\)。快速幂运算结合矩阵乘法,可以巧解不少DP问题。本篇会由浅入深,从最基本的快速幂运算算法,到应用矩阵快速幂运算解DP问题,结合三道Leetcode题目来具体讲解。
Leetcode 50. Pow(x, n) (Medium)
Leetcode 50. Pow(x, n) 是实数的快速幂运算问题,题目如下。
Implement pow(x, n), which calculates x raised to the power n (i.e. \(x^n\)).
Example 1:
1 | Input: x = 2.00000, n = 10 |
Example 2:
1 | Input: x = 2.10000, n = 3 |
Example 3:
1 | Input: x = 2.00000, n = -2 |
快速幂运算解法分析
假设n是32位的int类型,将n写成二进制形式,那么n可以写成最多32个某位为 1(第k位为1则值为\(2^k\))的和。那么\(x^n\)最多可以由32个 \(x^{2^k}\)的乘积组合,例如:
\[ x^{\text{10011101}_{2}} = x^{1} \times x^{\text{100}_{2}} \times x^{\text{1000}_{2}} \times x^{\text{10000}_{2}} \times x^{\text{10000000}_{2}} \]
快速幂运算的特点就是通过32次循环,每次循环根据上轮\(x^{2^k}\)的值进行平方后得出这一轮的值:\(x^{2^k} \times x^{2^k} = x^{2^{k+1}}\),即循环计算出如下数列
\[ x^{1}, x^2=x^{\text{10}_{2}}, x^4=x^{\text{100}_{2}}, x^8=x^{\text{1000}_{2}}, x^{16}=x^{\text{10000}_{2}}, ..., x^{128} = x^{\text{10000000}_{2}} \]
在循环时,如果n的二进制形式在本轮对应的位的值是1,则将这次结果累乘计入最终结果。
下面是python 3 的代码,由于循环为32次,所以容易看出算法复杂度为 \(O(log(n))\)。
1 | # AC |
对应的 Java 的代码。
1 | // AC |
矩阵快速幂运算
快速幂运算也可以应用到计算矩阵的幂,即上面的x从实数变为方形矩阵。实现上,矩阵的幂需要矩阵乘法:$ A_{r c} B_{c p}$ ,Python中可以用numpy的 np.matmul(A, B)来完成,而Java版本中我们手动实现简单的矩阵相乘算法,从三重循环看出其算法复杂度为\(O(r \times c \times p)\)。
1 | public int[][] matrixProd(int[][] A, int[][] B) { |
Leetcode 509. Fibonacci Number (Easy)
有了快速矩阵幂运算,我们来看看如何具体解题。Fibonacci问题作为最基本的DP问题,在上一篇Leetcode 679 24 Game 的 Python 函数式实现中我们用python独有的yield来巧解,这次再拿它来做演示。
The Fibonacci numbers, commonly denoted F(n) form a sequence, called the Fibonacci sequence, such that each number is the sum of the two preceding ones, starting from 0 and 1. That is,
1 | F(0) = 0, F(1) = 1 |
Given N, calculate F(N).
Example 1:
1 | Input: 2 |
Example 2:
1 | Input: 3 |
Example 3:
1 | Input: 4 |
转换为矩阵幂运算
Fibonacci的二阶递推式如下:\[ \begin{align*} F(n) =& F(n-1) + F(n-2) \\ F(n-1) =& F(n-1) \end{align*} \]
\[ \begin{bmatrix}F(n)\\F(n-1)\end{bmatrix} = \begin{bmatrix}1 & 1\\1 & 0\end{bmatrix} \begin{bmatrix}F(n-1)\\F(n-2)\end{bmatrix} \]
\[ \begin{bmatrix}F(n)\\F(n-1)\end{bmatrix} = \begin{bmatrix}1 & 1\\1 & 0\end{bmatrix}^{n-1} \begin{bmatrix}F(1)\\F(0)\end{bmatrix} \]
AC代码
有了上面的矩阵幂公式,代码稍作改动即可。Java 版本代码。
1 | /** |
Python 3的numpy.matmul() 版本代码。
1 | # AC |
或者也可以直接调用numpy.matrix_power() 代替手动的快速矩阵幂运算。
1 | # AC |
Leetcode 1411. Number of Ways to Paint N × 3 Grid (Hard)
下面来看一道稍难一点的DP问题,1411. Number of Ways to Paint N × 3 Grid。
You have a grid
of size n x 3
and you want
to paint each cell of the grid with exactly one of the three colours:
Red, Yellow or Green
while making sure that no two adjacent cells have the same colour (i.e
no two cells that share vertical or horizontal sides have the same
colour).
You are given n
the number of rows of the grid.
Return the number of ways you can paint this
grid
. As the answer may grow large, the answer must
be computed modulo 10^9 + 7
.
Example 1:
1 | Input: n = 1 |
Example 2:
1 | Input: n = 2 |
Example 3:
1 | Input: n = 3 |
Example 4:
1 | Input: n = 7 |
Example 5:
1 | Input: n = 5000 |
标准DP解法
分析题目容易发现第i行的状态只取决于第i-1行的状态,第i行会有两种不同状态:三种颜色都有或者只有两种颜色。这个问题容易识别出是经典的双状态DP问题,那么我们定义dp2[i]为第i行只有两种颜色的数量,dp3[i]为第i行有三种颜色的数量。
先考虑dp3[i]和i-1行的关系。假设第i行包含3种颜色,即dp3[i],假设具体颜色为红,绿,黄,若i-1行包含两种颜色(即dp2[i-1]),此时dp2[i-1]只有以下2种可能:
还是dp3[i] 红,绿,黄情况,若i-1行包含三种颜色(从dp3[i-1]转移过来),此时dp3[i-1]也只有以下2种可能:因此,dp3[i]= dp2[i-1] * 2 + dp3[i-1] * 2。
同理,若第i行包含两种颜色,即dp2[i],假设具体颜色为绿,黄,绿,若i-1行是两种颜色(dp2[i-1]),此时dp2[i-1]有如下3种可能:
dp2[i]的另一种情况是由dp3[i-1]转移过来,则dp3[i-1]有2种可能,枚举如下:因此,dp2[i] = dp2[i-1] * 3 + dp3[i-1] * 2。 初始值dp2[1] = 6,dp3[1] = 6,最终答案为dp2[i] + dp3[i]。
很容易写出普通DP版本的Python 3代码,时间复杂度为\(O(n)\)。
1 | # AC |
快速矩阵幂运算解法
和Fibonacci一样,我们将DP状态转移方程转换成矩阵乘法:\[ \begin{bmatrix}dp2(n)\\dp3(n)\end{bmatrix} = \begin{bmatrix}3 & 2\\2 & 2\end{bmatrix} \begin{bmatrix}dp2(n-1)\\dp3(n-1)\end{bmatrix} \]
\[ \begin{bmatrix}dp2(n)\\dp3(n)\end{bmatrix} = \begin{bmatrix}3 & 2\\2 & 2\end{bmatrix}^{n-1}\begin{bmatrix}6\\6\end{bmatrix} \]
代码几乎和Fibonacci一模一样,仅仅多了mod 计算。下面是Java版本。
1 |
|
Python 3实现为
1 | # AC |
评论
shortname
for Disqus. Please set it in_config.yml
.