LeetCode之动态规划

Easy难度

十载的澜沧网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。成都全网营销的优势是能够根据用户设备显示端的尺寸不同,自动调整澜沧建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。创新互联从事“澜沧网站设计”,“澜沧网站推广”以来,每个客户项目都认真落实执行。

Easy难度的都是一维DP,前面几道都是我们在学习dp的时候经常会遇到的例题。我认为dp的关键在于最优子结构的选择,最优子结构选择好了对应的状态转移就可以很容易的求解。建议把一些常用的最优子结构背下来(就跟当初背快排一样),遇到类似的题目直接套用或者改变就好了。我的dp解题思路为 确定使用动态规划->确定最优子结构->确定状态转移方程->确定边界条件。动态规划并不是一种具体的解题方法,没有万能的公式可以套,而是一种解题的思维方法。

53. 最大子序和

分析:对于包含负数的求和容易陷入局部最优解而忽略全局最优解。

最优子结构:假设dp[i]表示以j结尾的最大子序列,也可以假设dp[i,j]为以i为起点j为终点的最大子序和,这种子结构明显比第一种复杂。

状态转移方程:最优子结构确定后可以确定状态转移方程了,假设i-1时的sum为正数,那么以i结尾的子序和为dp[i-1] + nums[i];如果当前sum为负数,那么不论nums[i]是正是负,加上一个负数之后都会减小,直接不加就完事了。因此状态转移方程为:dp[i]=max(dp[i-1] + nums[i], nums[i])

class Solution:

def maxSubArray(self, nums: List[int]) -> int:

dp = []

for i in range(len(nums)):

if i == 0:

dp.append(nums[i])

else:

dp.append(max(dp[i-1] + nums[i], nums[i]))

return max(dp)

70. 爬楼梯

分析: 一维dp还有一种简单的解法就是采用数学归纳法,首先计算前几项,然后归纳出一个一般通项。这里不需要严格证明,一般归纳的结果都是正确的,这里用这种方法作为解法。

最优子结构:对于一级台阶i,最后一步可能有两种情况,即从i-1上来,或者从i-2上来。

状态转移方程:采用数学归纳法:假设台阶数为n,方法数为dp

n = 1, dp[1] = 1

n = 2, dp[2] = 2

n = 3, dp[3] = 3 (111,12, 21)

n = 4, dp[4] = 5 (1111,121,211,112, 22)

容易发现 dp[i] = dp[i-1] + dp[i-2]

class Solution:

def climbStairs(self, n: int) -> int:

dp = [0, 1, 2]

i = 3

while i <= n:

dp.append(dp[i-1] + dp[i-2])

i = i + 1

return dp[n]

Medium难度

Easy难度的都是一维DP,到了Medium难度一般都是二维DP了或者有条件的一维DP。好的讲解动态规划的文章都只介绍了一维的DP,导致看完之后觉得很简单,到实际做起题来发现无从下手。二维DP的矩阵,当前dp[i,j]一般与三个值有关,即 dp[i-1][j], dp[i][j-1]和dp[i-1][j-1]。

62. 不同路径

分析:

1. 状态转移方程:到达终点可以分成两部分,第一部分从终点上方到达终点如红线所示,或者从终点左边到达终点如蓝线所示。那么到达终点的路径总数就等于红线总数加上蓝线总数(因为不可以斜着走),因此状态转移方程为 dp[i][j] = dp[i-1][j] + dp[i][j-1]

2. 初始条件: 终点和起点重合时候只有一条路径,因此dp[1][1] = 1

class Solution:

def uniquePaths(self, m: int, n: int):

dp = [([0] * (n+1)) for i in range(m+1)] # create 2d array

for i in range(1, m+1):

for j in range(1, n+1):

if i == 1 and j == 1:

dp[i][j] = 1

else:

dp[i][j] = dp[i-1][j] + dp[i][j-1]

return dp[m][n]

P.S.:二维DP一般DP矩阵会比原始数据大一圈,一是为了符合真实环境,二是DP很多初始时刻值都为0

63. 不同路径 II

分析:

1. 状态转移方程:这道题和上面一道题类似,只不过加了一些限定条件。如图所示,如果终点上方存在障碍物,那么从终点上面的路径将全部失效,如果左边上方存在障碍物,那么从终点左边的路径将全部失效。而有没有障碍物已经给出,因此状态转移方程为: dp[i][j] = dp[i-1][j] * (1 - obstacleGrid[i-2][j-1]) + dp[i][j-1] * (1 - obstacleGrid[i-1][j-2])

2. 初始条件: 终点和起点重合时候只有一条路径,因此dp[1][1] = 1

3. 特殊情况:当障碍物在终点时,无论多大,路径都不存在

class Solution:

def uniquePathsWithObstacles(self, obstacleGrid):

m = len(obstacleGrid)

n = len(obstacleGrid[0])

if obstacleGrid[m-1][n-1] == 1:

return 0

dp = [([0] * (n+1)) for i in range(m+1)]

