LeetCode每日一题7月记录
- 7.1最长重复子数组
- 7.2有序矩阵中第k小的元素
- 7.3将有序数据转换为二叉搜索树
- 7.4最长有效括号
- 7.5通配符匹配
- 7.6 不同路径Ⅱ
- 7.7路径总和
- 7.8跳水板
- 7.9 恢复空格
- 7.10 最佳买卖股票时机含冷冻期
- 7.11 计算右侧小于当前元素的个数
- 7.12 地下城游戏
- 7.13 两个数组的交集
- 7.14 三角形最小路径和
- 7.15不同的二叉搜索树
- 7.16 判断二分图
- 7.17搜索插入位置
- 7.18交错字符串
- 7.19 戳气球
- 7.20 两数之和Ⅱ
- 7.21 不同的二叉搜索树Ⅱ
- 7.22 剑指 Offer 11. 旋转数组的最小数字
- 7.23 最小路径和
- 7.24 除数博弈
- 7.25 分割数组的最大值
- 7.26 矩阵中的最长递增路径
- 7.27 判断子序列
- 7.28 二叉树的最大深度
- 7.29 寻宝
- 7.30 整数拆分
- 7.31 魔术索引
7.1最长重复子数组
原题地址.
A[i:]和B[j:]的最长重复长度为A[i+1:]和B[j+1:]最长长度加1(如果A[i]==B[j]),所以可以用动态规划思想解决。
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
n = len(A)
m = len(B)
dp = [[0] * (m+1) for _ in range(n+1)]
res = 0
#最小的字串开始,往更长的字串去计算dp[i][j]表示A[i:]与B[j:]的最大长度
for i in range(n-1,-1,-1):
for j in range(m-1,-1,-1):
dp[i][j] = dp[i+1][j+1] + 1 if A[i] == B[j] else 0
res = max(res,dp[i][j])
return res
7.2有序矩阵中第k小的元素
原题地址.
首先利用矩阵的性质,可以写出一个便捷的函数判断矩阵中有多少元素小于某一个值。
def check(mid):
i,j = n-1,0
#小于mid的元素个数
num = 0
while i >= 0 and j < n:
if matrix[i][j] <= mid:
#一升序,所以上面全部小于mid
num += i+1
j += 1
else:
i -= 1
return num >= k
进而用二分搜索就可以找找到满足条件的值
left, right = matrix[0][0], matrix[-1][-1]
while left < right:
mid = (left + right) // 2
if check(mid):
right = mid
else:
left = mid + 1
return left
7.3将有序数据转换为二叉搜索树
原题地址.
转换主要考虑选取根节点的问题,方式不止一种,但是最容易方便的就是二分法,每次选取中间的点当根,这样左右子树就会平衡
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> TreeNode:
def helper(l,r):
if l > r:
return
mid = l + (r-l)//2
root = TreeNode(nums[mid])
#递归构建左右子树
root.left = helper(l,mid-1)
root.right = helper(mid+1,r)
return root
return helper(0,len(nums)-1)
7.4最长有效括号
原题地址.
遇到括号匹配问题,首先想到利用栈的一边扫描,一边出栈,一边判断长度。
class Solution:
def longestValidParentheses(self, s: str) -> int:
out = 0
#方便计数初始值
stack = [-1]
for i in range(len(s)):
if s[i] == '(':
stack.append(i)
else:
stack.pop()
if stack:
#比较当前长度
out = max(out,i - stack[-1])
else:
#出现了多余的右括号,所以起始计数位置要重置
stack.append(i)
return out
7.5通配符匹配
原题地址.
匹配问题可以采用动态规划思想。
用dp[i][j] 标记s[:i]与p[:j]是否匹配,那么可写出转移方程。
如果p[j-1]==s[i-1] or '?'
,就和前缀字串相同了,如果p[j-1] == ‘*’
则分不进行匹配dp[i][j] = dp[i][j-1]
和匹配当前dp[i][j] = dp[i-1][j]
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
#初始化
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
for i in range(1, n + 1):
#连续为‘*’号才True
if p[i - 1] == '*':
dp[0][i] = True
else:
break
for i in range(1, m + 1):
for j in range(1, n + 1):
#通匹符号可以匹配任意多个或者不匹配
if p[j-1] == '*':
#不用匹配就是不用‘*’出马,j往前看一个就行;匹配的话‘*’肯定那把s[i]顶掉,所以i往前看就行。
dp[i][j] = dp[i][j-1] or dp[i-1][j]
#如果接下来一个字符可以对上,就和前缀字串一致
elif p[j-1] =='?' or p[j-1] == s[i-1]:
dp[i][j] = dp[i-1][j-1]
return dp[-1][-1]
7.6 不同路径Ⅱ
原题地址.
还是dp思想,但是判断条件更多,边界条件也更麻烦。
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
m = len(obstacleGrid)
n = len(obstacleGrid[0])
#一开始就堵了
if obstacleGrid[0][0] == 1:
return 0
dp = [0] * n
#第一行一直往右走,一旦堵了就全堵了
for i in range(n):
if obstacleGrid[0][i] == 0:
dp[i] = 1
else:
break
for i in range(1,m):
#第一列只能往下走,要看上一行第一个走不走得通
dp[0] = dp[0] if obstacleGrid[i][0] == 0 else 0
for j in range(1,n):
#本身堵了就是0,没有就看上和左的路径了
dp[j] = dp[j-1] + dp[j] if obstacleGrid[i][j] == 0 else 0
return dp[-1]
7.7路径总和
原题地址.
一边递归一边更新总和值即可。
class Solution:
def hasPathSum(self, root: TreeNode, sum: int) -> bool:
if not root:
return False
#满足条件
if not root.left and not root.right:
return sum == root.val
return self.hasPathSum(root.left, sum - root.val) or self.hasPathSum(root.right, sum - root.val)
7.8跳水板
原题地址.
利用数量总和一定解答
class Solution:
def divingBoard(self, shorter: int, longer: int, k: int) -> List[int]:
if k == 0:
return []
if shorter == longer:
#只有一种情况,其实不要也能通过,但是可以剪枝加速
return [k * shorter]
else:
#数量和是一定的
return [(k - i) * shorter + i * longer for i in range(k + 1)]
7.9 恢复空格
原题地址.
暴力也可以通过,但是为了优化,需要用到前缀树和dp。
class TreeNode:
def __init__(self):
#定义前缀树,is_word是单词结尾标志
self.child = {}
self.is_word = False
class Solution:
def make_tree(self, dictionary):
#递归构建前缀树
for word in dictionary:
node = self.root
for s in word:
if not s in node.child:
node.child[s] = TreeNode()
node = node.child[s]
node.is_word = True
def respace(self, dictionary: List[str], sentence: str) -> int:
self.root = TreeNode()
self.make_tree(dictionary)
n = len(sentence)
#dp数组,dp[i]是sentence[i:]的未识别字符数
dp = [0] * (n + 1)
for i in range(n-1, -1, -1):
dp[i] = n - i
node = self.root
#判断sentence[i:]里的字串是否在字典
for j in range(i, n):
c = sentence[j]
#转移方程
if c not in node.child:
#认命,sentence[i:j]也是未识别字符
dp[i] = min(dp[i], dp[j+1]+j-i+1)
break
if node.child[c].is_word:
#是一个单词,可以不考虑sentence[i:j]
dp[i] = min(dp[i], dp[j+1])
else:
#认命,sentence[i:j]也是未识别字符
dp[i] = min(dp[i], dp[j+1]+j-i+1)
node = node.child[c]
return dp[0]
7.10 最佳买卖股票时机含冷冻期
原题地址.
分持有和未持有两种情况使用动态规划思想。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n= len(prices)
if not n:
return 0
#该天不持有和持有股票的收益
dp = [[0,0]for i in range(n)]
#第一天购入
dp[0][1] = -prices[0]
for i in range(1,n):
#当天不持有,卖出去了或者一直没有
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i])
#当天持有,一直持有或者当前买了
dp[i][1] = max(dp[i-1][1],dp[i-2][0]-prices[i])
return dp[-1][0]
7.11 计算右侧小于当前元素的个数
原题地址.
利用插入排序的思想,从原数组的尾元素开始往前遍历,插入并维护一个新的有序数组,插入位置就是该元素右侧小于它的元素个数。
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
sortns = []
res = []
for n in reversed(nums):
idx = bisect.bisect_left(sortns, n)
res.append(idx)
sortns.insert(idx,n)
return res[::-1]
7.12 地下城游戏
原题地址.
一个经典的dp问题,定义dp[i][j]
为从i行j列到最后右下角所需要的最少生命值。就可以写出对应的转移方程
class Solution:
def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
#初始化
n, m = len(dungeon), len(dungeon[0])
dp = [[float('inf')] * (m + 1) for _ in range(n + 1)]
#边界条件
dp[n][m - 1] = dp[n - 1][m] = 1
for i in range(n - 1, -1, -1):
for j in range(m - 1, -1, -1):
#过来所需要的初始值
minn = min(dp[i + 1][j], dp[i][j + 1])
#初始值必须大于1,dungeon可能加血
dp[i][j] = max(minn - dungeon[i][j], 1)
return dp[0][0]
7.13 两个数组的交集
原题地址.
终于又来简单题了。利用hash表完成常数实际访问,具体为以短数组构建hash表,遍历长数组判断是否在其中。
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
if len(nums1) > len(nums2):
#hash表用小数组更好
return self.intersect(nums2, nums1)
m = collections.Counter(nums1)
intersection = list()
#看另外一个数组元素是否在hash表
for num in nums2:
if num in m.keys():
intersection.append(num)
m[num] -= 1
if m[num] == 0:
m.pop(num)
return intersection
7.14 三角形最小路径和
原题地址.
本质上是一个经典的dp问题,而且每一行的路径答案只和上一行相关,所以可以进行状态压缩。
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
#dp[?][i] 根据?是prev或者curr储存当前行或者上一行i位置的路径最小和
dp = [[0] * n for _ in range(2)]
#初始值
dp[0][0] = triangle[0][0]
for i in range(1,n):
#判断当前使用的是那一行
curr, prev = i % 2, 1 - i % 2
#边界值
dp[curr][0] = dp[prev][0] + triangle[i][0]
dp[curr][i] = dp[prev][i-1] + triangle[i][i]
#状态转移
for j in range(1,i):
dp[curr][j] = min(dp[prev][j-1],dp[prev][j]) + triangle[i][j]
return min(dp[1 - n%2])
7.15不同的二叉搜索树
原题地址.
运用动态规划或者说是带记忆的递归解决。
class Solution:
def numTrees(self, n: int) -> int:
G = [0]*(n+1)
G[0], G[1] = 1, 1
for i in range(2,n + 1):
for j in range(0,i):
#左右子树节点数为j和i-1-j
G[i]+= G[j]*G[i-1-j]
return G[-1]
如果有足够的数学知识,会知道这是卡特兰数,可以直接用递推公式求解。
class Solution(object):
def numTrees(self, n):
C = 1
for i in range(0, n):
C = C * 2*(2*i+1)/(i+2)
return int(C)
7.16 判断二分图
原题地址
在遍历每一个边时,判断端点颜色是否符合规矩以及对未染色的节点染色。
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
n = len(graph)
UNCOLORED, RED, GREEN = 0, 1, 2
#存储颜色
color = [UNCOLORED] * n
valid = True
def dfs(node,c):
nonlocal valid
color[node] = c
#另外一个节点应该的颜色
cNei = (GREEN if c == RED else RED)
for neighbor in graph[node]:
#未染色的,染上这个颜色
if color[neighbor] == UNCOLORED:
dfs(neighbor, cNei)
if not valid:
return
#颜色发生冲突
elif color[neighbor] != cNei:
valid = False
return
for i in range(n):
#遍历所有节点,因为可能有多个子图
if color[i] == UNCOLORED:
dfs(i, RED)
if not valid:
break
return valid
7.17搜索插入位置
原题地址
没啥好说的,二分搜索注意一下边界条件。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
l = 0
r = len(nums) - 1
#<=的写法因为对终止循环前的元素判断了对于target的大小,l就是该插入的地方
while l <= r:
mid = (l+r)//2
if nums[mid] < target:
l = mid + 1
elif nums[mid] > target:
r = mid - 1
else:
#提前返回
return mid
else:
return l
7.18交错字符串
原题地址
利用动态规划解决,类别编辑距离处理。又可以用滚动数组优化二维dp数据。
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
m = len(s1)
n = len(s2)
t = len(s3)
if m+n != t:
return False
#dp数组初始化
dp = [False]*(n+1)
dp[0] = True
#原来二维i每一行之和上一行有关
for i in range(m+1):
for j in range(n+1):
p = i + j - 1
if i > 0:
dp[j] = dp[j] and s3[p] == s1[i-1]
if j > 0:
dp[j] = dp[j] or (dp[j-1] and s3[p] == s2[j-1])
return dp[-1]
7.19 戳气球
原题地址
自底向上得动态规划。dp[i][j]
记录以ij为左右边界得最大值。
class Solution:
def maxCoins(self, nums: List[int]) -> int:
n = len(nums)
rec = [[0] * (n + 2) for _ in range(n + 2)]
val = [1] + nums + [1]
for i in range(n - 1, -1, -1):
#右边界
for j in range(i + 2, n + 2):
#左边界
for k in range(i + 1, j):
#中间元素
total = val[i] * val[k] * val[j]
total += rec[i][k] + rec[k][j]
rec[i][j] = max(rec[i][j], total)
return rec[0][n+1]
7.20 两数之和Ⅱ
原题地址
使用双指针从两端向中间搜索。
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
i = 0
j = len(numbers) - 1
while numbers[i] + numbers[j] != target:
if numbers[i] + numbers[j] > target:
j -= 1
else:
i += 1
return [i+1,j+1]
7.21 不同的二叉搜索树Ⅱ
原题地址
递归成生成左右子树的小问题解决。
class Solution:
def generateTrees(self, n: int) -> List[TreeNode]:
def generateTrees(start, end):
if start > end:
return [None,]
res = []
for i in range(start,end + 1):
#递归
leftTrees = generateTrees(start, i - 1)
rightTrees = generateTrees(i + 1, end)
#拼接
for l in leftTrees:
for r in rightTrees:
currTree = TreeNode(i)
currTree.left = l
currTree.right = r
res.append(currTree)
return res
return generateTrees(1, n) if n else []
7.22 剑指 Offer 11. 旋转数组的最小数字
原题地址
其实就是寻找旋转数组旋转点,利用二分法可以方便的找到。
class Solution:
def minArray(self, numbers: List[int]) -> int:
l,r = 0,len(numbers)-1
while l < r:
mid = (l+r)//2
if numbers[mid] > numbers[r]:
l = mid + 1
elif numbers[mid] < numbers[r]:
r = mid
else:
r -= 1
return numbers[l]
7.23 最小路径和
原题地址
常规的动态规划问题,利用滚动数组优化。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
dp = [0] * n
#一行行求解
for i in range(m):
for j in range(n):
if i == 0 and j != 0:
dp[j] = dp[j-1] + grid[i][j]
elif j ==0 and i !=0:
dp[j] = dp[j] + grid[i][j]
elif i ==0 and j == 0:
dp[j] = grid[i][j]
else:
dp[j] = min(dp[j],dp[j-1]) + grid[i][j]
return dp[-1]
7.24 除数博弈
原题地址
需要思考一个数的转化过程,拿到一个奇数运算后肯定变成一个偶数,拿到一个偶数之后肯定变成一个奇数;而最后拿到2这个偶数的获胜,拿到1这个奇数的就输了。所以先手偶数的就会获胜。
class Solution:
def divisorGame(self, N: int) -> bool:
return N%2==0
7.25 分割数组的最大值
原题地址
这种需要多次求解子问题的问题一般都用动态规划思想,设数组dp[i][j]
为前i个元素分为j段的结果,那么状态转移方程就是在i个元素中选择第k个划分为j-1段以及后面一段求和。
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
n = len(nums)
#dp初始化
dp = [[float('inf')] * (m + 1) for _ in range(n + 1)]
dp[0][0] = 0
#sub是序列和,用来快速求出段落之和
sub = [0]
for elem in nums:
sub.append(sub[-1] + elem)
for i in range(1,n+1):
for j in range(1,min(i,m) + 1):
for k in range(i):
#k分为2段选择其中大的。
dp[i][j] = min(dp[i][j],max(dp[k][j-1],sub[i]-sub[k]))
return dp[n][m]
7.26 矩阵中的最长递增路径
原题地址
经典的dfs搜索可以解决,但是会超时,所以加入记忆化搜索。
class Solution:
def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
direcs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
if not matrix:
return 0
#记忆化数组
meme = {}
def dfs(row: int, column: int) -> int:
best = 1
for dx, dy in direcs:
newRow, newColumn = row + dx, column + dy
if 0 <= newRow < rows and 0 <= newColumn < columns and matrix[newRow][newColumn] > matrix[row][column]:
#直接提取
if (newRow,newColumn) in meme:
best = max(best, meme[(newRow,newColumn)] + 1)
else:
#计算,仅第一次
best = max(best, dfs(newRow,newColumn) + 1)
meme[(row,column)] = best
return best
ans = 0
rows, columns = len(matrix), len(matrix[0])
for i in range(rows):
for j in range(columns):
ans = max(ans, dfs(i, j))
7.27 判断子序列
原题地址
因为在两个字符串中都要遍历,考虑使用双指针法遍历。
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
n, m = len(s), len(t)
#双指针分别在s,t中遍历
i = j = 0
while i < n and j < m:
#找到一个,s指针后移
if s[i] == t[j]:
i += 1
#不管有没有,t指针都要后移
j += 1
return i == n
如果需要处理很多个s判断子序列的话,可以用动态规划,先把t的信息储存。f[i][j]表示位置i之后第一个出现字母j的坐标
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
n, m = len(s), len(t)
#dp数组,第二维代表26个字母
f = [[0] * 26 for _ in range(m)]
f.append([m] * 26)
#填充数组
for i in range(m - 1, -1, -1):
for j in range(26):
f[i][j] = i if ord(t[i]) == j + ord('a') else f[i + 1][j]
add = 0
for i in range(n):
#无效位置
if f[add][ord(s[i]) - ord('a')] == m:
return False
add = f[add][ord(s[i]) - ord('a')] + 1
return True
7.28 二叉树的最大深度
原题地址
利用栈遍历,出栈时比较最大深度
class Solution:
def maxDepth(self, root: TreeNode) -> int:
stack = []
if root is not None:
stack.append((1, root))
res = 0
while stack:
current_depth, root = stack.pop()
if root:
res = max(res, current_depth)
stack.append((current_depth+1,root.left))
stack.append((current_depth+1,root.right))
return res
7.29 寻宝
原题地址
这题建议直接看题解.。。。
这道困难题让我的青春结束了。
7.30 整数拆分
原题地址
采用动态规划思想,数字i拆分成数字j和其余部分处理。
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0] * (n + 1)
for i in range(2, n + 1):
for j in range(i):
#子问题,j这个数字可以选择拆分或者不拆分。
dp[i] = max(dp[i],j * (i - j),dp[j]*(i-j))
return dp[n]
7.31 魔术索引
原题地址
暴力遍历也可以,但是既然是有序的,就要利用好这个性质,省略一下对比操作。
class Solution:
def findMagicIndex(self, nums: List[int]) -> int:
n = len(nums)
i = 0
while i < n:
if nums[i] == i:
return i
if nums[i] > i: # 此时我们可以排除索引i到nums[i-1]这一整段
i = nums[i] # 由于数组可以保持平稳,所以nums[i]这一元素不可排除
else:
i += 1
return -1