给定一个序列或网格,需要找到序列中某个/些子序列或网格中的某条路径。
动态规划方程dp[i]中的下标i表示以a[i]为结尾的满足条件的子序列的性质,dp[i][j]中的下标i,j表示以格子(i,j)为结尾的满足条件的路径的性质。坐标型动态规划的初始条件dp[0]是指以a[0]为结尾的子序列的性质。
1. uniquePathII
题目描述:给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者向右走一步;网格中有些地方有障碍,机器人不能通过障碍格,问有多少种不同的方式走到右下角。输入中第一行中有两个数字m,n,表示网格的行数和列数,接下来是m行是网格的表示,其中1表示障碍,0表示无障碍;最后输出为走到右下角的不同方式数。
input : 0 0 0 0 0 1 0 0 output:26
0 0 0 1 0 0 0 0
0 0 0 0 0 1 0 0
0 0 0 0 0 0 0 0
1. 确定状态:
1.1 最后一步:
这道题和uniquePathI的相似度很高,只不过是在网格中加了障碍。那么最后一步还是有两种情况,从右下角左边的格子(m-1,n-2)向右走到(m-1,n-1)处;从右下角上边的格子(m-2,n-1)向下走到(m-1,n-1)处。
1.2 子问题:
这样原问题走到右下角的不同方式数转化为规模更小的子问题走到位置(m-1,n-2)处不同方式数和走到位置(m-2,n-1)处不同方式数。
状态dp[i][j] = 走到位置(i,j)处的不同方式数
2. 转移方程:
设状态dp[i][j] = 走到位置(i,j)处的不同方式数,那么对于任意的(i,j)有:
3. 初始条件和边界情况:
初始条件:dp[0][0] = 1,即有一种方式到达左上角
边界情况:当i=0或j=0时,前一步只能由一个方向过来,即第一行中只能从左往右走,第一列中只能从上往下走,所以有dp[i][j] = 1;此外,当位置(i,j)处为障碍时,不能到达此位置,输出为0
4. 计算顺序:
按照从上到下,从左到右的顺序计算。由初始条件dp[0][0] = 0,计算:
第0行:dp[0][0],dp[0][1],dp[0,2],...,dp[0][n-1]
第1行:dp[1][0],dp[1][1],dp[1][2],...,dp[1][n-1]
...
第m-1行:dp[m-1][0],dp[m-1][1],dp[m-1][3],...,dp[m-1][n-1]
结果:dp[m-1][n-1]
时间复杂度:O(mn),空间复杂度(数组大小):O(mn)
5. 代码实现:
def uniquePathII(array)->int:
'''
input: array:要走的网格
output: dp[m-1][n-1]:走到右下角的方式数
'''
m = len(array) # 获取网格的行数
if m == 0:
return 0
n = len(array[0]) # 获取网格的列数
if n == 0:
return 0
# 创建一个m行n列的数组用于存储达到每个位置处的方式数,默认值为0
dp = [[0]*n for i in range(m)]
for i in range(m):
for j in range(n):
# 障碍用1表示,若(i,j)处为障碍,则dp[i][j] = 0
if array[i][1] == 1:
dp[i][j] = 0
else:
# 初始条件和边界情况
if i == 0 or j == 0:
dp[i][j] = 1
else:
# 转移方程
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
if __name__ == '__main__':
array = [[0,0,0,0,0,1,0,0],
[0,0,0,1,0,0,0,0],
[0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,0]]
print(uniquePathII(array))
2. Longest Continuous Monotone Subsequence
题目描述:给定a[0],...,a[n-1],找到最长的连续子序列i,i+1,i+2,...,j,使得a[i]<a[i+1]<...<a[j],或者a[i]>a[i+1]>...a[j],输出长度j-i+1
input: 5 1 2 3 4 output:4 (子序列1,2,3,4)
input: 5 4 2 1 3 output:4 (子序列5,4,2,1)
分析:题目中要求的是最长连续单调子序列的长度,也就是既要求最长连续上升子序列又要求最长连续下降子序列,那么我们可以只求最长连续上升子序列,然后再反转数组,再求一次最长连续上升子序列(即原数组的最长连续下降子序列)即可。
1. 确定状态:
1.1 最后一步:
对于最优策略一定存在最后一个元素a[j],由最后一个元素可以分解出两种情况:
case1:最优策略中最长连续上升子序列就是{a[j]},即序列的长度为1
case2:子序列的长度大于1,那么一定会存在a[j]的前一个元素,a[j-1],且有a[j-1]<a[j]
1.2 子问题:
这样原问题以a[j]结尾的最长连续上升子序列的长度转化为规模更小的子问题以a[j-1]结尾的最长连续上升子序列的长度。
状态dp[j] = 以a[j]结尾的最长连续上升子序列的长度
2. 转移方程:
设状态dp[j] = 以a[j]结尾的最长连续上升子序列的长度,那么有:
3. 初始条件和边界情况:
初始条件:空
边界情况:j>0,即保证a[j]的前面至少还有一个元素a[j-1];a[j-1]<a[j],保证序列是上升的
4. 计算顺序:
计算dp[0],dp[1],...,dp[n-1]
结果:max{dp[0],dp[1],...,dp[n-1]}(因为不确定最长子序列在何处结尾,所以结果并不一定是dp[n-1])
时间复杂度:O(n),空间复杂度:O(n)
5. 代码实现:
result = 0 # 定义全局变量result
def longestContinuousMonotoneSubsequence(array)->int:
n = len(array)
if n == 0:
return 0
def calc(array,n):
# 定义函数calc计算最长连续上升子序列的长度
global result
dp = [1] * n # 创建长度为n的数组,默认值为1
for i in range(n):
# 若满足边界条件,dp[i] = dp[i-1]+1
if i > 0 and array[i-1] < array[i]:
dp[i] = dp[i-1] + 1
result = max(dp[i],result)
calc(array,n) # 求最长连续上升子序列的长度
array.reverse() # 反转数组
calc(array,n) # 求最长连续下降子序列的长度
return result
if __name__ == '__main__':
print(longestContinuousMonotoneSubsequence([5,1,2,3,4]))
3. Minimum Path Sum
题目描述:给定m行n列的网格,每个格子(i,j)里都有一个非负数A[i][j],求一个从左上角(0,0)到右下角的路径,每一步只能向下或者向右走一步,使得路径上的格子里的数字之和最小,输出最小数字之和。
input: output:22(1+4+7+3+2+3+2)
1 5 7 6 8
4 7 4 4 9
10 3 2 3 2
1. 确定状态:
1.1 最后一步:
无论怎么走,最后一步只有两个选择,即从(m-1,n-2)向右移动到右下角或从(m-2,n-1)向下移动到右下角。
1.2 子问题:
这样原问题走到右下角格子中的数字之和最小值转化为规模更小的子问题走到位置(m-2,n-1)处格子中的数字之和最小值或走到位置(m-1,n-2)处格子中的数字之和最小值。
状态dp[i][j] = 走到位置(i,j)处的路径上的格子中的数字之和的最小值
2. 转移方程:
设状态dp[i][j] = 走到位置(i,j)处的路径上的格子中的数字之和的最小值,那么有:
即取从当前位置(i,j)左边走过的路径中的数字的最小值和从当前位置(i,j)上边走过的路径中的数字的最小值这二者中的最小值,然后加上当前格子中的数字即为dp[i][j]
3. 初始条件和边界情况:
初始条件:dp[0][0] = A[0][0]
边界情况:i=0或j=0时,走到当前位置(i,j)只有一个方向。
4. 计算顺序:
依旧是从上到下,从左到右依次计算。
结果:dp[m-1][n-1]
时间复杂度:O(MN),空间复杂度:O(MN)
5. 空间优化:
可以看到dp[i][j]的值,只与其上一行有关,所以我们只需要保存两行的dp值,即dp[i][0,...,n-1]和dp[i-1][0,...,n-1]。故在初始化的时候我们不需要开m*n的数组,而只需要开一个2*n的数组。这个数组叫做滚动数组。那么具体怎么做呢?
开一个2*n的数组,第一行中存储dp[0][0,...,n-1],第二行中存储dp[1][0,...,n-1],计算得到dp[2][0,...,n-1]后,dp[0][0,...,n-1]就没用了,这时可以将dp[2][0,...,n-1]存到第一行,把dp[0][0,...,n-1]覆盖掉,依次类推,把dp[3][0,...,n-1]存到第二行将dp[1][0,...,n-1]覆盖掉。
最后dp[m-1][n-1]会存储在dp[0][n-1]或dp[1][n-1]中。经过空间优化后,算法的空间复杂度为O(N)
6. 代码实现:
def minimumPathSum(array)->int:
'''
input: array: 含有数字的网格
output: dp[(m-1)%2][n-1]:到达右下角的最小值
'''
m = len(array) # 获取网格的行数
n = len(array[0]) # 获取网格的列数
if m == 0 or n == 0:
return 0
# 创建一个2行n列的数组
dp = [[0]*n for i in range(2)]
for i in range(m):
for j in range(n):
# 初始条件
if i == 0 and j == 0:
dp[i%2][j] = array[i][j]
continue
# 从左边的格子右移到(i%2,j)处
if i > 0:
temp1 = dp[(i%2)-1][j]
else:
temp1 = float('inf')
# 从上面的格子下移到(i%2,j)处
if j > 0:
temp2 = dp[i%2][j-1]
else:
temp2 = float('inf')
# 取temp1和temp2中的最小值加上当前位置的数
dp[i%2][j] = array[i][j] + min(temp1,temp2)
return dp[(m-1)%2][n-1]
if __name__ = '__main__':
array = [[1,5,7,6,8],
[4,7,4,4,9],
[10,3,2,3,2]]
print(minimumPathSum(array))
4. Bomb Enemy
题目描述:有一个M*N的网格,每个格子可能是空的,可能有一个敌人,可能有一堵墙,只能在某个空格子里放一个炸弹,炸弹会炸死所有同行同列的敌人,但是不能穿透墙,最多能炸死几个敌人。
input: 0 E 0 0 output: 3 (即在第二行第二列处放置炸弹最多可炸死3个敌人)
E 0(B) W E
0 E 0 0(其中E表示敌人,B表示炸弹,W表示墙)
注:先分析炸弹沿着一个方向能炸死的敌人数,同样的方式再分析炸弹沿着其他方向能炸死的敌人数,最后把各个方向的结果相加即可。以炸弹向上炸为例,然后再分析下、左、右三个方向。
1. 确定状态:
假设有敌人或者有墙的格子也能放置炸弹:
有敌人的格子:格子里的敌人都被炸死,并继续向上爆炸;有墙的格子:炸弹不能炸死任何敌人。
1.1 最后一步:
在(i,j)处放一个炸弹,它向上能炸死的敌人数分三种情况是:
case1: (i,j)格为空地,那么为(i-1,j)格向上能炸死的敌人数
case2:(i,j)格为敌人,那么为(i-1,j)格向上能炸死的敌人数+1
case3:(i,j)格为墙,那么能炸死的敌人数为0
1.2 子问题:
这样原问题在(i,j)处放置炸弹向上最多能炸死多少敌人转化为规模更小的子问题在(i-1,j)处放置炸弹向上最多能炸死多少敌人。
状态Up[i][j] = 在(i,j)处放置炸弹向上最多能炸死的敌人数。
2. 转移方程:
设状态Up[i][j] = 在(i,j)处放置炸弹向上最多能炸死的敌人数。
3. 初始条件和边界情况:
初始条件:第0行的up值和格子内容相关:Up[0][j] = 0,若(0,j)格不是敌人;Up[0][j] = 1,若(0,j)格是敌人。
4. 计算顺序:
按照从上到下,从左到右的顺序计算。计算出Up[i][j]后可以通过类似的方法计算出Down[i][j],Left[i][j],Right[i][j],最后将四个方向能最多能炸死的敌人数相加即:Up[i][j]+Down[i][j]+Left[i][j]+Right[i][j]。
结果:取每个位置放置炸弹炸死的敌人最多数
时间复杂度:O(MN) 空间复杂度:O(MN)
5. 代码实现:
def bombEnemy(array)->int:
m = len(array) # 获取网格的行数
n = len(array[0]) # 获取网格的列数
if m == 0 or n == 0:
return 0
dp = [[0]*n for i in range(m)]
res = [[0]*n for i in range(m)]
# up
for i in range(m):
for j in range(n):
# 若当前格子为墙,则能炸死0个敌人
if array[i][j] == 'W':
dp[i][j] = 0
else:
dp[i][j] = 0
# 若当前格子为E,则能炸死1个敌人
if array[i][j] == 'E':
dp[i][j] = 1
# 若i>0,dp[i][j] += dp[i-1][j]
if i > 0:
dp[i][j] += dp[i-1][j]
res[i][j] += dp[i][j]
# down
for i in range(m-1,-1,-1):
for j in range(n):
# 若当前格子为墙,则能炸死0个敌人
if array[i][j] == 'W':
dp[i][j] = 0
else:
dp[i][j] = 0
# 若当前格子为E,则能炸死1个敌人
if array[i][j] == 'E':
dp[i][j] = 1
# 若i+1<m,dp[i][j] += dp[i+1][j]
if i+1 < m:
dp[i][j] += dp[i+1][j]
res[i][j] += dp[i][j]
# left
for i in range(m):
for j in range(n):
# 若当前格子为墙,则能炸死0个敌人
if array[i][j] == 'W':
dp[i][j] = 0
else:
dp[i][j] = 0
# 若当前格子为E,则能炸死1个敌人
if array[i][j] == 'E':
dp[i][j] = 1
# 若j>0,dp[i][j] += dp[i][j-1]
if j > 0:
dp[i][j] += dp[i][j-1]
res[i][j] += dp[i][j]
# right
for i in range(m):
for j in range(n-1,-1,-1):
# 若当前格子为墙,则能炸死0个敌人
if array[i][j] == 'W':
dp[i][j] = 0
else:
dp[i][j] = 0
# 若当前格子为E,则能炸死1个敌人
if array[i][j] == 'E':
dp[i][j] = 1
# 若j+1<n,dp[i][j] += dp[i][j+1]
if j+1 < n:
dp[i][j] += dp[i][j+1]
res[i][j] += dp[i][j]
result = 0
for i in range(m):
for j in range(n):
# 取格子中数为'0'的所有res的最大值
if array[i][j] == '0':
if res[i][j] > result:
result = res[i][j]
return result
if __name__ == '__main__':
array = [['0','E','0','0'],
['E','0','W','0'],
['0','E','0','0']]
print(bombEnemy(array))
5. Longest Increasing Subsequence
题目描述:给定a[0],...,a[n-1],找到最长的子序列,使得,输出K。
input:[4,2,4,5,3,7] output:4(子序列2,4,5,7)
注:这道题解法和第2道题相似,要比第二道题简单,但是要注意和第二道题的区别,此题中的上升子序列不一定是连续的。
1. 确定状态:
1.1 最后一步:
最优策略中一定存在最后一个元素a[j],对于a[j]可以分为两种情况:
case1:最优策略中最长上升子序列就是{a[j]},即序列的长度为1
case2:子序列的长度大于1,那么一定会存在a[j]的前一个元素,a[i],且有a[i]<a[j](i<j,但a[i]和a[j]不一定相邻)
1.2 子问题:
原问题以a[j]结尾的最长上升子序列的长度转化为规模更小(i<j)的子问题以a[i]结尾的最长上升子序列的长度
状态dp[j] = 以a[j]结尾的最长上升子序列的长度。
2. 转移方程:
设状态为dp[j] = 以a[j]结尾的最长上升子序列的长度,那么有:
3. 初始条件和边界情况:
初始条件:空
边界情况:i>=0,即保证a[j]的前面至少还有一个元素a[i];a[i]<a[j],保证序列是上升的
4. 计算顺序:
计算dp[0],dp[1],...,dp[n-1]
结果:max{dp[0],dp[1],...,dp[n-1]}(因为不确定最长子序列在何处结尾,所以结果并不一定是dp[n-1])
时间复杂度:O(n),空间复杂度:O(n)
5. 代码实现:
def longestIncreasingSubsquence(array)->int:
n = len(array)
if n == 0:
return 0
dp = [1]*n # 创建长度为n的数组,默认值为1
for j in range(n):
for i in range(j):
# 边界情况
if i >= 0 and array[j] > array[i]:
# 转移方程
dp[j] = dp[i] + 1
return max(dp)
if __name__ == '__main__':
array = [4,2,4,5,3,7]
print(longestIncreasingSubsquence(array))
6. Russian Doll Envelopes
题目描述:给定N个信封的长度和宽度,如果一个信封袋的长和宽都分别小于另一个信封的长和宽,则这个信封可以放入另一个信封。问最多嵌套多少个信封。
input:[[5,4],[6,4],[6,7],[2,3]] output:3([2,3]=>[5,4]=>[6,7])
1. 确定状态:
1.1 最后一步:
1.2 子问题:
2. 转移方程:
3. 初始条件和边界情况:
4. 计算顺序: