一 矩阵链乘法问题
1.1 基本知识
1.1 两个矩阵的乘法复杂度:
当两个矩阵A(p,q),B(q,r)进行乘法时,C(p,r) = AxB,时间复杂度是O(pqr)
。
1.2 三个及多个矩阵的复杂度:
从下图看出三个矩阵进行乘法时,在不同位置进行结合,他们之间的复杂度不同!
那么当有多个矩阵进行乘法时,是否能找到计算效率最高的方法?
也就是让一下问题:
如何确定相乘顺序(给矩阵链加括号),提高计算效率?
1.2 矩阵链乘法问题
由以上的铺垫,下面直接给出矩阵链乘法问题的动态规划转移方程。
1) 动态规划转移方程:
2) 初始化
对dp table初始化,𝒊 = 𝒋时,矩阵链只有一个矩阵,乘法次数为0。由于是求最小值问题,其余先初始化为inf
。
3)确定计算顺序
由dp table可看出,最终的解依赖子问题的最优解,因此计算顺序是自底向上计算。最终的最优解是dp[0][n]
,就是右上角位置。对于这种问题,又可以分为两个方法来计算。 #但这两种方法时间复杂度都是O(n^3)
(1):斜线遍历计算
(2):从下到上从左到右遍历计算
(1):斜线遍历计算
(2):从下到上从左到右遍历计算
1.3 代码实现
(1)动态规划:斜线遍历计算
class Solution2: # 时间复杂度O(n^3)
def MatrixChainMultiply(self, p, n):
D = [[float('inf')]*(n) for _ in range(n)]
rec = [[0]*(n) for _ in range(n)]
for i in range(n):
D[i][i] = 0
# 动态规划, 相当于斜着遍历!
for l in range(2, n+1): # 区间长度从小到大
# 以此计算子问题
for i in range(0, n-l+1):
j = i + l - 1
# print(i,j)
for k in range(i, j): # 枚举所有分割位置
# 计算最少乘法次数
q = D[i][k] + D[k + 1][j] + p[i - 1] * p[k] * p[j]
D[i][j] = min(D[i][j], q)
return D[0][n-1], D
if __name__ == "__main__":
p = [2, 3, 7, 9, 5, 2, 4, 8, 10, 12, 6, 5]
n = len(p) - 1
count, D = Solution2().MatrixChainMultiply(p, n)
print("--------无决策记录+斜线遍历--------")
print("最少计算次数:", count)
# for i in D:
# print(i)
--------无决策记录+斜线遍历--------
最少计算次数: 954
(2)动态规划:下上->左右遍历
class Solution2: # 时间复杂度O(n^3)
def MatrixChainMultiply(self, p, n):
D = [[float('inf')]*(n) for _ in range(n)]
rec = [[0]*(n) for _ in range(n)]
for i in range(n):
D[i][i] = 0
# 动态规划,下上->左右遍历!
for i in range(n-2, -1,-1): # i从下到上遍历,倒数第二行开始
for j in range(i, n): # j从左到右遍历
# print(i,j)
for k in range(i, j): # 枚举所有分割位置
# 计算最少乘法次数
q = D[i][k] + D[k + 1][j] + p[i - 1] * p[k] * p[j]
D[i][j] = min(D[i][j], q)
return D[0][n-1], D
if __name__ == "__main__":
p = [2, 3, 7, 9, 5, 2, 4, 8, 10, 12, 6, 5]
n = len(p) - 1
count, D = Solution2().MatrixChainMultiply(p, n)
print("--------无决策记录+下上->左右遍历--------")
print("最少计算次数:", count)
# for i in D:
# print(i)
输出结果
--------无决策记录+下上->左右遍历--------
最少计算次数: 954
(3) 最优方案追踪
下面这种方法是输出具体是如何增加进行加括号的方法。只需增加rec数组,然后进行方案追踪。更为详细的过程请看:算法设计与分析—童咏昕 https://www.icourse163.org/learn/BUAA-1449777166?tid=1463474515#/learn/content?type=detail&id=1241406029&sm=1
# 矩阵链乘法问题
class Solution: # 时间复杂度O(n^3)
def MatrixChainMultiply(self, p, n):
D = [[float('inf')]*(n) for _ in range(n)]
rec = [[0]*(n) for _ in range(n)]
for i in range(n):
D[i][i] = 0
# 动态规划
for l in range(2, n+1): # 区间长度从小到大
# 以此计算子问题
for i in range(0, n-l+1):
j = i + l - 1
# print(i,j)
for k in range(i, j): # 枚举所有分割位置
# 计算最少乘法次数
q = D[i][k] + D[k+1][j] + p[i-1]*p[k]*p[j]
if q < D[i][j]:
D[i][j] = q
rec[i][j] = k # 记录最优决策
return D[0][n-1], rec, D
# 最优方案追踪
# 输入矩阵链U1_6,追踪数组rec,位置索引i,j
# 输出矩阵链的加括号方式
def printMatrixChain(U, rec, i, j):
if i == j:
print(U[i], end='')
return
print("(", end='')
printMatrixChain(U, rec, i, rec[i][j])
print(")(", end='')
printMatrixChain(U, rec, rec[i][j] + 1, j)
print(")", end='')
return
if __name__ == "__main__":
p = [2, 3, 7, 9, 5, 2, 4, 8, 10, 12, 6, 5]
n = len(p) - 1
count, rec, D = Solution().MatrixChainMultiply(p, n)
print("最少计算次数:", count)
# for i in D:
# print(i)
print("--------最优方案--------")
U = '1234567890a' # 每个字符相当于一个矩阵
i, j = 0, n - 1
printMatrixChain(U, rec, i, j)
print()
输出结果
最少计算次数: 954
--------最优方案--------
(1)(((((((2)((3)((4)((5)(6)))))(7))(8))(9))(0))(a))
二 LeetCode题
2.1 题目描述
2.2 代码实现
通过对矩阵链乘法问题的学习,戳气球就能迎刃而解了。需要注意的是,这里的状态转移方程与前面稍微有点差别:
戳气球:dp[i][j] = max(dp[i][j],dp[i][k] + dp[k][j] +points[i] * points[j] *points[k]
而矩阵链的是:D[i][j] = min(D[i][j], D[i][k] + D[k + 1][j] + p[i - 1] * p[k] * p[j])
。
这里不是说他们求问题的解不同(一个是最小值,一个是最大值),当然这也是一个方面,而主要是:他们的子问题有点不同。
戳气球的子问题是dp[i][k]
与dp[k][j]
;
矩阵链是:D[i][k]
与D[k + 1][j]
。
就是他们后面一项不同,这里要注意。所以在遍历计算dp table的时候就要注意细节。计算顺序同样也是自底向上,分为斜线遍历与从下到上从左到右遍历。
(1)动态规划:斜线遍历
class Solution:
def maxCoins(self, nums):
ns = [1] + nums + [1]
N = len(ns)
dp = [[0] * N for _ in range(N)]
for l in range(N):
for i in range(N-l):
j = i + l
for k in range(i+1,j):
dp[i][j] = max(dp[i][j], \
dp[i][k]+dp[k][j]+ns[i]*ns[k]*ns[j])
return dp[0][-1]
输出结果
------戳气球-------
167
力扣提交结果:
(2) 动态规划:从下到上从左到右遍历
class Solution:
def maxCoins(self, nums):
n = len(nums)
# 添加两侧的虚拟气球
points = [1] + nums + [1]
# base case初始化为0
dp = [[0] * (n+2) for _ in range(n+2)]
# 开始状态转移,
for i in range(n, -1, -1): # i从下往上遍历,倒数第二行开始遍历
for j in range(i+1, n+2): # j从左往右遍历
# 戳破的气球的位置
for k in range(i+1, j):
# 择优做选择
dp[i][j] = max(
dp[i][j],
dp[i][k] + dp[k][j] +
points[i] * points[j] * points[k])
return dp[0][-1], dp
nums = [3,1,5,8]
print("------戳气球-------")
coins, dp = Solution().maxCoins(nums)
print(coins)
for row in dp:
print(row)
输出结果
------戳气球-------
167
力扣提交结果:
参考文章&资料
- 算法设计与分析—童咏昕 https://www.icourse163.org/learn/BUAA-1449777166?tid=1463474515#/learn/content?type=detail&id=1241406029&sm=1
- https://leetcode-cn.com/problems/burst-balloons/
- https://mp.weixin.qq.com/s/I0yo0XZamm-jMpG-_B3G8g
- https://leetcode-cn.com/problems/burst-balloons/solution/yi-wen-tuan-mie-qu-jian-dp-by-bnrzzvnepe-2k7b/