目录
一、动态规划题型
1.计数问题
-有多少种方式走到右下角
-有多少种方法选出k个数使得和为sum
2.求最大最小值
-从左上角走到右下角路径的最大数字和
-最长上升子序列长度
3.求存在性
-取石子游戏,先手是否必胜
-能不能选出k个数使得和为sum
二、动态规划的组成部分☆
1、确定状态
最后一步
化成子问题
2、转移方程
将最后一步转化为子问题的递推式
3、初始条件和边界情况
初始条件: 靠递推式计算不出来的量,需手工定义
边界情况:防止越界
4、计算顺序
保证在计算时后面计算所需的量在前面的计算过程中已经被算了出来(一般是从前往后)
以一道题为例:
(求最值型动态规划)题目:有2元,5元,7元三种面值的硬币,如何使用最少的硬币数目拼出27元?
1、确定状态
最后一步:在满足题目要求的最优解中的最后一枚面值为a的硬币(相当于结束的出口)
子问题:用最少的硬币(去除最后一枚硬币后剩下子问题的最优解)拼出剩下的27-a元
2、转移方程
f(x) = min{f(x-2)+1, f(x-5)+1, f(x-7)+1}
(因为到达最后一步可以分别有2,5,7三种情况)
注意,这里面的f(x)相当于值为x的最优解,在代码中一般用一个一维数组来储存f(x)的值,从前往后重复计算,如果遇到光靠一个一维的数组无法表示所有情况时,当然可以用到二维三维乃至更多的数组来储存更多的状态
3、初始条件和边界情况
f[0] = 0(通过转移方程无法计算,且需定义)
如果不能拼出某个值x时,那么应该将f[x]设为无穷大(在后面min中会筛选掉)
4、计算顺序
从前往后
def dpdp(coins=27): # 拼27块钱
c = [2, 5, 7] # 总共有2,5,7面值的硬币
MAXNUM = 8888 # 无穷大的数
f = [0 for _ in range(coins+1)]
for i in range(coins+1):
if i == 0:
f[0] = 0 # 初始条件
else:
f[i] = MAXNUM
for j in c:
if i-j >= 0:
f[i] = min(f[i-j]+1, f[i])
if f[coins-1] > MAXNUM/2: # 无穷大表示无法拼出
return None
else:
return f[coins] # 拼27块钱需要的最少硬币数
小结
dp问题感觉和递归的思路有点像,都是重复调用前一时间的自身的值,且都需要递推出口和递推主体,但显然dp的性能更优,相比于递归而言少了很多重复的步骤,一般来说空间复杂度也更小。
三、刷题
1、不同路径 & 不同路径(障碍)
题目链接:https://leetcode-cn.com/problems/unique-paths-ii/
思路:转换方程:f[i][j] = f[i-1][j] + f[i][j-1] i,j位置的最优解等于上面一个位置的最优解加上左边一个的最优解
边界条件:第一行第一列的最优解为1,因为只有一种方式到达
对于带障碍的问题,就是要注意在障碍的地方最优解为0
代码:
# 不同路径
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
l = collections.defaultdict(dict)
for j in range(m):
for i in range(n):
if j == 0 or i == 0:
l[j][i] = 1
else:
l[j][i] = l[j-1][i] + l[j][i-1]
return l[m-1][n-1]
# 不同路径2
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
if obstacleGrid[0][0] == 1: # 如果起始位置为1(有障碍)就直接输出0,因为后面我们为了让所有值去递进,把起始位置设置成了1
return 0
for j in range(len(obstacleGrid)):
for i in range(len(obstacleGrid[0])):
if obstacleGrid[j][i] == 1:
obstacleGrid[j][i] = 0
else:
if i == 0 or j == 0:
if j == 0:
if i > 0:
obstacleGrid[j][i] = obstacleGrid[j][i-1]
if i == 0:
if j > 0:
obstacleGrid[j][i] = obstacleGrid[j-1][i]
else:
obstacleGrid[j][i] = obstacleGrid[j-1][i] + obstacleGrid[j][i-1]
if i+j == 0:
obstacleGrid[j][i] = 1
else:
return obstacleGrid[len(obstacleGrid)-1][len(obstacleGrid[0])-1]
2、乘积最大子数组
题目
难度中等1481
给你一个整数数组 nums
,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1] 输出: 0 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
思路:
因为有负数的存在,所以说产生最优解的可能有正数子序列*最后一个正数,负数子序列*最后一个负数,所以对于问题的递推式自然可以想到判断最后一步的正负,如果为正数,就找前面的最大正数序列,如果为负数就找前面最大负数序列,思路很简单,但在代码的实现上会很绕,以为正数负数之间是可以相互转换的,不是两个独立的状态,所以对每个位置单独找最大正数序列和最大负数序列会很难处理正负数转换的边界条件。
在官方题解中,避开了正负数转换的问题,直接从结果出发,确立一个最大值序列和一个最小值序列,而不去管这个最大值或最小值是怎么得到的,值得学习。
看到一条惊艳的解法
def maxProduct(self, A):
B = A[::-1] # 翻转数组,相当于从右往左遍历
for i in range(1, len(A)):
A[i] *= A[i - 1] or 1 # or 1 实现了遇到0重置
B[i] *= B[i - 1] or 1
return max(A + B) # 找A和B中的最大值
思路:
情况1: 判断负号的个数,如果为偶数个则从头到尾累乘一次(遇到0就重置)就可以得到结果
情况2:如果为奇数个,则第一个负号的右边或者最后一个负号的左边就还剩偶数个负号,重复情况1
最后寻找最大值位置。
tip:
代码中,for循环相当于从左到右,从右到左各遍历了一次。
想不通的话将[-1, -1, -1, -1]和 [-1, -1, -1]带进去思考一下