本期任务:介绍算法中关于动态规划思想的几个经典问题
一、问题描述
假设我们有一个n乘以n的矩阵w[n][n]。矩阵存储的都是正整数。
棋子起始位置在左上角,终止位置在右下角。我们将棋子从左上角移动到右下角。每次只能向右或者向下移动一位。
从左上角到右下角,会有很多不同的路径可以走。我们把每条路径经过的数字加起来看作路径的长度。
那从左上角移动到右下角的最短路径长度是多少呢?
输入:矩阵行数n,矩阵M
4
1 3 5 9
2 1 3 4
5 2 6 7
6 8 4 3
输出:从(0, 0)到(3, 3)的最短路径长度
19
二、算法思路
本题的解法与数字三角形问题大同小异,可以阅读【算法】【动态规划篇】第3节:数字三角形问题,加深对此类问题的理解。
1. 策略选择
一个模型:
- 数字矩阵问题是典型的“多阶段决策最优解”问题,如下图所示,整个决策过程分为6个阶段,每个阶段决策是向下还是向右;最优解是从(0, 0)到(3, 3)的最短路径长度
三个特征:
- 重复子问题:
- 不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
- 如图,本题中
到
有多条路径
- 无后效性:
- 最优子结构:
- 后面阶段的状态可以通过前面阶段的状态推导出。
- 本题中,我们把从起始位置(0, 0)到(i, j)的最小路径,记作min_dist(i, j)。因为我们只能往右或往下移动,所以,我们只有可能从(i, j-1)或者(i-1, j)两个位置到达(i, j)。也就是说,到达(i, j)的最短路径要么经过(i, j-1),要么经过(i-1, j),而且到达(i, j)的最短路径肯定包含到达这两个位置的最短路径之一。换句话说就是,min_dist(i, j)可以通过min_dist(i, j-1)和min_dist(i-1, j)两个状态推导出来。这就说明,这个问题符合“最优子结构”。
综上所述,本问题满足一个模型、三个特征,所以可以使用动态规划来求解。
当然,凡是能用动态规划解决的问题,都可以用回溯思想来暴力求解,具体实现代码文末已给出,更多关于回溯思想的应用,可以参照:【算法】【回溯篇】第7节:0-1背包问题
2. 动态规划算法思路
动态规划使用的流程:自顶向下分析问题,自底向上解决问题!
- 使用与原矩阵规模一致的二维维数组来保存从 到 的最短路径长度。
- 更新过程(状态转移思路):
- 状态更新过程:
三、Python代码实现
1. 动态规划解法
class Solution():
def dp(self, n, arr):
"""
使用动态规划法求解数字矩阵问题
:param n: 矩阵规模n,
:param arr: 矩阵M
:return: 从(0, 0)到(3, 3)的最短路径长度
"""
dp_arr = [[0 for j in range(n)] for i in range(n)] # 维护一个二维数组用来存储max_dist(i,j)
dp_arr[0][0] = arr[0][0] # 初始化首个元素
for i in range(1, n):
dp_arr[i][0] += arr[i][0] + dp_arr[i - 1][0] # 初始化第一列元素
dp_arr[0][i] += arr[0][i] + dp_arr[0][i - 1] # 初始化第一行元素
# 填写各阶段重复子问题的最优解
for i in range(1, n):
for j in range(1, n):
# (0, 0)到(i, j)的最短路径为:当前位置的值+(0, 0)到上一轮两个父节点的最短路径的较小者
dp_arr[i][j] = arr[i][j] + min(dp_arr[i - 1][j], dp_arr[i][j - 1])
return dp_arr[n - 1][n - 1] # 返回右下角元素值
def main():
n = 4
arr = [[1, 3, 5, 9], [2, 1, 3, 4], [5, 2, 6, 7], [6, 8, 4, 3]]
client = Solution()
print(client.dp(n, arr))
if __name__ == '__main__':
main()
运行结果:
19
2. 回溯解法
class Solution():
def trackback(self, n, arr):
"""
使用回溯法求解数字矩阵问题
:param n: 矩阵规模n,
:param arr: 矩阵M
:return: 从(0, 0)到(3, 3)的最短路径长度
"""
self.size = n
self.arr = arr
self.min_v = int(1e+5)
self.res = dict() # 用来优化重复子问题
self.helper(0, 0, 1)
return self.min_v
def helper(self, index_i, index_j, value):
if index_i == self.size - 1 and index_j == self.size - 1: # 当遍历到右下角时进行结算
if self.min_v > value:
self.min_v = value
return
if index_i + 1 < self.size: # 避免行方向越界访问
i, j, v = index_i + 1, index_j, value + self.arr[index_i + 1][index_j]
if self.res.get((i, j)) == None or v < self.res[(i, j)]: # 优化重复子问题,只有比现有value的值小的情况才有再计算意义
self.res[(i, j)] = v
self.helper(i, j, v)
if index_j + 1 < self.size: # 避免列方向越界访问
i, j, v = index_i, index_j + 1, value + self.arr[index_i][index_j + 1]
if self.res.get((i, j)) == None or v < self.res[(i, j)]: # 优化重复子问题,只有比现有value的值小的情况才有再计算意义
self.res[(i, j)] = v
self.helper(i, j, v)
def main():
n = 4
arr = [[1, 3, 5, 9], [2, 1, 3, 4], [5, 2, 6, 7], [6, 8, 4, 3]]
client = Solution()
print(client.trackback(n, arr))
if __name__ == '__main__':
main()
参考
http://gk.link/a/102op