202007leetcode刷题记录
这个月的题好多DP,而且还没时间去学,有点懵逼,月底再复习一次吧。
有好几天没刷,咕咕咕。
32. 最长有效括号
题目要求:
给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。
思路:
用栈模拟。括号匹配第一时间就想到了栈,但是这一题要的是最长有效括号的长度,不能直接将括号扔到栈里,而是要把索引放到栈里。初始化一个栈,栈顶元素为-1
。遍历字符串:
- 遇到
'('
时,就将对应的索引入栈; - 遇到
')'
时,进行一次出栈操作,此时又可以分为两种情况:
- 如果此时栈为空,则说明这个
')'
没有与之匹配的'('
,那么就将这个')'
对应的索引入栈,其含义是:最后一个不匹配的右括号的索引,之后用于计算有效括号的长度 - 如果栈不空,则说明当前的
')'
可以匹配,则当前索引减去栈顶元素即为当前有效括号的长度。
class Solution:
def longestValidParentheses(self, s: str) -> int:
if not s:
return 0
n = len(s)
stack = [-1]
ans = 0
for i in range(n):
if s[i] == '(':
stack.append(i)
else:
stack.pop()
if not stack:
stack.append(i)
else:
ans = max(ans, i - stack[-1])
return ans
这题可以用DP做,之后再说吧。
35. 搜索插入位置
题目要求:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。
思路:
二分查找。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
left, right = 0, n
while left < right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid
elif nums[mid] < target:
left = mid + 1
return left
62. 不同路径
题目要求:
一个机器人位于一个m x n
网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
思路:
动态规划。依题意,任意一个网格只能从其左侧或者上方到达。而第一列和第一行只有一种方法可以达到,因此把整个数组都初始化为1
。
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1 for _ in range(n)] for _ in range(m)]
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]
63. 不同路径 II
题目要求:
一个机器人位于一个m x n
网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
思路:
动态规划。跟上题不同的是,第一行和第一列可能会有障碍物,因此不能直接全部初始化为1
。对于第一行和第一列来说,遇到障碍后之后的格子都无法到达了。
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
m, n = len(obstacleGrid), len(obstacleGrid[0])
if not m or not n:
return 0
if obstacleGrid[0][0] == 1:
return 0
dp = [[0] * n for _ in range(m)]
dp[0][0] = 1
for i in range(1, m):
if not obstacleGrid[i][0]:
dp[i][0] = dp[i - 1][0]
for j in range(1, n):
if not obstacleGrid[0][j]:
dp[0][j] = dp[0][j - 1]
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 0:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[-1][-1]
64. 最小路径和
题目要求:
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
思路:
跟前面两题是一个系列的。
class Solution:
def minPathSum(self, grid: [[int]]) -> int:
m, n = len(grid[0]), len(grid)
for i in range(n):
for j in range(m):
if not i and not j:
continue
elif i == 0:
grid[i][j] = grid[i][j - 1] + grid[i][j]
elif j == 0:
grid[i][j] = grid[i - 1][j] + grid[i][j]
else:
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1])
return grid[-1][-1]
104. 二叉树的最大深度
题目要求:
给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
思路:
递归。二叉树的深度就等于左子树的深度与右子树的深度的最大值再加上一。
class Solution:
def maxDepth(self, root):
if not root:
return 0
else:
lh = self.maxDepth(root.left)
rh = self.maxDepth(root.right)
return max(lh, rh) + 1
108. 将有序数组转换为二叉搜索树
题目要求:
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
思路:
这是一个有序的数组,不需要对搜索树进行调整,因此难度较低。既然要平衡二叉树,那每次将数组中间的那个元素拿来当根结点就好了,递归实现。
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> TreeNode:
if not nums:
return None
mid = (len(nums) - 1) // 2
root = TreeNode(nums[mid])
root.left = self.sortedArrayToBST(nums[:mid])
root.right = self.sortedArrayToBST(nums[mid + 1:])
return root
112. 路径总和
题目要求:
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明:叶子节点是指没有子节点的节点。
思路:
递归。每走一步,就把sum - root.val
的值传给递归函数;当遇到叶子结点时,就判断sum == root.val
。
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)
120. 三角形最小路径和
题目要求:
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点在这里指的是下标
与上一层结点下标
相同或者等于上一层结点下标 + 1
的两个结点。
思路:
动态规划。
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
dp = [[0] * n for _ in range(n)]
dp[0][0] = triangle[0][0]
for i in range(1, n):
dp[i][0] = dp[i - 1][0] + triangle[i][0]
for j in range(1, i):
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]
dp[i][i] = dp[i - 1][i - 1] + triangle[i][i]
return min(dp[n - 1])
167. 两数之和 II - 输入有序数组
题目要求:
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
- 返回的下标值(index1 和 index2)不是从零开始的。
- 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
思路:
双指针法。大了就小一点,小了就大一点。
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left, right = 0, len(numbers) - 1
while left < right:
total = numbers[left] + numbers[right]
if total == target:
return [left + 1, right + 1]
elif total < target:
left += 1
else:
right -= 1
268. 缺失数字
题目要求:
给定一个包含0, 1, 2, ..., n
中 n 个数的序列,找出0 ... n
中没有出现在序列中的那个数。
思路:
直接对数组求和,求与0, 1, 2, ..., n
的和的差,即为答案。
class Solution:
def missingNumber(self, nums: List[int]) -> int:
n = len(nums)
sum_ = n * (n + 1) // 2
sum_nums = sum(nums)
return sum_ - sum_nums
309. 最佳买卖股票时机含冷冻期
题目要求:
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
思路:
动态规划。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices:
return 0
n = len(prices)
f = [[-prices[0], 0, 0]] + [[0] * 3 for _ in range(n - 1)]
for i in range(1, n):
f[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i])
f[i][1] = f[i - 1][0] + prices[i]
f[i][2] = max(f[i - 1][1], f[i - 1][2])
return max(f[n - 1][1], f[n - 1][2])
315. 计算右侧小于当前元素的个数
题目要求:
给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质:counts[i]
的值是nums[i]
右侧小于nums[i]
的元素的数量。
思路:
反向遍历数组,将遍历过的数字放到有序数组中,用二分查找的方式找到插入位置,也就是比当前数小的个数。
import bisect
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
if not nums:
return []
n = len(nums)
counts = [0] * n
ordered = []
for x in nums[::-1]:
pos = bisect.bisect_left(ordered, x)
counts[i] = pos
bisect.insort(ordered, x)
return counts
329. 矩阵中的最长递增路径
题目要求:
给定一个整数矩阵,找出最长递增路径的长度。对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。
思路:
动态规划。首先将矩阵转换为三元组的形式,再对其进行排序,从小的开始动态规划。状态转移方程为dp[i][j] = max(dp[i][j], 1 + dp[r][c])
,dp[r][c]
为其周围四个元素的路径长度。
class Solution:
def longestIncreasingPath(self, matrix):
if not matrix or not matrix[0]:
return 0
m, n = len(matrix), len(matrix[0])
from itertools import product
matrix_copy = [(matrix[i][j], i, j) for i, j in product(range(m), range(n))]
matrix_copy.sort()
dp = [[0] * n for _ in range(m)]
for num, i, j in matrix_copy:
dp[i][j] = 1
for di, dj in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
r, c = i + di, j + dj
if 0 <= r < m and 0 <= c < n:
if matrix[i][j] > matrix[r][c]:
dp[i][j] = max(dp[i][j], 1 + dp[r][c])
return max([dp[i][j] for i, j in product(range(m), range(n))])
343. 整数拆分
题目要求:
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
思路:
这个动态规划就比较好懂了。例如一个数n
,将其拆分为n = a + b
,那么此时其乘积可以看作dp[n] = a * dp[b]
,有点穷举内味。
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0] * (n + 1)
for i in range(2, n + 1):
for j in range(i):
dp[i] = max(dp[i], j * (i - j), j * dp[i - j])
return dp[-1]
349. 两个数组的交集
题目要求:
给定两个数组,编写一个函数来计算它们的交集。
说明:
- 输出结果中的每个元素一定是唯一的。
- 我们可以不考虑输出结果的顺序。
思路:
哈希表或者直接用Python的集合。题目要求如果出现多次只要输出一次就行了,因此用集合或者哈希表是可行的。
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
set1, set2 = set(nums1), set(nums2)
return [x for x in set1 if x in set2]
350. 两个数组的交集 II
题目要求:
给定两个数组,编写一个函数来计算它们的交集。
说明:
- 输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。
- 我们可以不考虑输出结果的顺序。
思路:
哈希表。这题需要考虑次数,可以先遍历其中一个数组,用哈希表记录其中每个数出现的次数。再遍历另一个数组,每次出现哈希表中已经有的数字(key),如果其次数大于0
,则将次数(value)减一,并加到答案中。
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
if len(nums1) > len(nums2):
return self.intersect(nums2, nums1)
hashmap = {}
ans = []
for x in nums1:
if x not in hashmap:
hashmap[x] = 1
else:
hashmap[x] += 1
for x in nums2:
if x in hashmap and hashmap[x] > 0:
ans.append(x)
hashmap[x] -= 1
return ans
378. 有序矩阵中第K小的元素
题目要求:
给定一个n x n
矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k
小的元素。
请注意,它是排序后的第k
小元素,而不是第k
个不同的元素。
思路:
矩阵的每一行都是有序的,第一时间想到的是用归并。给每一行安排一个指针,找这些指针对应的最小元素,再将对应的指针后移(注意边界)。
class Solution:
def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
n = len(matrix)
idx = [0] * n
for i in range(k):
index = 0
ans = matrix[-1][-1]
for j in range(n):
if idx[j] < n and ans >= matrix[j][idx[j]]:
index = j
ans = matrix[j][idx[j]]
idx[index] += 1
return ans
当时这个东西还是得用二分查找才能发挥出这个矩阵性质的好处,以后有空在说吧。
392. 判断子序列
题目要求:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
思路:
上个月刚做过一次。遍历长的字符串,用一个指针指向短字符串中的字符。当两个字符相等时,指针右移。若最终指针移至最后,说明是子序列。
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
m, n = len(s), len(t)
if m == 0:
return True
ptr = 0
for i in range(n):
if t[i] == s[ptr]:
ptr += 1
if ptr == m:
return True
return False
410. 分割数组的最大值
题目要求:
给定一个非负整数数组和一个整数m
,你需要将这个数组分成m
个非空的连续子数组。设计一个算法使得这m
个子数组各自和的最大值最小。
注意:
数组长度n
满足以下条件:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
思路:
将数组分为m
个连续子数组,就很动态规划。令dp[i][j]
表示将数组的前 i 个数分割为 j 段所能得到的最大连续子数组和的最小值。可以先求出其前缀和,以便后面求子数组的和。
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
n = len(nums)
dp = [[10 ** 18] * (m + 1) for _ in range(n + 1)]
sub = [0]
for elem in nums:
sub.append(sub[-1] + elem)
dp[0][0] = 0
for i in range(1, n + 1):
for j in range(1, min(i, m) + 1):
for k in range(i):
dp[i][j] = min(dp[i][j], max(dp[k][j - 1], sub[i] - sub[k]))
return dp[-1][-1]
718. 最长重复子数组
题目要求:
给两个整数数组A
和B
,返回两个数组中公共的、长度最长的子数组的长度。
思路:
动态规划。这题比较基础,还是能想到解法的。但是这个月好多DP,很懵逼。
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
m, n = len(A), len(B)
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
ans = 0
for i in range(m - 1, -1, -1):
for j in range(n - 1, -1, -1):
dp[i][j] = dp[i + 1][j + 1] + 1 if A[i] == B[j] else 0
ans = max(ans, dp[i][j])
return ans
785. 判断二分图
题目要求:
给定一个无向图graph
,当这个图为二分图时返回true
。
如果我们能将一个图的节点集合分割成两个独立的子集A和B,并使图中的每一条边的两个节点一个来自A集合,一个来自B集合,我们就将这个图称为二分图。
graph
将会以邻接表方式给出,graph[i]
表示图中与节点i
相连的所有节点。每个节点都是一个在0
到graph.length - 1
之间的整数。这图中没有自环和平行边:graph[i]
中不存在i
,并且graph[i]
中没有重复的值。
思路:
并查集。
class UFS:
def __init__(self, length):
self.p = [i for i in range(length)]
self.rank = [0] * length
def find(self, x):
if self.p[x] != x:
self.p[x] = self.find(self.p[x])
return self.p[x]
def union(self, x, y):
rx, ry = self.find(x), self.find(y)
if self.rank[rx] < self.rank[ry]:
self.p[rx] = ry
elif self.rank[rx] > self.rank[ry]:
self.p[ry] = rx
else:
self.p[rx] = ry
self.rank[ry] += 1
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
ufs = UFS(len(graph))
for i, g in enumerate(graph):
root = ufs.find(i)
for j in range(len(g)):
if root == ufs.find(g[j]):
return False
for j in range(1, len(g)):
ufs.union(g[j - 1], g[j])
return True
1025. 除数博弈
题目要求:
爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。
最初,黑板上有一个数字N
。在每个玩家的回合,玩家需要执行以下操作:
- 选出任一
x
,满足0 < x < N
且N % x == 0
。 - 用
N - x
替换黑板上的数字N
。
如果玩家无法执行这些操作,就会输掉游戏。只有在爱丽丝在游戏中取得胜利时才返回True
,否则返回False
。假设两个玩家都以最佳状态参与游戏。
思路:
用动态规划来模拟这个过程。
class Solution:
def divisorGame(self, N: int) -> bool:
dp = [False] * (N + 2)
dp[2] = True
if N < 3:
return dp[N]
else:
for i in range(3, N + 1):
for j in range(1, i // 2):
if i % j == 0 and dp[i - j] == False:
dp[i] = True
break
return dp[N]
还有找规律大法。。。
class Solution:
def divisorGame(self, N: int) -> bool:
return N % 2 == 0
剑指 Offer 11. 旋转数组的最小数字
题目要求:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组[3,4,5,1,2]
为[1,2,3,4,5]
的一个旋转,该数组的最小值为1。
思路:
二分查找。要判断nums[mid]
左右元素的大小,就能知道谁是被转过去的那个“支点”。
class Solution:
def minArray(self, numbers: [int]) -> int:
n = len(numbers)
left, right = 0, n - 1
while left < right:
mid = left + (right - left) // 2
if numbers[mid] > numbers[right]:
left = mid + 1
elif numbers[mid] < numbers[right]:
right = mid
else:
right -= 1
return numbers[left]
面试题 08.03. 魔术索引
题目要求:
魔术索引。 在数组A[0...n-1]
中,有所谓的魔术索引,满足条件A[i] = i
。给定一个有序整数数组,编写一种方法找出魔术索引,若有的话,在数组A中找出一个魔术索引,如果没有,则返回-1。若有多个魔术索引,返回索引值最小的一个。
思路:
直接遍历就好了,花里胡哨的。
class Solution:
def findMagicIndex(self, nums: List[int]) -> int:
n = len(nums)
for i in range(n):
if nums[i] == i:
return i
return -1
思路二:
直接遍历虽然很好理解,但是题中有一个条件是有序整数数组,没有用到这个特性,就很浪费。但是题中没有说数组中没有重复的元素,如果其中的元素都是不重复的,那么如果nums[mid] > mid --> nums[mid] >= mid + 1
,于是就有nums[mid + 1] > nums[mid] >= mid + 1
,由此可见,魔法索引不可能在mid
的右边。而本题可以有重复元素,上面的推导就不成立了。
那么要怎么利用该数组有序的性质呢?我们可以基于直接遍历来优化。举个例子,如果nums[0] = 10
,那么nums[1] ~ nums[9]
还有可能是魔术索引吗?因为数组中的元素升序排列,这是显然不可能的。这时候至少得到nums[10]
处才有可能找到魔术索引。利用数组升序的性质,可以跳过一部分多余的索引,不过最差情况下还是跟直接遍历是一样的复杂度。
class Solution:
def findMagicIndex(self, nums: List[int]) -> int:
if nums[0] == 0:
return 0
p, n = 0, len(nums)
while p < n:
if nums[p] > p:
p = nums[p]
elif nums[p] == p:
return p
else:
p += 1
return -1
面试题 16.11. 跳水板
题目要求:
你正在使用一堆木板建造跳水板。有两种类型的木板,其中长度较短的木板长度为shorter
,长度较长的木板长度为longer
。你必须正好使用k块木板。编写一个方法,生成跳水板所有可能的长度。
返回的长度需要从小到大排列。
思路:
这个题,不能想太多,就两种长度的木板,排列一下就行了。
class Solution:
def divingBoard(self, shorter: int, longer: int, k: int) -> List[int]:
if k == 0:
return []
if shorter == longer:
return [shorter * k]
diff = longer - shorter
return [k * shorter + diff * i for i in range(k + 1)]
面试题 17.13. 恢复空格
题目要求:
哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"I reset the computer. It still didn’t boot!"
已经变成了"iresetthecomputeritstilldidntboot"
。在处理标点符号和大小写之前,你得先把它断成词语。当然了,你有一本厚厚的词典dictionary
,不过,有些词没在词典里。假设文章用sentence
表示,设计一个算法,把文章断开,要求未识别的字符最少,返回未识别的字符数。
提示:
0 <= len(sentence) <= 1000
dictionary
中总字符数不超过150000。- 你可以认为
dictionary
和sentence
中只包含小写字母。
思路:
动态规划。将dictionary
中的元素放到字典中,并求出其长度作为对应键的值。如果在当前字符之后的子串中都找不到字典中的单词,那就说明这是一个无法识别的字符。
class Solution:
def respace(self, dictionary: List[str], sentence: str) -> int:
d = {}
for i in dictionary:
d[i] = len(i)
n = len(sentence)
dp = [0] * (n + 1)
for i in range(n - 1, -1, -1):
dp[i] = dp[i + 1] + 1
for word in d:
if sentence[i: i + d[word]] == word:
dp[i] = min(dp[i], dp[i + d[word]])
return dp[0]