LeetCode Hot100 ---- 动态规划专题
动态规划问题
力扣121:买卖股票(一次交易)
力扣122:买卖股票(多次交易)
力扣134:加油站
力扣309:买卖股票(包含冷冻时间)
力扣322:零钱兑换
力扣518:零钱兑换
力扣53:最大子緒和
力扣674:未经排序数组最长连续递增序列
把数字翻译成字符串
剪绳子
接雨水
礼物的最大价值
动态规划解题步骤
核心思想是递推,难点在于想清楚 状态 dp[i] 代表什么,然后构造状态转移矩阵,利用初始条件递推出最终结果
- 将原问题拆分成子问题
- 确认状态
- 确认边界状态(初始条件)
- 状态转移方程
矩阵类型
minimum-path-sum
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
思路:动态规划
state: f(x, y) 从起点走到 (x, y) 的最短路径
function: f(x, y) = min(f(x - 1, y), f(x, y - 1)) + A(x, y)
intialize: f(0, 0) = A(0, 0)、f(i, 0) = sum(0,0 -> i,0)、 f(0, i) = sum(0,0 -> 0,i)
answer: f(n - 1, m - 1)
class Solution(object):def minPathSum(self, grid):""":type grid: List[List[int]]:rtype: int"""m = len(grid)n = len(grid[0])dp = [[0]*n for _ in range(m)]dp[0][0] = grid[0][0]for i in range(1,m):dp[i][0] = grid[i][0]+dp[i-1][0]for i in range(1,n):dp[0][i] = grid[0][i]+dp[0][i-1]for i in range(1,m):for j in range(1,n):dp[i][j] = min(grid[i][j]+dp[i-1][j],grid[i][j]+dp[i][j-1])return dp[-1][-1]
unique-paths
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 问总共有多少条不同的路径?
我们令 dp[i][j] 是到达 i, j 最多路径
动态方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]
注意,对于第一行 dp[0][j],或者第一列 dp[i][0],由于都是在边界,所以只能为 1
时间复杂度:O(m∗n)
空间复杂度:O(m∗n)
优化:因为我们每次只需要 dp[i-1][j],dp[i][j-1]
class Solution(object):def uniquePaths(self, m, n):dp = [[0 for _ in range(n)] for _ in range(m)]# 第一行都赋予 1for j in range(n):dp[0][j] = 1# 第一列都赋予 1 for i in range(m):dp[i][0] = 1# 两个for循环推导,对于(i,j)来说,只能由上方或者左方转移过来 for i in range(1, m):for j in range(1, n):dp[i][j] = dp[i - 1][j] + dp[i][j - 1]return dp[-1][-1]
unique-paths-ii
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 问总共有多少条不同的路径? 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
class Solution(object):def uniquePathsWithObstacles(self, obstacleGrid):n = len(obstacleGrid)m = len(obstacleGrid[0])dp = [[0] * m for _ in range(n)]#(0,0)这个格子可能有障碍物dp[0][0] = 0 if obstacleGrid[0][0] else 1#处理第一列for i in range(1, n):if obstacleGrid[i][0] == 1 or dp[i - 1][0] == 0:dp[i][0] = 0else:dp[i][0] = 1#处理第一行 for j in range(1, m):if obstacleGrid[0][j] == 1 or dp[0][j - 1] == 0:dp[0][j] = 0else:dp[0][j] = 1for i in range(1, n):for j in range(1, m):#如果当前格子是障碍物if obstacleGrid[i][j] == 1:dp[i][j] = 0#路径总数来自于上方(dp[i-1][j])和左方(dp[i][j-1]) else:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]return dp[-1][-1]
序列类型
climbing-stairs
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
class Solution(object):def climbStairs(self, n):dp = [0] * (n + 1)dp[0] = dp[1] = 1for i in range(2, n + 1):dp[i] = dp[i - 1] + dp[i - 2]return dp[-1]
312. 戳气球
有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。
求所能获得硬币的最大数量。
思路:
- dp[i][j]代表i和j之间的球能获得最大硬币数,k代表最后一个戳破的气球则有
dp[i][k] + nums[i]*nums[k]*nums[j] + dp[k][j]
。那么dp[i][j]就等于遍历i和j之间的k,找到最大的那个。 - 遍历时,根据i和j之间的区间长度遍历,因为更长的区间来自更短的区间。这样通过动态规划可以得到所有的区间长度。
假设这个区间是个开区间,最左边索引 i,最右边索引 j
我这里说 “开区间” 的意思是,我们只能戳爆 i 和 j 之间的气球,i 和 j 不要戳
DP思路是这样的,就先别管前面是怎么戳的,你只要管这个区间最后一个被戳破的是哪个气球
这最后一个被戳爆的气球就是 k
k是这个区间 最后一个 被戳爆的气球!!!!!
假设最后一个被戳爆的气球是粉色的,k 就是粉色气球的索引
然后由于 k 是最后一个被戳爆的,所以它被戳爆之前的场景是什么样子?
因为是最后一个被戳爆的,所以它周边没有球了!没有球了!只有这个开区间首尾的 i 和 j 了!
这就是为什么DP的状态转移方程是只和 i 和 j 位置的数字有关
假设 dp[i][j] 表示开区间 (i,j) 内你能拿到的最多金币
那么这个情况下 你在 (i,j) 开区间得到的金币可以由 dp[i][k] 和 dp[k][j] 进行转移
如果你此刻选择戳爆气球 k,那么你得到的金币数量就是:
那你可能又想问了,戳爆粉色气球我能获得 val[i]*val[k]*val[j] 这么多金币我能理解(因为戳爆 k 的时候只剩下这三个气球了), 但为什么前后只要加上 dp[i][k] 和 dp[k][j] 的值就行了呀? 因为 k 是最后一个被戳爆的,所以 (i,j) 区间中 k 两边的东西必然是先各自被戳爆了的, 左右两边互不干扰,大家可以细品一下 这就是为什么我们 DP 时要看 “最后一个被戳爆的” 气球,这就是为了让左右两边互不干扰,这大概就是一种分治的思想.
然后呢,你就从 (i,j) 开区间只有三个数字的时候开始计算,储存每个小区间可以得到金币的最大值
然后慢慢扩展到更大的区间,利用小区间里已经算好的数字来算更大的区间
class Solution:def maxCoins(self, nums):# nums首尾添加1,方便处理边界情况nums.insert(0, 1)nums.insert(len(nums), 1)dp = [[0] * (len(nums)) for _ in range(len(nums))]def range_best(i, j):m = 0# k是(i,j)区间内最后一个被戳的气球for k in range(i + 1, j): # k取值在(i,j)开区间中# 以下都是开区间(i,k), (k,j)left = dp[i][k]right = dp[k][j]tmp = left + nums[i] * nums[k] * nums[j] + rightif tmp > m:m = tmpdp[i][j] = m# 对每一个区间长度进行循环for delta in range(2, len(nums)): # 区间长度 #长度从3开始,n从2开始# 开区间长度会从3一直到len(nums)# 因为这里取的是range,所以最后一个数字是len(nums)-1# 对于每一个区间长度,循环区间开头的ifor start in range(0, len(nums) - delta): # i+n = len(nums)-1# 计算这个区间的最多金币range_best(start, start + delta)return dp[0][len(nums) - 1]
121. 买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
思路:
考虑仅遍历一遍, 要找到最大利润, 等价于找到两个索引a和b, 其中b>a, 且 利润=prices[b] - prices[a] 最大.
我们来假设自己来购买股票。随着时间的推移,每天我们都可以选择出售股票与否。那么,假设在第 i 天,如果我们要在今天卖股票,那么我们能赚多少钱呢?
显然,如果我们真的在买卖股票,我们肯定会想:如果我是在历史最低点买的股票就好了!太好了,在题目中,我们只要用一个变量记录一个历史最低价格 minprice,我们就可以假设自己的股票是在那天买的。那么我们在第 i 天卖出股票能得到的利润就是 prices[i] - minprice。
因此,我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案
以上遍历包含了所有的情况, 则最大利润即为其中最大的值.
class Solution:def maxProfit(self, prices: List[int]) -> int:inf = int(1e9)minprice = infmaxprofit = 0for price in prices:maxprofit = max(price - minprice, maxprofit)minprice = min(price, minprice)return maxprofit
238. 除自身以外数组的乘积
长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。要求不使用除法。
示例:
输入: [1,2,3,4] 输出: [24,12,8,6]
思路:
从左往右遍历一轮,得到每个索引位置左边所有元素乘积。然后再从右往左,乘以每个索引位置右边的乘积。
class Solution:def productExceptSelf(self, nums: List[int]) -> List[int]:length = len(nums)# L 和 R 分别表示左右两侧的乘积列表L, R, answer = [0]*length, [0]*length, [0]*length# L[i] 为索引 i 左侧所有元素的乘积# 对于索引为 '0' 的元素,因为左侧没有元素,所以 L[0] = 1L[0] = 1for i in range(1, length):L[i] = nums[i - 1] * L[i - 1]# R[i] 为索引 i 右侧所有元素的乘积# 对于索引为 'length-1' 的元素,因为右侧没有元素,所以 R[length-1] = 1R[length - 1] = 1for i in reversed(range(length - 1)):R[i] = nums[i + 1] * R[i + 1]# 对于索引 i,除 nums[i] 之外其余各元素的乘积就是左侧所有元素的乘积乘以右侧所有元素的乘积for i in range(length):answer[i] = L[i] * R[i]return answer
Two Sequences DP
72. 编辑距离
给你两个单词 word1
和 word2
,请你计算出将 word1
转换成 word2
所使用的最少操作数 。
思路:
dp[i][j] 代表 word1 到 i 位置转换成 word2 到 j 位置需要最少步数
- 动态规划:建立二维数组dp,
dp[i][j]
代表从word1前i个字符子串到word2前j个字符子串的编辑距离,则:- 如果
word1[i] == word2[j]
,dp[i][j] = dp[i-1][j-1]
- 如果不等,则
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
, 分别对应替换一个字符操作、删除一个字符、增加一个字符。 - 开头增加一行和一列空字符,作为初始化状态。
- 如果
那么dp[i][0] 和 dp[0][j] 表示什么呢?
dp[i][0]: 以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。
那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,
即:dp[i][0] = i; 同理dp[0][j] = j;
第一行,是 word1
为空变成 word2
最少步数,就是插入操作
第一列,是 word2
为空,需要的最少步数,就是删除操作
class Solution:def minDistance(self, word1, word2):m = len(word1)n = len(word2)dp = [[0] * (n + 1) for _ in range(m + 1)]for i in range(m+1):dp[i][0] = ifor j in range(n+1):dp[0][j] = jfor i in range(1, m + 1):for j in range(1, n + 1):if word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1]else:dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] ) + 1#print(dp) return dp[-1][-1]
练习
Matrix DP (10%)
- triangle
- minimum-path-sum
- unique-paths
- unique-paths-ii
Sequence (40%)
- climbing-stairs
- jump-game
- jump-game-ii
- palindrome-partitioning-ii
- longest-increasing-subsequence
- word-break
Two Sequences DP (40%)
- longest-common-subsequence
- edit-distance
Backpack & Coin Change (10%)
- coin-change
- backpack
- backpack-ii
LeetCode Hot100 ---- 动态规划专题相关推荐
- LeetCode Hot100 ---- 二叉树专题
树 力扣102:二叉搜索树的层次遍历 力扣105:从前序和中序重构二叉树 力扣108:将有序数组转化为二叉搜索树 力扣110:平衡二叉树 力扣113:路径总和 力扣124:二叉树的最大路径和 力扣13 ...
- 【LeetCode】动态规划专题
动态规划 打家劫舍 198. 打家劫舍 你是一个专业的小偷,计划偷窃沿街的房屋.每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小 ...
- LeetCode Hot100 ---- 排序专题
排序算法 快速排序 插入排序 归并排序 希尔排序 堆排序 快速排序 快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个 ...
- LeetCode Hot100 ---- 链表专题专题
链表 力扣109:将有序链表转化为二叉搜素树 力扣141:环形链表判断是否有环 力扣142:环形链表检测入口位置 力扣143:重拍链表 力扣160:相交链表 力扣206:反转链表 力扣21:合并两个有 ...
- Leetcode之动态规划(DP)专题-1025. 除数博弈(Divisor Game)
Leetcode之动态规划(DP)专题-1025. 除数博弈(Divisor Game) 爱丽丝和鲍勃一起玩游戏,他们轮流行动.爱丽丝先手开局. 最初,黑板上有一个数字 N .在每个玩家的回合,玩家需 ...
- leetcode hot100 梳理
熟悉一些常用的算法编程题,在应对面试的时候,还是很有帮助的.通过分门别类,将 leetcode hot100 的题目进行梳理,能够加强个人的归纳总结能力,也能让自己在下次复习的时候能够快速的熟悉. 对 ...
- 20200707:动态规划专题之不同路径
动态规划复习day01 动态规划的题目一直是本人的弱项,这周尝试连续7天的动态规划专题,争取做到有的放矢. 不同路径Ⅰ和Ⅱ解析 题目一:不同路径 解题思路 依据动态规划解题的套路思路 确定状态和选择, ...
- LeetCode题解 - 动态规划-股票买卖
LeetCode题解 - 动态规划-股票买卖 文章目录 LeetCode题解 - 动态规划-股票买卖 **一.穷举框架** **二.状态转移框架** **三.秒杀题目** 121. 买卖股票的最佳时机 ...
- 【LeetCode】动态规划入门(专项打卡21天合集)
[LeetCode]动态规划入门(专项打卡21天合集) 下图为证 文章目录 [LeetCode]动态规划入门(专项打卡21天合集) Day1 斐波拉契数 第 N 个泰波那契数 Day2 爬楼梯 使用最 ...
最新文章
- 转帖:iOS UIWindow UIWindowLevel
- ZOJ 3781 Paint the Grid Reloaded
- Linux socket 流模式(STREAM)跟数据报模式(DGRAM)的区别
- 力扣--让字符串成为回文串的最少插入次数
- react学习路线图,学习react就是有捷径
- 开发人员工作周报_如何增加找到开发人员工作的机会
- Visual Studio 2017新建及运行C++程序步骤
- SpringBoot使用GZIP压缩返回数据
- 错把女生的耍脾气当拒绝
- 如何清空Python的List
- ZOJ 3429 Cube Simulation (思维题)
- 时间序列之平稳时间序列预测、趋势型序列预测、复合型序列预测
- [渝粤教育] 武汉大学 唐诗艺术 参考 资料
- java服务端性能优化_记我的一次 Java 服务性能优化
- android 实现点击水波纹,Android 水波纹点击效果(Ripple Effect)
- unity再战PBR材质流程与材质制作实践
- js使用cookie实现7天免登录
- 《OpenHarmony 3GPP协议开发深度剖析》之--搜网流程之PLMN选择
- 用C++ 输出[1,100]范围内的所有奇数,每行10个。
- 用 Python 生成 CSV 文件