#动态规划#
初学动态规划
动态规划的核心:
1.构建状态转移方程,建立dp[i]与dp[i-1],甚至是dp[i-2]之间的关系。
2.确定边界条件
一维动态规划
打家劫舍:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
本题的解法用到了动态规划的思想
1.建立状态转移方程
每一个房间,都有两种选择:偷或者不偷。假设偷了第i个房间,那么第i-1个房间就不能偷了,而没偷第i个房间,那么第i-1个房间仍是可以行窃。也就是说,从第一个房间到第i个房间最大的行窃的财富可能是两部分的其中之一,1.偷窃了第i房间,当前的财富等于第i个房间的财富加上偷窃到前i-2个房间的财富的总和,2.没有偷窃第i个房间的情况下,当前的财富等于偷窃到前i-1个房间的总和。用数学表达式来说明的话,可以写成dp[i]=max(dp[i-2]+nums[i],dp[i-1]),当然,有人可能会疑惑,为什么不是dp[i-3],dp[i-4]呢?ok,从这个递推式中可以看到,dp[i-2],dp[i-1]已经包含了dp[i-3],以及之前的那些状态了。
2.边界条件
由于dp[i]=max(dp[i-2]+nums[i],dp[i-1]),i应该是大于等于2的整数,因此,缺失了dp[0]和dp[1]这两个状态,dp[0]表示,只有第一间房子,求最大的财富,那么必然是nums[0]了,而dp[1]前两个房间,我们可以获得的最大财富值,由于不能同时对相邻两个房间进行行窃,所以,只能取两个房间的较大值,dp[1]=max(nums[1],nums[0])。
class Solution:
def rob(self, nums):
length =len(nums)
if length==0:
return 0
elif length==1:
return nums[0]
else:
dp =[0 for _ in range(length)]
dp[0] =nums[0]
dp[1] =max(nums[0],nums[1])
for i in range(2,length):
dp[i] =max(dp[i-1],dp[i-2]+nums[i])
max_profit =max(dp[length-1],dp[length-2])
return max_profit
if __name__ == '__main__':
example = Solution()
nums =[1,2,3,1]
res=example.rob(nums)
print(res)
nums =[2,7,9,3,1]
res=example.rob(nums)
print(res)
nums =[1]
res=example.rob(nums)
print(res)
二维动态规划
不同路径
:在mxn的网格中,每次只能向右或者向下移动一步,求解从左上到右下一共有多少条不同的路径
由于题目限制机器人只能向两个方向移动,右和下,所以我们可以根据这个条件来建立状态转移方程
我们用dp[i][j]表示到达位置(i,j)总共的路径数,那么dp[i][j] =dp[i-1][j] + dp[i][j-1],表示(i,j)位置的路径数与其上方的格子(i-1,j),左边的格子(i,j-1)有关。类似于爬楼梯的问题,每次爬一阶或两阶台阶,那么到第10层台阶的走法数等于到第8层台阶的走法数,加上到第9层台阶的走法数。
举个例子,假设要到达(1,1)这个位置,有两种走法(0,0)->(0,1)->(1,1) or (0,0)->(1,0)->(1,1) ,所以到(1,1)的走法等于到(0,1)的走法,加上到(1,0)的走法。
强调一下状态转移方程:
dp[i][j] =dp[i-1][j] + dp[i][j-1]
那么边界条件怎么确定呢?
假设机器人处于第一列或者第一行,那么机器人走的路径唯一,且可以确定是哪一条,要么横着走,要么竖着走。所以第一行和第一列的所有位置的路径数目都为1,即有:
1.dp[0][j]=1
2.dp[i][0]=1
上述两点,表明从最上方往下走,路径唯一;从最左边往右走,路径唯一。
处在(1,1)往(m,n)的方向,所走的路径均满足状态转移方程,因此有:
class Solution:
def uniquePaths(self, m, n):
"""
:type m: int
:type n: int
:rtype: int
"""
c= [[1 for _ in range(n)] for _ in range(m)]
for i in range(1,m):
for j in range(1,n):
c[i][j]=c[i-1][j]+c[i][j-1]
return c[m-1][n-1]
有障碍的走法
仍然以机器人走网格为例,只不过在mxn的网格中设下了若干个障碍物,求所有的走法。
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
- 向右 -向右 -向下 -向下
- 向下 -向下 -向右 -向右
分析:
增加了障碍物,也就是对所走的路径增加了额外的限制条件。题目中用1表示在某个位置存在障碍物,因此每走到一个位置,需要查看障碍物的表,看看是否有障碍物。
逆序思维,先假设已经走到终点(m,n)处,这里用实际的位置表示,列表中与之对应的位置是(m-1,n-1)。那么在终点(m,n)处的走法总和由两部分组成,一部分是来自上方位置,一部分来自左侧位置。当然了也不一定都有两部分组成,当遇到障碍物的时候,可能只有一个。
因此,总体上,状态转移方程还是满足:dp[i][j] =dp[i-1][j] + dp[i][j-1],
但是有可能dp[i][j] =dp[i-1][j] 或者是 dp[i][j] =dp[i][j-1],这是由障碍物决定的。
同时,我们的边界条件也发生了变化,位于第一行或者第一列的位置,原本的走法都是唯一确定,但是假设在第一行或者第一列某个或者某几个位置处,存在障碍物,从第一个障碍物开始,后面的路径都是不通的,在上一题中,我们把边界条件都设置为1,在这里,可能需要部分转变成0。
首先初始化所有位置,初始化为1.
m =len(obstacleGrid)
n = len(obstacleGrid[0])
dp = [[1 for _ in range(n)] for _ in range(m)]
检查第一行和第一列是否有障碍物,有的话,重新更新边界条件。
#检查行
tmp_position=None
for i in range(m):
if obstacleGrid[i][0]==1:
tmp_position = i
break
if tmp_position != None:
for i in range(tmp_position,m):
dp[i][0]=0
#检查列
tmp_position=None
for j in range(n):
if obstacleGrid[0][j]==1:
tmp_position = j
break
if tmp_position != None:
for j in range(tmp_position,n):
dp[0][j]=0
完整的代码为:
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid):
"""
:type obstacleGrid: List[List[int]]
:rtype: int
"""
m =len(obstacleGrid)
n = len(obstacleGrid[0])
dp = [[1 for _ in range(n)] for _ in range(m)]
tmp_position=None
for i in range(m):
if obstacleGrid[i][0]==1:
tmp_position = i
break
if tmp_position != None:
for i in range(tmp_position,m):
dp[i][0]=0
tmp_position=None
for j in range(n):
if obstacleGrid[0][j]==1:
tmp_position = j
break
if tmp_position != None:
for j in range(tmp_position,n):
dp[0][j]=0
#print(dp)
for i in range(1,m):
for j in range(1,n):
#正常地走
if obstacleGrid[i][j]!=1:
dp[i][j] = dp[i-1][j]+dp[i][j-1]
#在网格中间遇见障碍物,需要封住该道路,令经过该路径的数目为0,方便后继的路径只能通过其中的一条,或者是全部堵住。
else:
dp[i][j]=0
return dp[m-1][n-1]
if __name__ == '__main__':
example =Solution()
obstacleGrid =[
[0,0,0],
[0,1,0],
[0,0,0]
]
res=example.uniquePathsWithObstacles(obstacleGrid)
print(res)
最小路径和:
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
本题也是一个二维动态规划问题,与上一题所走路径数目极为相似。
假设 dp[i][j] 是其中一条路径,到位置 (i,j) 为止,所有的数字和.我们知道,这必定是从位置 (i,j-1) 或者 (i-1,j) 走过来的.只不过在每次走的过程中,我们尽量的去走能令最后结果最小的那个位置。
假设 dp[m-1][n-1]是最小的路径和,那么dp[m-2][n-1] or dp[m-1][n-2] 也必定是最小的
dp[m-1][n-1] = min( dp[m-2][n-1] , dp[m-1][n-2] ) + nums[m-1][n-1]
继续往前递推一步
dp[m-1][n-2] = min( dp[m-1][n-2] , dp[m-1][n-3] ) + nums[m-1][n-2]
因此,我们可以得出结论,处于每一个位置的最小数字和,等于其上方或者是其左边较小的和和加上当前位置的值。所以状态转移方程为:
dp[i][j] = min( dp[i-1][j] , dp[i][j-1] ) + nums[i][j]
这是针对从位置(1,1)开始的元素,那么最左边或者最上面的元素呢?我们知道假设处于最左边或者最上面,路径唯一,要么横着走,要么竖着走,所以我们将这些边界条件先求出来,第一行和第一列的元素累积求和,作为边界条件。
min_num =grid
tmp=0
for j in range(n):
tmp +=grid[0][j]
min_num[0][j]=tmp
tmp=0
for i in range(m):
tmp +=grid[i][0]
min_num[i][0]=tmp
所以完整的代码为:
class Solution:
def minPathSum(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
m =len(grid)
n =len(grid[0])
min_num =grid
tmp=0
for j in range(n):
tmp +=grid[0][j]
min_num[0][j]=tmp
tmp=0
for i in range(m):
tmp +=grid[i][0]
min_num[i][0]=tmp
for i in range(1,m):
for j in range(1,n):
min_num[i][j]=min(min_num[i-1][j],min_num[i][j-1])+grid[i][j]
return min_num[m-1][n-1]