Leetcode随机抽题检测
- 70 爬楼梯
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 118 杨辉三角
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 198 打家劫舍
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 279 完全平方数
- 未看解答自己编写的青春版
- 重点
- 怎么没看出来,这道题是完全背包啊!因为是求最少的数目,所以不管是组合数求法还是排列数求法都可以,这意味着,先遍历物品还是先遍历背包都可以!
- 完全背包:一维DP时,背包必须正序遍历,先物品后背包:求组合数;先背包后物品:求排列数。
- 题解的代码
- 日后复习重新编写
- 322 零钱兑换
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 139 单词拆分
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 这几道题做下来,我觉得动态规划我又需要复习了,果然是做一道忘一道,怎么都记不住啊。
- 300 最长递增子序列
- 未看解答自己编写的青春版
- 重点
- 这个贪心+二分的思想,真的太奈斯了!之前从来没接触过。
- 题解的代码
- 日后复习重新编写
- 152 乘积最大子数组
- 未看解答自己编写的青春版
- 重点
- 嫌空间占用大?在最原始的DP代码编写中,发现当前状态只和前一个状态有关,那就进行状态压缩!压缩后占用空间打败60%。
- 题解的代码
- 日后复习重新编写
- 416 分割等和子集
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 62 不同路径
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 64 最小路径和
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 5 最长回文子串
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 1143 最长公共子序列
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
- 72 编辑距离
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后复习重新编写
70 爬楼梯
未看解答自己编写的青春版
动态规划写多了,果然就会不太注重初始值的设置,会有些想当然,比如本题我一开始想的是,设定 dp[0] = 1 , dp[1] = 1 , 这样的结果也是正确的,但是就是可能初始化的解释性上不太好,还是设定 dp[1] dp[2] 更好一些。
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 1:
return 1
dp = [0]*3
dp[1] = 1
dp[2] = 2
for i in range(3,n+1):
total = dp[1]+dp[2]
dp[1] = dp[2]
dp[2] = total
return dp[2]
重点
题解的代码
日后复习重新编写
118 杨辉三角
未看解答自己编写的青春版
感觉这道题没什么DP的味道。
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
res = [[1]]
for i in range(1,numRows):
temp = [1]*(i+1)
flag = res[-1]
for j in range(1,i):
temp[j] = flag[j-1]+flag[j]
res.append(temp)
return res
重点
题解的代码
日后复习重新编写
198 打家劫舍
未看解答自己编写的青春版
其实不需要设置两个初始状态, dp[1] dp[2] ,一个 dp[1] = nums[0] 就够了,这样也不用单独处理:只有一个房屋的情况了。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 0 :
return 0
if n == 1 :
return nums[0]
dp = [0]*(n+1)
dp[1] = nums[0]
dp[2] = max(nums[0],nums[1])
for i in range(3,n+1):
dp[i] = max(dp[i-1],dp[i-2]+nums[i-1])
return dp[n]
重点
动态规划,在人为设置初始状态时,要考虑输入直接被设置好的情况,及时判断并return , 不然会导致 index 错误。
题解的代码
日后复习重新编写
279 完全平方数
未看解答自己编写的青春版
这次写了个没有提前算好的版本,比提前算法的版本要耗时啊。
class Solution:
def numSquares(self, n: int) -> int:
dp = [inf]*(n+1)
dp[0] = 0
dp[1] = 1
if n == 1 :
return 1
for i in range(2,n+1):
mini = inf
temp = int(sqrt(i)+1)
for j in range(1,temp):
if i-j*j >= 0 :
mini = min(mini,dp[i-j*j])
dp[i] = 1 + mini
print(dp)
return dp[n]
提前算好每个平方数:(注意,双重循环的循序,先遍历 item ,再遍历 n )
class Solution:
def numSquares(self, n: int) -> int:
items = []
for i in range(1,int(sqrt(n))+2):
if i*i <= n :
items.append(i*i)
m = len(items)
dp = [inf]*(n+1)
dp[0] = 0
for i in range(m):
for j in range(items[i],n+1):
dp[j] = min(dp[j],dp[j-items[i]]+1)
return dp[n]
提前算好每个平方数:(颠倒遍历顺序,耗时和第一版,不提前算好的方法,差不多了)
class Solution:
def numSquares(self, n: int) -> int:
items = []
for i in range(1,int(sqrt(n))+2):
if i*i <= n :
items.append(i*i)
m = len(items)
dp = [inf]*(n+1)
dp[0] = 0
for i in range(1,n+1):
for j in range(m):
if i >= items[j] :
dp[i] = min(dp[i],dp[i-items[j]]+1)
return dp[n]
重点
综上,这道题还是选用方法二,因为迭代的次数较少,可以发现,每一个数,至少都可以写为 n 个 1 相加的情况,所以我们应该先遍历 item ,去判断取不取当前 item 就好了。
统一写法:
class Solution:
def numSquares(self, n: int) -> int:
items = []
for i in range(1,int(sqrt(n))+2):
if i*i <= n :
items.append(i*i)
m = len(items)
dp = [inf]*(n+1)
dp[0] = 0
for i in range(m):
for j in range(items[i],n+1):
dp[j] = min(dp[j],dp[j-items[i]]+1)
return dp[n]
怎么没看出来,这道题是完全背包啊!因为是求最少的数目,所以不管是组合数求法还是排列数求法都可以,这意味着,先遍历物品还是先遍历背包都可以!
01背包中,二维DP数组的两个for遍历,先后顺序可以颠倒。一维DP数组的两个for循环的顺序,一定是先遍历物品,再遍历背包,另外背包要倒序遍历,因为要保证每个物品只拿一个。而如果,01背包的一维DP数组,倒序遍历背包,但是先遍历背包,再遍历物品,得到的结果是,最后的背包中只有一个物品。(这里因为背包是倒序遍历,上来就把结果位置的输出遍历掉了,此时其他位置的值还都是初始值)
完全背包,不管是一维DP数组还是二维DP数组,遍历顺序都可以颠倒,一维DP数组时,背包的遍历顺序为正序遍历,且必须是正序遍历。
但是完全背包有一个要注意的点,就是完全背包问题的拓展应用题中,会涉及组合数和排列数的问题,而01背包没有类似的问题。求组合数,必须是先遍历物品,再遍历背包;求排列数,是先遍历背包,再遍历物品、
完全背包:一维DP时,背包必须正序遍历,先物品后背包:求组合数;先背包后物品:求排列数。
题解的代码
日后复习重新编写
322 零钱兑换
未看解答自己编写的青春版
也是完全背包!也是求可能结果中的最少元素个数,所以先遍历物品或背包,都无所谓,一维DP,正序遍历。
lass Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
n = len(coins)
dp = [inf]*(amount+1)
if amount == 0 :
return 0
dp[0] = 0
for i in range(n):
for j in range(1,amount+1):
if j >= coins[i] :
dp[j] = min(dp[j],dp[j-coins[i]]+1)
if dp[amount]==inf :
return -1
else :
return dp[amount]
重点
题解的代码
日后复习重新编写
139 单词拆分
未看解答自己编写的青春版
典型完全背包,求排列数的题目,因为字符的排列是要求顺序的.
用一维背包去写,全部顺序遍历,先遍历背包,再遍历物品.
注意这里的递推关系,要有 dp[i],并且逻辑关系是 or 。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
n = len(wordDict)
m = len(s)
dp = [False]*(m+1)
dp[0] = True
# 典型完全背包,求排列数的题目,因为字符的排列是要求顺序的
# 用一维背包去写,全部顺序遍历,先遍历背包,再遍历物品
for i in range(1,m+1):
for j in range(n):
if i >= len(wordDict[j]):
if s[i-len(wordDict[j]):i]==wordDict[j] :
# 注意这里的递推关系,要有 dp[i],并且逻辑关系是 or
dp[i] = dp[i] or dp[i-len(wordDict[j])]
return dp[m]
重点
题解的代码
日后复习重新编写
这几道题做下来,我觉得动态规划我又需要复习了,果然是做一道忘一道,怎么都记不住啊。
300 最长递增子序列
未看解答自己编写的青春版
DP 方法容易想到,很简单,DP数组的含义就是 dp[i] : 以 i 为结尾的最长子序列的长度。
DP方法的时间复杂度为 O(n^2) 。
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
dp = [1]*n
maxi = 0
for i in range(n):
# 判断 i>0 可以省略,因为不涉及index=i-1的操作
for j in range(i):
if nums[i] > nums[j] :
dp[i] = max(dp[i],dp[j]+1)
maxi = max(maxi,dp[i])
#print(dp)
return maxi
重点
题目进阶:时间复杂度为 O( nlogn ) 的方法是什么?
想不到。看题解。
这个贪心+二分的思想,真的太奈斯了!之前从来没接触过。
题解的代码
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
d = []
for n in nums:
if not d or n > d[-1]:
d.append(n)
else:
l, r = 0, len(d) - 1
loc = r
while l <= r:
mid = (l + r) // 2
if d[mid] >= n:
loc = mid
r = mid - 1
else:
l = mid + 1
d[loc] = n
return len(d)
日后复习重新编写
152 乘积最大子数组
未看解答自己编写的青春版
用2个指标,去记录以 i 结尾的子串,最大正值和最小负值。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
n = len(nums)
# dp[i][0] : 以i为结尾的最大正值
# dp[i][1] : 以i为结尾的最小负值
dp = [[0]*2 for _ in range(n)]
if nums[0] > 0 :
dp[0][0] = nums[0]
else :
dp[0][1] = nums[0]
# 这里初始化,如果给 maxi 为 -inf,那么就要单独考虑数组中只有一个元素的情况
# 如果初始化为,第一个元素,就不需要特殊考虑了。
maxi = nums[0]
for i in range(1,n):
if nums[i] > 0 :
dp[i][0] = max(nums[i],dp[i-1][0]*nums[i])
dp[i][1] = dp[i-1][1]*nums[i]
else :
dp[i][0] = dp[i-1][1]*nums[i]
dp[i][1] = min(nums[i],dp[i-1][0]*nums[i])
maxi = max(maxi,dp[i][0])
#print(dp)
return maxi
我的方法速度倒是挺快,但是在内存占用上只打败了5%。
重点
评论中的一种贪心的思想:很有道理
class Solution {
public int maxProduct(int[] nums) {
int a=1;
int max=nums[0];
for(int num:nums){
a=a*num;
if(max<a)max=a;
if(num==0)a=1;
}
a=1;
for(int i=nums.length-1;i>=0;i--){
a=a*nums[i];
if(max<a)max=a;
if(nums[i]==0)a=1;
}
return max;
}
}
嫌空间占用大?在最原始的DP代码编写中,发现当前状态只和前一个状态有关,那就进行状态压缩!压缩后占用空间打败60%。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
n = len(nums)
# dp[i][0] : 以i为结尾的最大正值
# dp[i][1] : 以i为结尾的最小负值
dp = [0,0]
if nums[0] > 0 :
dp[0] = nums[0]
else :
dp[1] = nums[0]
# 这里初始化,如果给 maxi 为 -inf,那么就要单独考虑数组中只有一个元素的情况
# 如果初始化为,第一个元素,就不需要特殊考虑了。
maxi = nums[0]
for i in range(1,n):
if nums[i] > 0 :
dp[0] = max(nums[i],dp[0]*nums[i])
dp[1] = dp[1]*nums[i]
else :
# 在当前nums[i]是负数时,需要先保存一下dp[0],因为在dp[1]更新要用到dp[0]
# 但是这个值已经被改变了!其他情况,不存在这种被改变的case
temp = dp[0]
dp[0] = dp[1]*nums[i]
dp[1] = min(nums[i],temp*nums[i])
maxi = max(maxi,dp[0])
#print(dp)
return maxi
题解的代码
日后复习重新编写
416 分割等和子集
未看解答自己编写的青春版
01背包应用的典型题,一维DP,倒序遍历背包。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
# 这不就是个01背包嘛
n = len(nums)
sumsum = sum(nums)
if sumsum % 2 == 1 :
return False
target = sumsum // 2
dp = [0]*(target+1)
for i in range(n):
for j in range(target,nums[i]-1,-1):
dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])
if dp[-1]==target :
return True
else :
return False
重点
题解的代码
日后复习重新编写
62 不同路径
未看解答自己编写的青春版
这道题注意初始化就可以了,
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0] * n for _ in range(m)]
dp[0][0]=1
for i in range(1,m):
dp[i][0] = 1
for i in range(1,n):
dp[0][i] = 1
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[m-1][n-1]
重点
题解的代码
日后复习重新编写
64 最小路径和
未看解答自己编写的青春版
和上一题解法基本一致,但是时间和空间指标较差。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> 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,n):
dp[0][i] = dp[0][i-1]+grid[0][i]
for i in range(1,m):
dp[i][0] = dp[i-1][0]+grid[i][0]
for i in range(1,m):
for j in range(1,n):
dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j]
return dp[-1][-1]
重点
看了下评论,也没有什么更好的思路,就是最基础的动态规划题目。
题解的代码
日后复习重新编写
5 最长回文子串
未看解答自己编写的青春版
回文串,回文串!遇到和动态规划相关的回文串题目,就用这种思路!
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False]*n for _ in range(n)]
left = 0
right = 0
# dp[i][j]代表[i,j]是否是回文串,可以由dp[i+1][j-1]推出
for i in range(n-1,-1,-1):
for j in range(i,n):
if i==j :
dp[i][j]=True
else :
if s[i]==s[j]:
# 这里进过思考,不需要加一个条件,防止i+1溢出
# 因为如果溢出,则i=n-1,那么j大于等于i,且要小于n
# 那么j只能是n-1,则会执行上面的判断,不会进入到下面
if j-i == 1 or dp[i+1][j-1] :
dp[i][j]=True
if j-i > right-left :
left,right = i,j
return s[left:right+1]
重点
本题的双指针也值得学习!提供了一种另外的思路。
本质思路是:遍历每一个位置 i ,考虑两种情况,以当前位置 i 向两边进行扩散,以当前位置 i 和下一个位置 i+1 ,
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n <= 1 :
return s
start = 0
end = 0
for i in range(n):
left,right = self.find_point(i,i,s)
start,end = self.compare(end,start,right,left)
left,right = self.find_point(i,i+1,s)
# 注意这里,第二次比较,其实这次比较的,已经是前面,以单字符为中心的最大结果了
# 即是上一次的 left 和 right 结果,和这次的 left right 进行比较
start,end = self.compare(end,start,right,left)
return s[start:end]
def find_point(self,i,j,s):
while i >= 0 and j < len(s) and s[i]==s[j] :
i -= 1
j += 1
return i+1,j
def compare(self,a,b,c,d):
if a-b > c-d :
return b,a
else :
return d,c
题解的代码
日后复习重新编写
自己复写了二分法的方法:难点在于 find 函数的理解。以及时刻抓住循环不变量:左闭右闭区间。
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
start = 0
end = 0
# 注意区间,这里我定义为:左闭右闭
for i in range(n):
left,right = self.find(s,i,i)
if right-left > end-start :
start,end = left,right
# 想不明白,find函数到底能不能正确处理i+1=n(即超出index)的情况
# 那就用if逻辑把它剔除嘛,本来这种逻辑也是非法的。
if i < n-1 :
left,right = self.find(s,i,i+1)
if right-left > end-start :
start,end = left,right
return s[start:end+1]
def find(self,s,i,j):
n = len(s)
while i >= 0 and j < n :
# 这里注意二分法的定义,是以i,j这两个位置的元素为中心
# abba叫以'bb'为中心,而'aba'不叫以'ab'为中心!
# 'aba'的情况,被i=j='b'的情况所包含了
if s[i]!=s[j]:
break
else :
i-=1
j+=1
# 始终牢记左闭右闭规则
return [i+1,j-1]
1143 最长公共子序列
未看解答自己编写的青春版
一开始题意理解错了,以为 text1 是母串, text2 是子串,但是重读题后,发现两个串是互相独立的,这也就意味着:dp[i][j] 更新要同时考虑 max(dp[i-1][j],dp[i][j-1]) , 所以本题也无法做状态压缩,必须是二维DP数组。
这是编辑距离的经典类型题了,dp[i][j]的意义是:text1[0:i] 和 text2[0:j] 的最长公共子序列的长度。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
n = len(text1)
m = len(text2)
dp = [[0]*(m+1) for _ in range(n+1)]
for i in range(1,n+1):
for j in range(1,m+1):
if text1[i-1]==text2[j-1]:
dp[i][j] = dp[i-1][j-1]+1
else :
dp[i][j] = max(dp[i-1][j],dp[i][j-1])
return dp[-1][-1]
重点
题解的代码
日后复习重新编写
72 编辑距离
未看解答自己编写的青春版
编辑距离,主要理解两点:如何初始化;如何表示替换操作。
首先明确:插入和删除操作是互逆的,所以我们只需要考虑删除操作就可以了,其中,dp[i-1][j] , dp[i][j-1] ,都代表着删除操作,即:删除word1[i] , 用 word1[0:i-1] 和 word2[0:j] 匹配。删除word2[j] , 用 word1[0:i] 和 word2[0:j-1] 匹配。
而替换操作:dp[i-1][j-1] 将word1[i] or word2[j] 替换为相对应的那个值,这时候,word1[i] 和 word2[j] 是匹配上的,且不能用于前面子串的匹配,所以前面子串的操作个数为 : dp[i-1][j-1] 。
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m = len(word1)
n = len(word2)
if n == 0:
return m
if m==0 :
return n
dp = [[0] * (n+1) for _ in range(m+1)]
# 初始化很重要
for i in range(1,n+1):
dp[0][i] = i
for i in range(1,m+1):
dp[i][0] = i
for 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-1][j],dp[i][j-1],dp[i-1][j-1])+1
return dp[-1][-1]
重点
如果想复习编辑距离的原理,可以复习文章。
编辑距离