for i in range(1, m+1):

for j in range(1, n+1):

# print(i,j)

if i == 1 and j == 1:

dp[i][j] = 1 - obstacleGrid[i-1][j-1]

else:

dp[i][j] = dp[i-1][j] * (1 - obstacleGrid[i-2][j-1]) + dp[i][j-1] * (1 - obstacleGrid[i-1][j-2])

return dp[m][n]

64. 最小路径和

分析:这题和上面几题类似

1. 状态转移方程:如图所示,假设只有矩阵[[1,3],[1,5]],那么以5作为终点的话有两条路径,一条从上方过来,另一条从左边过来。由于题目要求最小路径,因此选取红线和蓝线中的较小者,加上该点的值就是该点作为终点DP的值。因此,状态转移方程为: dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1]

2. 边界条件:当只有一行时,该路径为这一行的和 dp[i][j] = dp[i][j-1] + grid[i-1][j-1];只有一列时,该路径为这一列的和 dp[i][j] = dp[i-1][j] + grid[i-1][j-1]

class Solution:

def minPathSum(self, grid):

m = len(grid)

n = len(grid[0])

dp = [([0] * (n+1)) for i in range(m+1)]

for i in range(1, m+1):

for j in range(1, n+1):

if i == 1:

dp[i][j] = dp[i][j-1] + grid[i-1][j-1]

elif j == 1:

dp[i][j] = dp[i-1][j] + grid[i-1][j-1]

else:

dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1]

return dp[m][n]

96. 不同的二叉搜索树

分析:一维DP,不过状态转移方程有点复杂

1. 状态转移方程: 本题是在树结构下的DP,首先我们可以知道dp[1] = 1, dp[2] = 2, dp[3] = 5, 看上去没有任何联系。这道题一开始一头雾水,因为找不到子结构。在树的背景下就要考虑树结构的特点。一个二叉树分为左子树和右子树,而左右子树的元素数为n-1(减去根节点)。那么很容易想到将n-1分解成两个数的和,这两个数分别为左右子树元素数目。左右子树元素必定少于n,那么就可以查表找到对应的搜索树数目,分析到这子结构就确定了。下面看状态转移方程,以n=2为例,如图所示

当n=2时,有两种情况,以1为根节点,那么剩余元素2。此时左子树只有0个元素,因此二叉搜索树总数为dp[0],右子树有1个元素,二叉搜索树总数为dp[1];另一种是以2为根节点,此时情况与以1为根节点情况相反。因此可以得出状态转移方程为

LeetCode之动态规划

class Solution:

def numTrees(self, n: int) -> int:

dp = [0] * (n+1)

dp[0] = 1

dp[1] = 1

for i in range(2, n+1):

for j in range(i):

dp[i] += dp[i-1-j] * dp[j]

return dp[-1]

120. 三角形最小路径和

分析:自底向上的DP

1. 状态转移方程:这道题如果都是整数那么会简单很多,引入了负数就需要考虑到所有情况。因此需要计算所有可能性的值,采用自底向上的方法。从倒数第二层开始,计算每一层所有可能的最小值。那么,第二层所有可能最小值如图所示为[7,6,10]

由此可得,状态转移方程为 dp[j] = min(dp[j], dp[j+1]) + triangle[i][j]。

class Solution:

def minimumTotal(self, triangle):

n = len(triangle)

dp = triangle[-1]

i = n-2

while i >=0:

j = 0无锡人流医院 http://www.bhnkyy39.com/

while j <= len(triangle[i])-1:

dp[j] = min(dp[j], dp[j+1]) + triangle[i][j]

j += 1

i -= 1

return dp[0]

304. 二维区域和检索 - 矩阵不可变

分析:303. 区域和检索 - 数组不可变的基础之上,扩充到二维

状态转移方程:矩阵可以分解成一行一行来看,对于被选中的一行来说,我们假设row_dp[j]表示这一行截止到j的所有元素之和。那么选中局限区域内的值为row_dp[col2] - row_dp[col1-1]。如图所示,红色框出来的那一行row_dp = [1,3,3,4,9],被选中区域的值为row_dp[3] - row_dp[0] = 4 - 1 = 3。因此状态转移方程为:row_dp[j] = row_dp[j-1] + matrix[i][j]。对每一行都这样处理就可以得到整个矩阵的dp

LeetCode之动态规划

class NumMatrix:

def __init__(self, matrix):

m = len(matrix)

if m == 0:

self.dp = None

return

n = len(matrix[0])

dp = []

for i in range(m):

row_dp = [matrix[i][0]]

for j in range(1, n):

row_dp.append(row_dp[j-1] + matrix[i][j])

dp.append(row_dp)

self.dp = dp

def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:

result = 0

i = row1

while i <= row2:

if col1 != 0:

result += self.dp[i][col2] - self.dp[i][col1-1]

else:

result += self.dp[i][col2]

i += 1

return result


本文标题:LeetCode之动态规划
网页URL:http://azwzsj.com/article/pggoph.html