写了也挺久的了吧,今天终于把牛客TOP101道题给写完了,算是有了一些基础编程能力了吧。因为大大小小的事耽误着,其实还写了蛮多天的,写完确实能感觉到有所收获。按时间顺序,也其实就是题目的顺序把剩下的解法都记录在这里。
9月20日
BM 55 没有重复项数字的全排列
url:牛客
BM55
考察知识点:回溯法
做了这题之后我才发现全排列就是原来数组进行两两交换之后的所有结果,当然也可以自己和自己交换,这样就是这步操作不改变结果。那第\(i\) 次操作时总共有和后面\(n-i+1\) 个元素(包括这个元素自己)进行交换的机会。
这个按字典序的意思就是按a-z,0-9的顺序排序,也就是在做之前排序一下元素即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution : def permute_item (self, base_num_list: List [int ], pos: int , result: List [List [int ]] ): if pos == len (base_num_list)-1 : result.append(base_num_list) return None for i in range (pos, len (base_num_list)): num_list = base_num_list.copy() num_list[pos], num_list[i] = num_list[i], num_list[pos] self.permute_item(num_list, pos+1 , result) def permute (self , num: List [int ] ) -> List [List [int ]]: result = [] num.sort() self.permute_item(num, 0 , result) return result
用以前学的回溯法框架写了一下,这次不是两两交换,而是逐个加入直到全部加入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class Solution : result = [] def backtrack (self, v ): if len (v) == 0 : print ("the end..." ) else : del v[-1 ] def advance (self, v ): visited = set (v) return visited def get_all_next_adj (self, visited, num ): wait_list = [] for i, item in enumerate (num): if item in visited: continue wait_list.append(i) return wait_list def dfs (self, v, num ): visited = self.advance(v) n = len (num) if len (v) == len (num): self.result.append(v.copy()) print (self.result) else : wait_list = self.get_all_next_adj(visited, num) print (wait_list) e = 0 while e < len (wait_list): v.append(num[wait_list[e]]) self.dfs(v, num) e += 1 self.backtrack(v) def permute (self , num: List [int ] ) -> List [List [int ]]: num.sort() self.dfs([], num) return self.result
BM 56 有重复项的全排列
url:牛客
BM 56
考察知识点:回溯法
似乎之前的BM55没有按字典序排序也过了,但BM56不行。我没想到怎么在中途对结果排序,于是就自己定义了一下比较方法然后在后面自己排序成字典序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 class Solution : has_visited = set () def permute_item (self, num: List [int ], pos: int , result: List [List [int ]] ): if pos == len (num)-1 : if "" .join([str (n) for n in num]) not in self.has_visited: result.append(num.copy()) self.has_visited.add("" .join([str (n) for n in num])) return None for i in range (pos, len (num)): if i != pos and num[i] == num[pos]: continue num[i], num[pos] = num[pos], num[i] self.permute_item(num, pos+1 , result) num[pos], num[i] = num[i], num[pos] def compare_result (self, r1: List [int ], r2: List [int ] ): for i in range (len (r1)): if r1[i] < r2[i]: return 1 elif r1[i] > r2[i]: return 0 else : continue def sort_to_dict (self, result: List [List [int ]] ): N = len (result) for i in range (N): min_ = i for j in range (i+1 , N): if self.compare_result(result[j], result[min_]): min_ = j result[min_], result[i] = result[i], result[min_] return result def permuteUnique (self , num: List [int ] ) -> List [List [int ]]: num.sort() self.has_visited = set () result = [] self.permute_item(num, 0 , result) return self.sort_to_dict(result)
能不能用列表自带的sort进行排序,然后指定比较接口呢?是可以的,但是有点烦,因为原来的key虽然支持传入函数,但是这个key的函数是要求在对元素计算一个可比较的数,要传入比较接口有两种办法,一种就是我们自己包装一下要比较的元素(我更推荐这种做法,因为后面字典也会要用到__hash__和__eq__来计算哈希值)。另一种就是用Python在functools里提供的cmp_to_key完成。第二种方法演示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 from functools import cmp_to_keydef compare_result (r1: List [int ], r2: List [int ] ): for i in range (len (r1)): if r1[i] < r2[i]: return -1 elif r1[i] > r2[i]: return 1 else : continue class Solution : has_visited = set () def permute_item (self, num: List [int ], pos: int , result: List [List [int ]] ): if pos == len (num)-1 : if "" .join([str (n) for n in num]) not in self.has_visited: result.append(num.copy()) self.has_visited.add("" .join([str (n) for n in num])) return None for i in range (pos, len (num)): if i != pos and num[i] == num[pos]: continue num[i], num[pos] = num[pos], num[i] self.permute_item(num, pos+1 , result) num[pos], num[i] = num[i], num[pos] def permuteUnique (self , num: List [int ] ) -> List [List [int ]]: num.sort() self.has_visited = set () result = [] self.permute_item(num, 0 , result) result.sort(key=cmp_to_key(compare_result)) return result
BM 57 岛屿数量
url:牛客
BM57
考察知识点:回溯法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution : def solve (self , grid: List [List [str ]] ) -> int : num_of_island = 0 n = len (grid) m = len (grid[0 ]) def dfs (grid: List [List [str ]], i: int , j: int ): grid[i][j] = "0" if j > 0 and grid[i][j-1 ] == "1" : dfs(grid, i, j-1 ) if j < m-1 and grid[i][j+1 ] == "1" : dfs(grid, i, j+1 ) if i > 0 and grid[i-1 ][j] == "1" : dfs(grid, i-1 , j) if i < n-1 and grid[i+1 ][j] == "1" : dfs(grid, i+1 , j) for l1 in range (n): for l2 in range (m): if grid[l1][l2] == "1" : num_of_island += 1 dfs(grid, l1, l2) return num_of_island
BM 58 字符串的排列
url:牛客
BM58
考察知识点:回溯法、排列问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution : def Permutation (self , str : str ) -> List [str ]: str_list = list (str ) N = len (str_list) result = set () def dfs (str_list: List [str ], pos: int , result ): if pos == N - 1 : target = "" .join(str_list) result.add(target) return None for i in range (pos, N): str_list[i], str_list[pos] = str_list[pos], str_list[i] dfs(str_list, pos+1 , result) str_list[pos], str_list[i] = str_list[i], str_list[pos] dfs(str_list, 0 , result) return list (result)
BM 59 N皇后问题
url:牛客
BM59
考察知识点:回溯法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class Solution : def Nqueen (self , n: int ) -> int : num_of_solution = 0 def exceedBound (col: List [int ] ): if len (col) <= 1 : return False else : q2 = [len (col)-1 , col[-1 ]] for i in range (len (col)-1 ): q1 = [i, col[i]] if abs (q1[0 ] - q2[0 ]) == abs (q1[1 ] - q2[1 ]): return True return False def dfs (col: List [int ] ): nonlocal num_of_solution, n if len (col) == n: num_of_solution += 1 else : j = 0 while j < n: if j in col: j += 1 continue if not exceedBound(col + [j]): col.append(j) dfs(col) j += 1 if len (col) > 0 : del col[-1 ] dfs([]) return num_of_solution
BM 60 括号生成
url:牛客
BM60
考察知识点:回溯法、栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Solution : def generateParenthesis (self , n: int ) -> List [str ]: str_list = [] result = [] def isValid (str_list ): stack = [] for i in range (len (str_list)): if str_list[i] == "(" : stack.append("(" ) else : if len (stack) > 0 and stack[-1 ] == "(" : stack.pop() else : return False return True def dfs (str_list, r_num ): if len (str_list) == 2 * n: result.append("" .join(str_list)) else : next_list = ["(" , ")" ] for item in next_list: if isValid(str_list + [item]): if item == "(" : if r_num + 1 > n: continue str_list.append(item) dfs(str_list, r_num+1 ) else : str_list.append(item) dfs(str_list, r_num) if len (str_list) > 0 : del str_list[-1 ] dfs([], 0 ) return result
9月21日
BM 61 矩阵最长递增路径
url:牛客
BM61
考察知识点:回溯法、动态规划
先来一个暴力回溯的方法,这个方法从起点一直找递增路径,然后保存最长递增路径的长度并返回。复杂度很高,过不了所有的用例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 class Solution : def solve (self , matrix: List [List [int ]] ) -> int : n = len (matrix) m = len (matrix[0 ]) max_path_length = 0 def isLeaf (v ): if len (v) == 0 : return False now_i, now_j = v[-1 ] now_v = matrix[now_i][now_j] if now_i > 0 and matrix[now_i-1 ][now_j] > now_v: return False elif now_j > 0 and matrix[now_i][now_j-1 ] > now_v: return False elif now_i < n-1 and matrix[now_i+1 ][now_j] > now_v: return False elif now_j < m-1 and matrix[now_i][now_j+1 ] > now_v: return False else : return True def all_next_point (v ): points = [] if len (v) == 0 : for i in range (n): for j in range (m): points.append([i, j]) else : now_i, now_j = v[-1 ] now_v = matrix[now_i][now_j] if now_i > 0 and matrix[now_i-1 ][now_j] > now_v: points.append([now_i-1 , now_j]) if now_j > 0 and matrix[now_i][now_j-1 ] > now_v: points.append([now_i, now_j-1 ]) if now_i < n-1 and matrix[now_i+1 ][now_j] > now_v: points.append([now_i+1 , now_j]) if now_j < m-1 and matrix[now_i][now_j+1 ] > now_v: points.append([now_i, now_j+1 ]) return points def dfs (v ): nonlocal max_path_length if isLeaf(v): if len (v) > max_path_length: max_path_length = len (v) else : points = all_next_point(v) for point in points: v.append(point) dfs(v) if len (v) > 0 : del v[-1 ] dfs([]) return max_path_length
动态规划可以做这题。原因是地图确定后,从某个点出发能到达的最长路径值是固定的,而且和从哪来的没关系。这就使得这个问题具有最优子结构性质,即最优解中也存在所有子问题的最优解。比如一个路径是"1->3->5"是最长递增路径,那么从"1->3"和"3->5"必不可能存在更长的递增路径了,否则原路径就不是最长递增路径。通过这个性质我们可以把这个问题抽象为从一个点出发的最长路径是所有下一个能访问点的最长路径长度加一的最大值,用程序可以表示为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Solution : def solve (self , matrix: List [List [int ]] ) -> int : n = len (matrix) m = len (matrix[0 ]) dp = [[0 for j in range (m)] for i in range (n)] next_ij = [(-1 , 0 ), (1 , 0 ), (0 , -1 ), (0 , 1 )] def dfs (i, j ): if dp[i][j] != 0 : return dp[i][j] dp[i][j] += 1 for k in range (4 ): next_i = i + next_ij[k][0 ] next_j = j + next_ij[k][1 ] if next_i >= 0 and next_i < n and \ next_j >= 0 and next_j < m and \ matrix[next_i][next_j] > matrix[i][j]: dp[i][j] = max (dfs(next_i, next_j)+1 , dp[i][j]) return dp[i][j] for i in range (n): for j in range (m): dfs(i, j) print (dp) return max (map (max , dp))
BM 62 斐波拉契数列
url:牛客
BM62
考察知识点:动态规划
非常经典的一道题,其递归表达式就说明非常适合动态规划求解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def Fibonacci (self , n: int ) -> int : dp = [0 for i in range (n)] for i in range (n): if i == 0 or i == 1 : dp[i] = 1 else : dp[i] = dp[i-1 ] + dp[i-2 ] return dp[-1 ]
BM 63 跳台阶
url:牛客
BM63
考察知识点:动态规划
这种题目就可以反着思考,比如我在离终点一格的地方有几种跳法呢,那就一种对吧,离终点两格的地方有2种跳法。那我要跳到离终点一格或者两格近的地方需要有几种跳法,这样递归思考,之后总跳法是各种跳法的和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution : def jumpFloor (self , number: int ) -> int : dp = [0 for i in range (number)] def jump (k: int , number: int ): if dp[k] != 0 : return dp[k] diff = number - k if diff <= 0 : return 0 elif diff == 1 : dp[k] = 1 elif diff == 2 : dp[k] = 2 else : dp[k] = jump(k+1 , number) + jump(k+2 , number) return dp[k] jump(0 , number) return dp[0 ]
BM 64 最小花费爬楼梯
url:牛客
BM64
考察知识点:动态规划
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution : def minCostClimbingStairs (self , cost: List [int ] ) -> int : n = len (cost) dp = [0 for i in range (n+1 )] def climb (k: int ): if dp[k] != 0 : return dp[k] if k == 0 or k == 1 : dp[k] = 0 else : dp[k] = min (climb(k-1 ) + cost[k-1 ], climb(k-2 ) + cost[k-2 ]) return dp[k] return climb(n)
BM 65 最长公共子序列(二)
url:牛客
BM65
考察知识点:动态规划、最长公共子序列
感觉可以写个专题,因为确实有点东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Solution : def LCS (self , s1: str , s2: str ) -> str : n = len (s1) m = len (s2) dp = [[0 ]*(m+1 ) for i in range (n+1 )] for i in range (1 , n+1 ): for j in range (1 , m+1 ): if s1[i-1 ] == s2[j-1 ]: dp[i][j] = dp[i-1 ][j-1 ] + 1 else : dp[i][j] = max (dp[i][j-1 ], dp[i-1 ][j]) i, j = n, m k = dp[i][j] lcs = "" while i > 0 and j > 0 and k > 0 : while dp[i][j] == dp[i-1 ][j] and i > 0 : i = i - 1 while dp[i][j] == dp[i][j-1 ] and j > 0 : j = j - 1 lcs = s1[i-1 ] + lcs i -= 1 j -= 1 k -= 1 return lcs if lcs else "-1"
BM 66 最长公共子串
url:牛客
BM66
考察知识点:动态规划
家人们谁懂啊,同样的算法就Python超时...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution : def LCS (self , str1: str , str2: str ) -> str : n = len (str1) m = len (str2) dp = [[0 ]*(m+1 ) for i in range (n+1 )] max_, max_i = 0 , 0 for i in range (1 , n+1 ): for j in range (1 , m+1 ): if str1[i-1 ] == str2[j-1 ]: dp[i][j] = dp[i-1 ][j-1 ] + 1 if dp[i][j] > max_: max_i = i max_ = dp[i][j] else : pass return str1[max_i-max_: max_i]
官方提供的非动态规划版本,理论时间复杂度是\(O(m^2n)\) ,但是却能过...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution : def LCS (self , str1: str , str2: str ) -> str : if len (str1) < len (str2): str1, str2 = str2, str1 res = '' max_len = 0 for i in range (len (str1)): if str1[i - max_len : i + 1 ] in str2: res = str1[i - max_len : i + 1 ] max_len += 1 return res
BM 67 不同路径的数目(一)
url:牛客
BM67
考察知识点:排列组合、动态规划
这道题还是有点意思,直接从左上角移动到右下角,只能进行向下或者向右的操作。路径的数目只和向下和向右的组合顺序有关,对于\(m*n\) 的图,向下操作要进行\(m-1\) 次,向右操作要进行\(n-1\) 次。所以路径总数相当于把\(m-1\) 个小球放入\(m-n-2\) 个位置里,即有: \[
C_{m-n-2}^{m-1}=\frac{(m-n-2)!}{(m-1)!(n-1)!}
\] 种不同的路径。
写代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def uniquePaths (self , m: int , n: int ) -> int : import math return math.factorial(m+n-2 ) // (math.factorial(m-1 ) * math.factorial(n-1 ))
如果要用动态规划老实操作呢,也可以做到。需要注意的是从出生点开始走只有1种走法,然后如果走到一个点那个点的走法种数是其左边和上边的走法之和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution : def uniquePaths (self , m: int , n: int ) -> int : dp = [[1 ]*n for i in range (m)] for i in range (m): for j in range (n): if i > 0 and j > 0 : dp[i][j] = dp[i-1 ][j] + dp[i][j-1 ] elif i > 0 : dp[i][j] = dp[i-1 ][j] elif j > 0 : dp[i][j] = dp[i][j-1 ] return dp[m-1 ][n-1 ]
9月22日
BM 68 矩阵最小路径和
url:牛客
BM68
考察知识点:动态规划
和BM67类似,理清动态规划的递推关系即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution : def minPathSum (self , matrix: List [List [int ]] ) -> int : n = len (matrix) m = len (matrix[0 ]) dp = [[0 ]*m for i in range (n)] for i in range (n): for j in range (m): if i == 0 and j == 0 : dp[i][j] = matrix[i][j] elif i == 0 : dp[i][j] = dp[i][j-1 ] + matrix[i][j] elif j == 0 : dp[i][j] = dp[i-1 ][j] + matrix[i][j] else : dp[i][j] = min (dp[i-1 ][j], dp[i][j-1 ]) + matrix[i][j] return dp[n-1 ][m-1 ]
BM 69 把数字翻译成字符串
url:牛客
BM69
考察知识点:动态规划
这个题目是很体现重叠子问题的一道题,不管先解码1个字符两次或者一次解码2个字符,后面的字符都一样而且需要多次解码。如果能集下后面字符的解码结果就能大大加速计算速度。可以用一个字典保存已经解码过的字符串从而避免重复解决重叠子问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class Solution : def solve (self , nums: str ) -> int : dp = {} def decode (s ): if len (s) == 1 : if s[0 ] != "0" : return 1 else : return 0 elif len (s) == 2 : if s[0 ] == "0" : return 0 elif int (s) <= 26 : if s[1 ] != "0" : return 2 else : return 1 elif int (s) > 26 : if s[1 ] != "0" : return 1 else : return 0 else : prefix = s[:2 ] num = 0 if s in dp: return dp[s] if prefix[0 ] == "0" : num = 0 elif int (prefix) <= 26 : if prefix[1 ] == "0" : num = decode(s[2 :]) else : num = decode(s[2 :]) + decode(s[1 :]) else : num = decode(s[1 :]) dp[s] = num return num return decode(nums)
看了题解,貌似题解还有问题,解码“120”的时候官方的题解结果是2,但实际是1。其原因就是当前数字是"0"是一个特殊的分支,它要单独考虑,看了评论区的一个题解不错,注释也挺清楚,贴上来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution : def solve (self , nums: str ) -> int : l = len (nums) dp = [1 for _ in range (l+1 )] for i in range (1 , l+1 ): if i == 1 : if nums[i-1 ] == '0' : return 0 else : if nums[i-1 ] == '0' : if (int (nums[i-2 ]) > 2 or nums[i-2 ] == '0' ): return 0 else : dp[i] = dp[i-2 ] else : if nums[i-2 ] == '0' or int (nums[i-2 ]+nums[i-1 ]) > 26 : dp[i] = dp[i-1 ] else : dp[i] = dp[i-2 ] + dp[i-1 ] return dp[-1 ]
BM 70 兑换零钱(一)
url:牛客
BM70
考察知识点:动态规划
先看一下不要求零钱数为整数的解法,该解法通过字典来记录子问题的解法,我觉得这道题用这种方法灵活度更高一些。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution : def minMoney (self , arr: List [int ], aim: int ) -> int : arr.sort(reverse=True ) dp = {0 : 0 } def give_change (aim ): if aim in dp: return dp[aim] result = [] for i in range (len (arr)): if aim >= arr[i]: pr = give_change(aim-arr[i]) if pr != -1 : result.append(1 + pr) if len (result) == 0 : dp[aim] = -1 return -1 else : dp[aim] = min (result) return dp[aim] return give_change(aim)
当然,aim为整数也可以利用这个性质为aim建个动态规划的数组,更省空间一些。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution : def minMoney (self , arr: List [int ], aim: int ) -> int : dp = [-1 for i in range (aim+1 )] dp[0 ] = 0 for i in range (aim+1 ): if i in arr: dp[i] = 1 else : result = [] for j in arr: if j <= i and dp[i-j] != -1 : result.append(dp[i-j] + 1 ) if len (result) > 0 : dp[i] = min (result) return dp[aim]
BM 71 最长上升子序列(一)
url:牛客
BM71
考察知识点:动态规划
动态规划里很经典的题,思考的方式是先看看之前的数组中的上升子序列能不能拼入该元素,然后取能拼入该元素的最长上升子序列长度作为以该元素结尾的最长上升子序列长度。整个问题的答案是dp数组中的最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def LIS (self , arr: List [int ] ) -> int : dp = [1 for i in range (len (arr))] for i in range (len (arr)): tmp_max_length = 1 for j in range (i): if arr[j] < arr[i] and dp[j] + 1 > tmp_max_length: tmp_max_length = dp[j] + 1 dp[i] = tmp_max_length return max (dp) if len (dp) > 0 else 0
BM 72 连续子数组的最大和
url:牛客
BM72
考察知识点:动态规划、贪心算法
核心是观察出对于某个位置结尾的和最大子数组,它要么是前一个元素子数组的最大和子数组拼上自己,要么是它本身。那可以写出动态规划版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def FindGreatestSumOfSubArray (self , array: List [int ] ) -> int : dp = [-101 for i in range (len (array))] for i in range (len (array)): if i == 0 : dp[i] = array[0 ] else : dp[i] = max (dp[i-1 ]+array[i], array[i]) return max (dp)
当然,也可以取缔掉整个dp数组转而只维护之前的子数组最大值和全局子数组最大值,因为我们只关心dp数组中的上一个值和最大值嘛。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution : def FindGreatestSumOfSubArray (self , array: List [int ] ) -> int : last_max = array[0 ] max_value = array[0 ] for i in range (1 , len (array)): if last_max > 0 : last_max = last_max + array[i] else : last_max = array[i] if last_max > max_value: max_value = last_max return max_value
BM 73 最长回文字符串
url:BM
73
考察知识点:动态规划
先用暴力解法做一遍,关键是判断回文字符串。所谓回文字符串就是正向遍历和反向遍历相同的字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def getLongestPalindrome (self , A: str ) -> int : max_length = 0 n = len (A) for i in range (n): for j in range (i, n): if (i == 0 and A[i:j+1 ] == A[j: : -1 ]) or \ (i > 0 and A[i:j+1 ] == A[j: i-1 : -1 ]): if j - i + 1 > max_length: max_length = j - i + 1 return max_length
这里判断回文字符串实际上还需要一次循环,因此这个代码复杂度实际来到\(O(n^3)\) 了,比较坏。可以使用扩散中心法来确定回文串长度,所谓扩散中心法就是把当前(或当前元素及其前一个元素)当作回文串的中心,然后向两边扩散直到不满足回文串定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution : def getLongestPalindrome (self , A: str ) -> int : max_length = 1 n = len (A) def helper (left: int , right: int ): while left >= 0 and right < n and A[right] == A[left]: left -= 1 right += 1 return right - left - 1 for i in range (1 , n): cur = max (helper(i, i), helper(i-1 , i)) if cur > max_length: max_length = cur return max_length
动态规划的写法稍微有点点麻烦,不过核心是\(dp[i][j]\) 代表什么,如果\(dp[i][j]\) 代表以i开头,以j结尾的字符子串是否为回文字符串的话就会豁然开朗。当A[i]=A[j]时,就有可能产生回文串。如果\(j-i+1 =
2\) 也即字符串长度等于2时,那么肯定是回文串,或者如果\(dp[i+1][j-1]\) 是回文串的话那\(dp[i][j]\) 也为回文串。有了这个想法,可以编写代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution : def getLongestPalindrome (self , A: str ) -> int : max_length = 1 n = len (A) dp = [[False ]*n for i in range (n)] for i in range (n): dp[i][i] = True for i in range (n-1 , -1 , -1 ): for j in range (i+1 , n): if A[i] == A[j]: if j -i + 1 == 2 or dp[i+1 ][j-1 ]: dp[i][j] = True if dp[i][j] and j - i + 1 > max_length: max_length = j -i + 1 return max_length
BM 74 数字字符串转化成IP地址
url:牛客
BM74
考察知识点:回溯法
这道题完全不知道怎么用动态规划做,先写了一版用回溯法做的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Solution : def restoreIpAddresses (self , s: str ) -> List [str ]: result = [] def dfs (v, s ): if len (v) == 4 : if len (s) == 0 : result.append("." .join(v)) else : for i in range (3 ): if len (s) < i+1 : continue region = s[:i+1 ] next_s = s[i+1 : ] if len (region) > 1 and region[0 ] == "0" : continue elif int (region) > 255 : continue v.append(region) dfs(v, next_s) if len (v) > 0 : del v[-1 ] dfs([], s) return result
原来不是用动态规划解的,那没事了。补充一种3分割点的循环方法吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class Solution : def restoreIpAddresses (self , s: str ) -> List [str ]: result = [] for i in range (1 , 4 ): for j in range (i+1 , i+4 ): for k in range (j+1 , j+4 ): if k > len (s)-1 : continue regions = [s[:i], s[i: j], s[j: k], s[k:]] vaild = True for region in regions: if len (region) > 3 : vaild = False break elif len (region) > 1 and region[0 ] == "0" : vaild = False break elif int (region) > 256 : vaild = False break if vaild: result.append("." .join(regions)) return result
9月23日
BM 75 编辑距离(一)
url:牛客
BM75
考察知识点:动态规划
本题的关键就是明白三个编辑操作实际上起到什么作用,插入是往str1中加入一个字符,那么str2的指针后移一位,str1指针不动;删除则是删除str1的一个字符,那么str1指针后移一位,str2指针不动;替换是将str1位置指针元素替换为str2的对应元素,两个字符串指针都后移一位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution : def editDistance (self , str1: str , str2: str ) -> int : n = len (str1) m = len (str2) dp = [[0 ]*(m+1 ) for i in range (n+1 )] for i in range (n+1 ): for j in range (m+1 ): if i == 0 : dp[i][j] = j elif j == 0 : dp[i][j] = i else : if str1[i-1 ] == str2[j-1 ]: dp[i][j] = dp[i-1 ][j-1 ] else : dp[i][j] = min (dp[i-1 ][j] + 1 , dp[i][j-1 ] + 1 , dp[i-1 ][j-1 ] + 1 ) return dp[n][m]
9月25日
BM 76 正则表达式匹配
url:牛客
BM76
考察知识点:正则表达式原理、动态规划
在《算法
(第4版)》的字符串章节有详细介绍正则表达式的原理,系统性地学习了一下可以编写如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 from typing import Union , List class Digraph : """用邻接表储存有向图的数据结构""" def __init__ (self, V: int ): self.V = V self.E = 0 self.adj = [set () for i in range (V)] def add_edge (self, v: int , w: int ): """添加边""" self.adj[v].add(w) self.E += 1 def get_adj (self, v: int ): """获取边的邻接表""" return self.adj[v] class DirectedDFS : """判断有向图可达性的API""" def __init__ (self, G: Digraph, sources: Union [int , List [int ]] ): """sources可以是单个数字,代表单点。也可以是一个列表,代表多点情况下的可达性""" self.marked_list = [False for i in range (G.V)] if isinstance (sources, int ): sources = [sources] for s in sources: if not self.marked_list[s]: self.dfs(G, s) def marked (self, v: int ): """返回该点是否可达""" return self.marked_list[v] def dfs (self, G: Digraph, v: int ): """用深度优先搜索判断可达性""" self.marked_list[v] = True for w in G.adj[v]: if not self.marked_list[w]: self.dfs(G, w) class Solution : def match (self , str1: str , pattern: str ) -> bool : def recognizes (txt: str , G: Digraph, re: List [str ] ): """通过NFA判断文本txt来是否能识别""" pc = set () dfs = DirectedDFS(G, 0 ) M = len (re) for v in range (G.V): if dfs.marked(v): pc.add(v) for i in range (len (txt)): match_list = set () for v in pc: if v < M: if re[v] == txt[i] or re[v] == "." : match_list.add(v+1 ) pc = set () dfs = DirectedDFS(G, match_list) for v in range (G.V): if dfs.marked(v): pc.add(v) if M in pc: return True else : return False def NFA (regexp: str ): """根据给定表达式构造NFA(非确定有限自动机)""" ops = [] re = list (regexp) M = len (re) G = Digraph(M+1 ) for i in range (M): lp = i if re[i] == "(" or re[i] == "|" : ops.append(i) elif re[i] == ")" : op_i = ops.pop() if re[op_i] == "|" : lp = ops.pop() G.add_edge(lp, op_i+1 ) G.add_edge(op_i, i) else : lp = op_i if i < M-1 and re[i+1 ] == "*" : G.add_edge(lp, i+1 ) G.add_edge(i+1 , lp) if re[i] == "(" or re[i] == "*" or re[i] == ")" : G.add_edge(i, i+1 ) return G, re def match0 (regexp: str , txt: str ): """返回字符串txt是否满足表达式regexp的结果""" G, re = NFA("(" + regexp + ")" ) return recognizes(txt, G, re) return match0(pattern, str1)
具体而言,主要使用了非确定有限状态机(NFA)和图的多点可达性性质,如果到达接受状态就是匹配上了,反之没匹配上。
这个是相对完整的正则表达式实现,如果只有通配符"."和闭包"*"的话应该能简单不少。我们看看动态规划版本的实现。
动态规划版本的实现比较巧妙,充分利用了"."和"*"的特性。
如果str1和pattern最末尾都是字符而且不匹配的话,那它们一定不匹配。如果是字符或通配符而且匹配的话,那就看str1[:-1]和pattern[:-1]是否匹配即可。
如果pattern最末尾是"*"号,那么有两种情况,一种就是pattern上一个字符和现在str1最末尾的字符匹配,那么它可以重复至少一次或者不重复,即为其上方和左二位置取或操作。另一种就是pattern上一个字符和现在str1最末尾的字符不匹配,那么它不能重复,就只能取左二位置的值。
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Solution : def match (self , str1: str , pattern: str ) -> bool : n = len (str1) m = len (pattern) dp = [[False ]*(m+1 ) for i in range (n+1 )] dp[0 ][0 ] = True for j in range (1 , m+1 ): if pattern[j-1 ] == "*" : dp[0 ][j] = dp[0 ][j-2 ] for i in range (1 , n+1 ): for j in range (1 , m+1 ): if pattern[j-1 ] == "*" : if pattern[j-2 ] == "." or pattern[j-2 ] == str1[i-1 ]: dp[i][j] = dp[i-1 ][j] or dp[i][j-2 ] else : dp[i][j] = dp[i][j-2 ] elif pattern[j-1 ] == str1[i-1 ] or pattern[j-1 ] == "." : dp[i][j] = dp[i-1 ][j-1 ] else : dp[i][j] = False return dp[n][m]
BM 77 最长的括号子串
url:牛客
BM77
考察知识点:动态规划
这题还挺难的,我写了一版好难到\(O(n)\) 复杂度,总是要\(O(n^2)\) 复杂度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Solution : def longestValidParentheses (self , s: str ) -> int : n = len (s) dp = [0 for i in range (n+1 )] def get_max (end: int ): stack = [] j = end max_ = 0 while j >= 0 : if s[j] == ")" : stack.append(")" ) else : if len (stack) == 0 : break else : stack.pop() if len (stack) == 0 : max_ = end - j + 1 + dp[j] break j -= 1 return max_ print (n) for i in range (1 , n+1 ): if s[i-1 ] == ")" : dp[i] = get_max(i-1 ) return max (dp)
大致的想法就是倒序检查,如果遇到能闭合的那就说明这段可以不用考虑了,以该元素结尾的最长的子串就是这段的长度加上它后面最长的子串。
这个方法是\(O(n^2)\) 复杂度的,并不能通过所有的测试用例。真正的解法在这上面要做一些改动。就是前面的虽然没有闭合,但是已经知道以某位置元素结尾的最长子串长度了,那直接跳过就能大大优化速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Solution : def longestValidParentheses (self , s: str ) -> int : n = len (s) dp = [0 for i in range (n+1 )] def get_max (end: int ): stack = [] j = end if j < 1 : return 0 elif s[j] == "(" : return 0 elif s[j] == ")" and s[j-1 ] == "(" : return dp[j-1 ] + 2 elif s[j-1 ] == ")" and j - dp[j] - 1 >= 0 and s[j-dp[j]-1 ] == "(" : return dp[j-dp[j]-1 ] + dp[j] + 2 else : return 0 for i in range (1 , n+1 ): dp[i] = get_max(i-1 ) return max (dp)
上面这样可以通过所有的测试用例了,然后每次也不涉及到变量j的迭代,因此复杂度被控制在\(O(n)\) 了。但是显然如果考虑到以j结尾的最长子串后,实际上不需要这个栈来检验了。跳过已经匹配了的子串后所有的匹配都是朴实的"()"匹配。
因此,我们可以重新写一下这个动态规划版本,不需要这些栈来辅助检查了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution : def longestValidParentheses (self , s: str ) -> int : n = len (s) dp = [0 for i in range (n+1 )] for i in range (2 , n+1 ): if s[i-1 ] == ")" : if s[i-2 ] == "(" : dp[i] = dp[i-2 ] + 2 else : if i - dp[i-1 ] - 2 >= 0 and s[i-dp[i-1 ]-2 ] == "(" : dp[i] = dp[i-1 ] + dp[i-dp[i-1 ]-2 ] + 2 return max (dp)
有点烦的就是注意为了避免\(i==2\) 时的边界检查,我们将dp数组特地申请长了一位。然后这样会导致字符串是从\(i-1\) 位开始的,因此在"如果不匹配得看跳过已经匹配的子串后是否匹配"那里实际上是在算\(i-1 - dp[i-1] -1\) 的下标。因为\(i-1\) 是当前位置,\(dp[i-1]\) 是当前位置前一个字符组成的最长子串长度,也即要跳过的字符长度,然后跳过之后再往前一位才是与当前位置匹配的括号位置,因此是\(i-dp[i-1]-2\) 。
还可以利用左括号匹配的坐标来做文章,以前都是直接把左括号弹进去的,啥额外信息也没有,太亏了。如果知道左括号的坐标的话,那很容易想到对于匹配的右括号可以根据坐标计算出符合规则的字串长度。这里有个不容易想到的就是如果左括号不够导致匹配失败了该怎么办,左括号不够匹配失败了那之后也一定会匹配失败,正确的方式是在发现了之后丢弃前面的子串,即把开始值重置为当前位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution : def longestValidParentheses (self , s: str ) -> int : res = 0 start = -1 stack = [] n = len (s) for i in range (n): if s[i] == "(" : stack.append(i) else : if len (stack) == 0 : start = i else : stack.pop() if len (stack) == 0 : res = max (res, i - start) else : res = max (res, i-stack[-1 ]) return res
BM 78 打家劫舍(一)
url:牛客
BM78
考察知识点:动态规划
我们可以从是否要打劫第i家开始考虑,如果要打劫第i家,必须放弃第i-1家的打劫,只能获取从开始到第i-2家的收益。反之,如果不打劫第i家,那么可以获取从开始到第i-1家的收益。那么走到第i家的最大收益就是前面两种情况的最大值,也即:
\[
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
\]
那为了避免处理只有一家或者只有两家之类的边界条件,我们可以人为在nums最开头插入两户虚拟人家,价值都是0,然后直接从第三户开始考虑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def rob (self , nums: List [int ] ) -> int : nums.insert(0 , 0 ) nums.insert(0 , 0 ) n = len (nums) dp = [0 for i in range (n)] for i in range (2 , n): dp[i] = max (dp[i-1 ], dp[i-2 ]+nums[i]) return dp[-1 ]
这样应该是本问题的最优解了。但是如果不插入虚拟节点呢,也即如果没有这两户虚拟人家dp数组该怎么初始化呢?这里有个隐藏的bug,就是如果认为在第2家的时候不打劫第2家,那就亏了!因此会把dp的前两个数组初始化为:
\[
dp[0]=nums[0], dp[1]=nums[1]
\] 但这实际上埋下祸根,我们考虑一下这样的nums: \([5, 1, 3,
7]\) 。按照之前的初始化dp数组为:\([5,
1, 8,
8]\) ,所以最后最大结果是8。但打劫第1家和第4家的话结果是12,与结果不同。问题出在哪了呢?走到第2家时非要打劫第2家吗,我们也可以打劫第1家,只要第1家比第2家值钱,那么就应该打劫第1家而放弃第2家。弄清楚这个问题之后可以知道初始化应该是:
\[
dp[0]=nums[0],dp[1]=max(nums[0], nums[1])
\] 实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution : def rob (self , nums: List [int ] ) -> int : n = len (nums) dp = [0 for i in range (n)] for i in range (n): if i == 0 : dp[0 ] = nums[0 ] elif i == 1 : dp[1 ] = max (nums[0 ], nums[1 ]) else : dp[i] = max (dp[i-1 ], dp[i-2 ]+nums[i]) return dp[-1 ]
BM 79 打家劫舍(二)
url:牛客
BM79
考察知识点:动态规划
相比一把场景换成圆形的了。圆形的同时偷第一个和偷最后一个就会冲突。如果分成两个子问题考虑就可以剪断这个环,一是我从第一家走到第n-1家,最后一家我不看了;二是我从第2家走到最后一家,第一家我不看了。然后取这两个子问题的最大值就是整个问题的最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution : def rob (self , nums: List [int ] ) -> int : n = len (nums) dp1 = [0 ] * n dp2 = [0 ] * n for i in range (n): if i == 0 : dp2[i] = nums[0 ] elif i == 1 : dp2[i] = max (dp2[0 ], nums[i]) dp1[i] = nums[i] elif i == n-1 : dp2[i] = dp2[i-1 ] dp1[i] = max (dp1[i-1 ], dp1[i-2 ]+nums[i]) else : dp2[i] = max (dp2[i-1 ], dp2[i-2 ]+nums[i]) dp1[i] = max (dp1[i-1 ], dp1[i-2 ]+nums[i]) return max (dp1[-1 ], dp2[-1 ])
9月26日
BM 80 买卖股票的最好时机(一)
url:牛客
BM80
考察知识点:动态规划、贪心算法
第一眼看上去似乎可以先求前后一天利润的diff数组,买进卖出一次也可以看作是买进那天买入,然后每天执行买入卖出操作直到真正卖出那天。这样问题就转化为了求这个diff数组的最大和子数组了,这个问题我们在BM
72 连续子数组的最大和 中解决过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) diff = [0 ] for i in range (1 , n): diff.append(prices[i] - prices[i-1 ]) dp = [0 for i in range (n)] for i in range (1 , n): dp[i] = max (dp[i-1 ]+diff[i], diff[i]) return max (dp)
也可以换一种思路,我们要求的就是最大卖出价和最小买入价的差值,限制就是这个卖出操作不能先于买入操作发生。那我们可以使用两个数组buy和sell,其中buy数组记录前i天的最低买入价,sell数组记录从第i天到最后一天的最高卖出价。然后两个数组做差取最大值即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) buy = [0 ]*n sell = [0 ]*n for i in range (n): if i == 0 : buy[0 ] = prices[0 ] else : buy[i] = min (prices[i], buy[i-1 ]) for j in range (n-1 , -1 , -1 ): if j == n - 1 : sell[j] = prices[j] else : sell[j] = max (prices[j], sell[j+1 ]) _max = 0 for i in range (n): if sell[i] - buy[i] > _max : _max = sell[i] - buy[i] return _max
但是怎么用O(1)的空间复杂度完成这件事呢,根本想不到。
看了题解之后发现一种更清晰的dp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) dp = [[0 ]*2 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 ], -prices[i]) return dp[n-1 ][0 ]
然后也知道了用O(1)复杂度的方法,就是实际在历史最高卖出价是不需要维护的,因为我们可以记录下之前的最低买入价,然后尝试每天卖出,碰到更大的收益就记下来,最后返回最大收益。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) _max = 0 best_buy_price = float ('inf' ) for i in range (n): if best_buy_price > prices[i]: best_buy_price = prices[i] if prices[i] - best_buy_price > _max : _max = prices[i] - best_buy_price return _max
BM 81 买卖股票的最好时机(二)
url:牛客
BM81
考察知识点:动态规划、贪心算法
接着BM80的想法,唯一的区别就是可以卖了再买,所以买入的时候考虑一下之前的卖出后剩下的余额即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) dp = [[0 ]*2 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-1 ][0 ] - prices[i]) return dp[n-1 ][0 ]
之前diff数组的思想也可以用上,如果允许多次买入卖出的话那也不需要连续子数组了,直接子序列的最大和即可。那啥时候子序列和最大,把数组里面的正数全算上就最大了啊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) diff = [0 ]*n for i in range (1 , n): diff[i] = prices[i] - prices[i-1 ] _max = 0 for i in range (1 , n): if diff[i] > 0 : _max += diff[i] return _max
这时候空间复杂度O(1)的解法也呼之欲出了,如果每次我们都直接算diff[i]然后直接判断要不要进行买卖操作即可O(1)空间复杂度完成问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) _max = 0 for i in range (1 , n): diff = prices[i] - prices[i-1 ] if diff > 0 : _max += diff return _max
BM 82买卖股票的最好时机(三)
url:牛客
BM82
考察知识点:动态规划
在(二)的基础上又加了个次数约束,我们也在(二)的dp基础上加了几个新的框用于考虑不同情况,感觉写得不优雅,但是能解决问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) dp = [[0 ]*5 for i in range (n)] dp[0 ][1 ] = -prices[0 ] dp[0 ][2 ], dp[0 ][3 ], dp[0 ][4 ] = -float ("inf" ), -float ("inf" ), -float ("inf" ) for i in range (1 , n): dp[i][1 ] = max (dp[i-1 ][1 ], -prices[i]) dp[i][2 ] = max (dp[i-1 ][2 ], dp[i-1 ][1 ] + prices[i]) dp[i][3 ] = max (dp[i-1 ][3 ], dp[i-1 ][2 ] - prices[i]) dp[i][4 ] = max (dp[i-1 ][4 ], dp[i-1 ][3 ] + prices[i]) return max (dp[-1 ][0 ], dp[-1 ][2 ], dp[-1 ][4 ])
看了题解,本来以为有啥贪心的方法能达到\(O(1)\) 复杂度,结果并不是靠贪心算法来搞的,而是就在原动态规划的基础上就能做得到。因为最后的结果只看最后一行的几个位置,而且迭代的时候也只需要上一行的数据就能迭代,因此可以只用两行dp数组实现。这个想法确实很有启发意义,如果对于任意下一行只依赖上一行而且结果在最后一行的动态规划算法,都可以通过两行dp数组代替那个大的dp矩阵(这样的叫法也许比较合适)来达到缩减空间复杂度的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution : def maxProfit (self , prices: List [int ] ) -> int : n = len (prices) dp = [0 , -prices[0 ], -float ("inf" ), -float ("inf" ), -float ("inf" )] for i in range (1 , n): dp_i = [0 ]*5 dp_i[1 ] = max (dp[1 ], -prices[i]) dp_i[2 ] = max (dp[2 ], dp[1 ] + prices[i]) dp_i[3 ] = max (dp[3 ], dp[2 ] - prices[i]) dp_i[4 ] = max (dp[4 ], dp[3 ] + prices[i]) dp = dp_i return max (dp[0 ], dp[2 ], dp[4 ])
BM 83 字符串变形
url:牛客
BM83
考察知识点:字符串
感觉用Python的API可以很容易实现,但是如果不调Python的API真的会很难受!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def trans (self , s: str , n: int ) -> str : s_list = s.split(" " ) s_list.reverse() s = list (" " .join(s_list)) for i in range (n): if ord (s[i]) >= ord ('a' ) and ord (s[i]) <= ord ('z' ): s[i] = chr (ord (s[i]) - 32 ) elif ord (s[i]) >= ord ('A' ) and ord (s[i]) <= ord ('Z' ): s[i] = chr (ord (s[i]) + 32 ) return "" .join(s)
不用空格分割手动翻转了一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Solution : def trans (self , s: str , n: int ) -> str : res = [] for i in range (n): if s[i] >= 'a' and s[i] <= 'z' : res.append(chr (ord (s[i]) - 32 )) elif s[i] >= 'A' and s[i] <= 'Z' : res.append(chr (ord (s[i]) + 32 )) else : res.append(s[i]) res.reverse() i, j = 0 , 0 while i < n: j = i while j < n and res[j] != " " : j += 1 temp_i, temp_j = i, j-1 while temp_i < temp_j: res[temp_i], res[temp_j] = res[temp_j], res[temp_i] temp_i += 1 temp_j -= 1 i = j + 1 return "" .join(res)
BM 84 最长公共前缀
url:BM
84
考察知识点:字符串操作
好像没啥的,在\(O(n*len)\) 复杂度下直接遍历就好了。甚至我觉得找最短字符串都有点浪费时间,不对的它自然就失败了。但是如果不先查找最短字符串的话每次还得判断下标是否越界,感觉多次调用len方法早就把找最短字符串偷的懒还回去了,因此感觉还是先找最短字符串比较好,省去内层循环判断越界。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Solution : def longestCommonPrefix (self , strs: List [str ] ) -> str : n = len (strs) if n == 0 : return "" min_i = 0 min_length = float ("inf" ) for i, s in enumerate (strs): if len (s) < min_length: min_length = len (s) min_i = i target = strs[min_i] j = 0 while j < min_length: c = target[j] valid = True for s in strs: if s[j] != c: valid = False break if valid: j += 1 else : break return target[:j]
BM 85 验证IP地址
url:牛客
BM85
考察知识点:字符串操作
没啥好说的,按照题目要求验证即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution : def solve (self , IP: str ) -> str : if "." in IP: regions = IP.split("." ) if len (regions) != 4 : return "Neither" for region in regions: if len (region) > 1 and region[0 ] == "0" : return "Neither" elif not region.isdigit() or int (region) > 255 : return "Neither" return "IPv4" elif ":" in IP: regions = IP.split(":" ) if len (regions) != 8 : return "Neither" for region in regions: if len (region) == 0 : return "Neither" elif len (region) >= 2 and region[:2 ] == "00" : return "Neither" for j in region: if not (j.isdigit() or j in "abcdefABCDEF" ): return "Neither" return "IPv6" else : return "Neither"
BM 86 大数加法
url:BM
86
考察知识点:字符串操作、模拟
一开始没看到模拟,就直接转成数字然后相加了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def solve (self , s: str , t: str ) -> str : if len (s) == 0 : num_s = 0 else : num_s = int (s) if len (t) == 0 : num_t = 0 else : num_t = int (t) return num_s + num_t
模拟从低位到高位的加减法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 class Solution : def solve (self , s: str , t: str ) -> str : if len (s) < len (t): s, t = t, s n = len (s) - 1 m = len (t) - 1 mt9 = False res = "" for i in range (len (t)): s_i = ord (s[n - i]) - ord ('0' ) t_i = ord (t[m - i]) - ord ('0' ) res_i = s_i + t_i if mt9: res_i += 1 mt9 = False if res_i >= 10 : mt9 = True res_i -= 10 res = f"{res_i} {res} " for j in range (len (t), len (s)): res_j = ord (s[n - j]) - ord ('0' ) if mt9: res_j += 1 mt9 = False if res_j >= 10 : mt9 = True res_j -= 10 res = f"{res_j} {res} " if mt9: return f"1{res} " else : return res
BM 87 合并两个有序的数组
url:牛客
BM87
考察知识点:数组
比较新颖的地方就是要求就地插入,即后方元素必须右移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution : def merge (self , A, m, B, n ): def move_element (start, end ): temp = end while temp > start: A[temp] = A[temp-1 ] temp -= 1 i = 0 for j in range (n): while i < m + j: if A[i] >= B[j]: move_element(i, m+j) A[i] = B[j] break i += 1 A[i] = B[j]
这里有个天才的想法,就是倒过来看!如果倒过来看A的加长部分,我们把两数组的最大元素依次往后挪,就可以轻松做到不挪动已有元素然后将数组he'bing排序到位!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def merge (self , A, m, B, n ): i, j = m-1 , n-1 for k in range (m+n-1 , -1 , -1 ): if i >= 0 and j >= 0 and A[i] > B[j]: A[k] = A[i] i -= 1 elif j >= 0 : A[k] = B[j] j -= 1 else : break
9月28日
BM 88 判断是否为回文字符串
url:牛客
BM88
考察知识点:双指针
如果是链表的话需要从前往后遍历和从后往前遍历进行比较,如果是字符串这种可以用下标迭代的就用双指针即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def judge (self , str1: str ) -> bool : i, j = 0 , len (str1)-1 while i < j: if str1[i] != str1[j]: return False i += 1 j -= 1 return True
BM 89 合并区间
url:牛客
BM89
考察知识点:双指针
一种方式时间复杂度是\(O(n\log
n)\) ,空间复杂度是\(O(n)\) ,\(n\) 为区间的数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Solution : def merge (self , intervals: List [Interval] ) -> List [Interval]: intervals.sort(key=lambda x: x.start) n = len (intervals) if n == 0 : return [] j = 0 merge_intervals = [intervals[0 ]] for i in range (1 , n): if intervals[i].start <= merge_intervals[j].end: merge_intervals[j].end = max (merge_intervals[j].end, intervals[i].end) else : merge_intervals.append(intervals[i]) j += 1 return merge_intervals
10月2日
BM 90 最小覆盖子串
url:牛客
BM90
考察知识点:双指针、字符串、哈希
不知道有啥窍门,先写了一个比较暴力的。即检索以某处字符开头的最小覆盖子串。如果第一个字符不是目标字符串中的字符,那它肯定不是最小覆盖子串,因为去掉它也是可以的,这里应该是有最优子结构在的。然后就是逐个匹配,用哈希表一个个把字符消除掉,一直消除到字典是空的就说明字符攒够了,也即找到了以该位置开头的最小覆盖子串。然后需要找n次,所以最后复杂度应该是每次查找要\(O(n)\) 然后要找n次,复杂度为\(O(n^2)\) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Solution : def minWindow (self , S: str , T: str ) -> str : n = len (S) T_dict = {} for t in T: T_dict[t] = T_dict.get(t, 0 ) + 1 min_length = 10000 target_str = "" for i in range (n): if S[i] not in T_dict: continue char_set = T_dict.copy() for j in range (i, n): if S[j] in char_set and char_set[S[j]] > 1 : char_set[S[j]] -= 1 elif S[j] in char_set and char_set[S[j]] == 1 : char_set.pop(S[j]) if len (char_set) == 0 : if j - i + 1 < min_length: target_str = S[i: j+1 ] min_length = j - i + 1 break return target_str if min_length < 10000 else ""
10月6日
BM 90 最小覆盖子串
url:牛客
BM90
考察知识点:双指针、字符串、哈希
不知道有啥窍门,先写了一个比较暴力的。即检索以某处字符开头的最小覆盖子串。如果第一个字符不是目标字符串中的字符,那它肯定不是最小覆盖子串,因为去掉它也是可以的,这里应该是有最优子结构在的。然后就是逐个匹配,用哈希表一个个把字符消除掉,一直消除到字典是空的就说明字符攒够了,也即找到了以该位置开头的最小覆盖子串。然后需要找n次,所以最后复杂度应该是每次查找要\(O(n)\) 然后要找n次,复杂度为\(O(n^2)\) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Solution : def minWindow (self , S: str , T: str ) -> str : n = len (S) T_dict = {} for t in T: T_dict[t] = T_dict.get(t, 0 ) + 1 min_length = 10000 target_str = "" for i in range (n): if S[i] not in T_dict: continue char_set = T_dict.copy() for j in range (i, n): if S[j] in char_set and char_set[S[j]] > 1 : char_set[S[j]] -= 1 elif S[j] in char_set and char_set[S[j]] == 1 : char_set.pop(S[j]) if len (char_set) == 0 : if j - i + 1 < min_length: target_str = S[i: j+1 ] min_length = j - i + 1 break return target_str if min_length < 10000 else ""
接着10月2日之后来做吧,看了一下题解,发现这个维护双指针的方法真的非常巧妙。大体的做法就是右指针先移到满足条件的位置,然后左指针收缩到不能再收缩,这时候就获得了以右指针元素结尾的最短子串。这里面它多用了一个先验,就是如果左、右指针的位置已经能构成最小覆盖子串了,那右指针后面的最小覆盖子串都可以从左指针的位置开始收缩,因为左指针前面的元素都可以去掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class Solution : def minWindow (self , S: str , T: str ) -> str : char_dict = {} for char in T: char_dict[char] = char_dict.get(char, 0 ) - 1 left, right = 0 , 0 n = len (S) min_length = 10001 target = "" while right < n: if S[right] in char_dict: char_dict[S[right]] += 1 complete = True for char in char_dict: if char_dict[char] < 0 : complete = False if complete: while S[left] not in char_dict or char_dict[S[left]] >= 1 : if S[left] in char_dict: char_dict[S[left]] -= 1 left += 1 if right - left + 1 < min_length: min_length = right - left + 1 target = S[left: right + 1 ] right += 1 return target
BM 91 反转字符串
url:牛客
BM91
考察知识点:字符串
还是比较基础的,甚至直接调用reverse就能反转过来,如果不讲究就像这样开个数组拼一下行了。但是对Python来说字符串是不可变的,因此不能像Java那样交换字符串的位置。要新开一个列表的话那就按逆序加入元素即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def solve (self , str1: str ) -> str : str_list = [] for char in str1: str_list.insert(0 , char) return "" .join(str_list)
BM 92 最长无重复子数组
url:牛客
BM92
考察知识点:哈希、双指针、数组
其实和BM90很类似,都是对所谓“子数组”的限制。这类问题一般可以抽象成一个滑动窗口,滑动窗口里维护一个字典或者集合,每次指针移动时就在字典或集合里更新现在字符串的状态。实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution : def maxLength (self , arr: List [int ] ) -> int : T = set () left, right = 0 , 0 n = len (arr) max_length = 0 while right < n: if arr[right] in T: if right - left > max_length: max_length = right - left while arr[left] != arr[right]: T.remove(arr[left]) left += 1 if left != right: left += 1 else : T.add(arr[right]) right += 1 if right - left > max_length: max_length = right - left return max_length
大体做法就是right指针一直移动,直到发现right位置当前元素不能加入子数组为止。这时候以left开头的最长无重复子数组已经找出来了,就是从left到right-1里的所有元素构成的子数组。然后,我们需要收缩left指针,一直收缩到和right位置元素相同的元素位置为止,因为只有去掉这个元素,right位置的元素才能加入子数组。当然也有一个特殊情况,就是left已经缩到和right一样了,那left也不用继续缩了,因为此时就一个元素了。
BM 93 盛水最多的容器
url:牛客
BM93
考察知识点:双指针、数组、贪心算法
这题感觉比较难,一般的思路肯定就是先确定左边的木板,然后再确定右边的木板,遍历的话要\(O(n^2)\) 的复杂度。写了之后发现过不了,然后又加了一些优化,结果还是超时。实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution : def maxArea (self , height: List [int ] ) -> int : n = len (height) max_area = 0 for i, h1 in enumerate (height): if h1 * (n - i) < max_area: continue for j in range (n-1 , i, -1 ): h = min (h1, height[j]) l = j - i if h * l > max_area: max_area = h * l if height[j] >= h1 or h1 * (j - i) < max_area: break return max_area
看了题解之后才知道,这题实际上隐含了一个最优子结构。假设我们已经找到了一个最大容积的容器,那在左边木板的左边部分不可能有不低于左木板的元素,同样右边部分不可能有低于右木板的元素。也就意味着如果我们从两端开始,然后尽可能地保留长边替换短边,是可以得到最优解的。因此,从左右两边开始,每次都把短的那边换成下一个边,往复直到左右指针相遇就可以获得最大容积的容器了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution : def maxArea (self , height: List [int ] ) -> int : n = len (height) max_area = 0 left, right = 0 , len (height) - 1 while left < right: h = min (height[left], height[right]) l = right - left if h * l > max_area: max_area = h * l if height[left] < height[right]: left += 1 else : right -= 1 return max_area
这个确实很巧妙,解法出来之后我还感觉会不会不能保证最优,想到容器左右两边都不可能有比它们高的元素了才觉得确实是合理的。
10月9日
BM 94 接雨水问题
url:牛客
BM94
考察知识点:双指针、动态规划
这题的常规思路就是双指针,和牛客
BM93 类似,只不过现在底不是平的了,而且是要求所有桶能盛下的水。
可以这样思考,最开始左右两边肯定盛不了水,但它们可以作为桶的边。每次移动高度低的那边指针,并且观察新的位置是否超出桶的高度了,如果超出了则说明进入了一个新的桶,更新桶的高度。否则,就还在那个桶里面,更新雨水的量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution : def maxWater (self , arr: List [int ] ) -> int : n = len (arr) left, right = 0 , n-1 water = 0 h = min (arr[left], arr[right]) while left < right: if arr[left] < arr[right]: left += 1 if arr[left] < h: water += h - arr[left] else : h = min (arr[left], arr[right]) else : right -= 1 if arr[right] < h: water += h - arr[right] else : h = min (arr[left], arr[right]) return water
另外一种办法就是先求出最高的柱子,然后以最高柱子为支点将原问题切分为左、右两个子问题。这样的好处就是简化思考,左边的子问题永远不需要动右边的柱子,只需要遇到比左边柱子高的值后将柱子更新过去即可,右边也是同理,不过要倒序遍历一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Solution : def maxWater (self , arr: List [int ] ) -> int : n = len (arr) water = 0 top = 0 for i in range (n): if arr[i] > arr[top]: top = i left = 0 for i in range (top): if arr[i] >= arr[left]: left = i else : water += arr[left] - arr[i] right = n-1 for i in range (n-2 , top, -1 ): if arr[i] >= arr[right]: right = i else : water += arr[right] - arr[i] return water
动态规划的思路也很巧妙,就是如果一个位置要盛水,那肯定是在它左边和右边都有比它更高的柱子,它能装下的水的高度是左边和右边最高柱子的较小值。那么如果我们用一个dp数组维护该位置左边和该位置右边的最高柱子高度,那计算时只需要求二者的最小值然后判断与当前高度的相对大小关系即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution : def maxWater (self , arr: List [int ] ) -> int : n = len (arr) dp = [[0 , 0 ] for i in range (n)] for i in range (1 , n): dp[i][0 ] = max (dp[i-1 ][0 ], arr[i-1 ]) for j in range (n-2 , -1 , -1 ): dp[j][1 ] = max (dp[j+1 ][1 ], arr[j+1 ]) water = 0 for i in range (n): h = min (dp[i][0 ], dp[i][1 ]) if h > arr[i]: water += h - arr[i] return water
BM 95 分糖果问题
url:牛客
BM95
考察知识点:贪心算法
先从右边倒数第二个人开始与右边的人比,如果分数多于右边而且糖果不多于右边就更新糖果为右边的糖果数+1。左边也类似,从左边第二个人开始与左边的人比,如果分数多于左边而且糖果不多于左边就更新糖果为左边的糖果数+1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution : def candy (self , arr: List [int ] ) -> int : n = len (arr) c = [1 ] * n for i in range (n-2 , -1 , -1 ): if arr[i] > arr[i+1 ] and c[i] <= c[i+1 ]: c[i] = c[i+1 ] + 1 for j in range (1 , n): if arr[j] > arr[j-1 ] and c[j] <= c[j-1 ]: c[j] = c[j-1 ] + 1 return sum (c)
BM 96 主持人调度(二)
url:牛客
BM95
考察知识点:贪心算法、堆
大体做法就是先按开始时间将活动排序,之后安排主持人。安排过的主持人结束时间记录在一个小根堆中,然后每次要安排新主持人前查看是否有已经结束的主持人将已经结束的主持人重新加回来,之后再分配主持人。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import heapqclass Solution : def minmumNumberOfHost (self , n: int , startEnd: List [List [int ]] ) -> int : startEnd.sort(key=lambda x: x[0 ]) cur_num = 0 all_num = 0 end_time = [] for i in range (n): cur = startEnd[i][0 ] while len (end_time) > 0 and end_time[0 ] <= cur: cur_num += 1 heapq.heappop(end_time) if cur_num == 0 : all_num += 1 else : cur_num -= 1 heapq.heappush(end_time, startEnd[i][1 ]) return all_num
BM 97 旋转数组
url:牛客
BM97
考察知识点:数组
如果没做过这道题,真的很难想明白旋转数组该怎么转!如果一直挪那个旧数组的话会发现怎么挪也挪不对。
看了题解之后发现3次翻转旧等同于进行这样的旋转了。第一步会把整个数组反转,之后把前m个元素反转,再之后把后n-m个元素反转即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution : def solve (self , n: int , m: int , a: List [int ] ) -> List [int ]: m = m % n for i in range (n // 2 ): a[i], a[n-i-1 ] = a[n-i-1 ], a[i] for i in range (m // 2 ): a[i], a[m-i-1 ] = a[m-i-1 ], a[i] for i in range (m, (n+m)//2 ): a[i], a[n+m-i-1 ] = a[n+m-i-1 ], a[i] return a
BM 98 螺旋矩阵
url:牛客
BM98
考察知识点:数组
用一个矩阵记录是否访问过该元素,然后按向右走、向下走、向左走和向上走的流程来模拟螺旋数组的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class Solution : def spiralOrder (self , matrix: List [List [int ]] ) -> List [int ]: m = len (matrix) if m == 0 : return [] n = len (matrix[0 ]) visited = [[False ]*n for i in range (m)] result = [] i, j = 0 , 0 while i < m and j < n and not visited[i][j]: while j < n and not visited[i][j]: result.append(matrix[i][j]) visited[i][j] = True j += 1 j -= 1 i += 1 while i < m and not visited[i][j]: result.append(matrix[i][j]) visited[i][j] = True i += 1 i -= 1 j -= 1 while j >= 0 and not visited[i][j]: result.append(matrix[i][j]) visited[i][j] = True j -= 1 j += 1 i -= 1 while i >= 0 and not visited[i][j]: result.append(matrix[i][j]) visited[i][j] = True i -= 1 i += 1 j += 1 return result
每次都只有上下左右的边界会收缩,用visited这样的二维数组来记录是否访问确实有点浪费,实际上我们只需要up,
down, right,
left四个边界值记录边界即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 class Solution : def spiralOrder (self , matrix: List [List [int ]] ) -> List [int ]: m = len (matrix) if m == 0 : return [] n = len (matrix[0 ]) up, down, left, right = 0 , m-1 , 0 , n-1 result = [] i, j = 0 , 0 while i <= down and j <= right: while j <= right: result.append(matrix[i][j]) j += 1 up += 1 j -= 1 if up > down: break i += 1 while i <= down: result.append(matrix[i][j]) i += 1 right -= 1 i -= 1 if left > right: break j -= 1 while j >= left: result.append(matrix[i][j]) j -= 1 down -= 1 j += 1 if up > down: break i -= 1 while i >= up: result.append(matrix[i][j]) i -= 1 left += 1 i += 1 if left > right: break j += 1 return result
BM 99 顺时针旋转矩阵
url:牛客
BM99
考察知识点:数组
一个想法就是转置,之后行翻转就行了。转置时需要新申请一个和原矩阵一样的空间,所以空间复杂度达到\(O(n^2)\) ,要操作每一个元素,时间复杂度为\(O(n^2)\) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def rotateMatrix (self , mat: List [List [int ]], n: int ) -> List [List [int ]]: rotate_mat = [] for j in range (n): rotate_row = [mat[i][j] for i in range (n)] rotate_row.reverse() rotate_mat.append(rotate_row) return rotate_mat
另外一种思路就是先进行行翻转,然后我们会发现目标矩阵和现在矩阵的差距就是需要进行反对角线元素的交换,那我们按反对角线进行元素交换即可。这种方法依然需要\(O(n^2)\) 级别的时间复杂度,但只需要\(O(1)\) 的空间复杂度,因为不涉及新空间的申请。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def rotateMatrix (self , mat: List [List [int ]], n: int ) -> List [List [int ]]: for mat_row in mat: mat_row.reverse() for i in range (n): for j in range (n-i-2 , -1 , -1 ): mat[i][j], mat[n-j-1 ][n-i-1 ] = mat[n-j-1 ][n-i-1 ], mat[i][j] return mat
看了题解之后发现自己多此一举了,转置不需要新申请数组也可以做呀,就是沿对角线交换,沿对角线交换的下标表示要简单很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution : def rotateMatrix (self , mat: List [List [int ]], n: int ) -> List [List [int ]]: for i in range (n): for j in range (i+1 , n): mat[i][j], mat[j][i] = mat[j][i], mat[i][j] for mat_row in mat: mat_row.reverse() return mat
10月10日
BM 100 设计LRU缓存
url:牛客
BM100
考察知识点:链表、哈希、模拟
这题的关键在于怎么标识最近最少使用的元素,而且需要以\(O(1)\) 的复杂度来更新这个最近最少使用的状态结构。数组肯定是不行的,因为数组删除中间一个元素之后不可避免地会引起后面元素位置的移动。纯链表结构也不行,因为这样更新已有key的状态的时候还需要在链表里找那个元素,这样都会使最坏时间复杂度来到\(O(n)\) 级别。
一种可行的办法是使用链表和字典配合来维护最近最少使用的元素,越靠近头节点的元素最近使用得越少,越靠近尾节点的元素最近使用得越频繁。在使用已有key时可用通过哈希表快速获得已有链表节点的引用,并将该节点插入到尾节点前面。
还有一个需要注意的点是LRU结构中,get和set方法都会更新节点使用情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 class ListNode : def __init__ (self, data ) -> None : self.next = None self.prev = None self.key = data class Solution : def __init__ (self, capacity: int ): self.capacity = capacity self.key_value_dict = {} self.list_head = ListNode(None ) self.list_tail = ListNode(None ) self.list_head.next = self.list_tail self.list_tail.prev = self.list_head self.key_pos = {} self.num = 0 def get (self, key: int ) -> int : if key in self.key_pos: update_node = self.key_pos[key] update_node.next .prev = update_node.prev update_node.prev.next = update_node.next update_node.next = self.list_tail update_node.prev = self.list_tail.prev self.list_tail.prev.next = update_node self.list_tail.prev = update_node return self.key_value_dict.get(key, -1 ) def set (self, key: int , value: int ) -> None : if self.num == self.capacity and key not in self.key_value_dict: node = self.list_head.next node.prev.next = node.next node.next .prev = node.prev self.key_pos.pop(node.key) self.key_value_dict.pop(node.key) self.num -= 1 if key not in self.key_value_dict: self.num += 1 insert_node = ListNode(key) insert_node.prev = self.list_tail.prev insert_node.next = self.list_tail self.list_tail.prev.next = insert_node self.list_tail.prev = insert_node self.key_pos[key] = insert_node else : update_node = self.key_pos[key] update_node.prev.next = update_node.next update_node.next .prev = update_node.prev update_node.next = self.list_tail update_node.prev = self.list_tail.prev self.list_tail.prev.next = update_node self.list_tail.prev = update_node self.key_value_dict[key] = value
BM 101 设计LFU缓存结构
url:牛客
BM101
考察知识点:堆、哈希
我的实现是通过一个堆来记录插入元素的优先级,优先级低的就会被删除。为了实现先比较操作次数再比较操作时间,实现一个堆元素的对象并重写其__lt__来自定义比较行为。对于要修改堆中元素优先级的操作(比如获取LFU中已有元素或者更新LFU已有元素),采用Python文档里比较迂回的实现,即标记旧元素为删除后插入一个新的数据元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import heapqclass HeapElement : def __init__ (self, call_num: int , call_time: int , key ) -> None : self.call_num = call_num self.call_time = call_time self.key = key self.delete = False def __lt__ (self, another ): if not isinstance (another, HeapElement): raise Exception(f"无法比较的类型: {type (self)} , {type (another)} " ) if self.call_num == another.call_num: return self.call_time < another.call_time else : return self.call_num < another.call_num class Solution : def LFU (self , operators: List [List [int ]], k: int ) -> List [int ]: heap = [] key2heap_element = {} key2value = {} capacity, key_num = k, 0 res = [] for i, op in enumerate (operators): if op[0 ] == 1 : key, value = op[1 ], op[2 ] if key_num == capacity and key not in key2value: heap_element = heapq.heappop(heap) while heap_element.delete: heap_element = heapq.heappop(heap) key2value.pop(heap_element.key) key_num -= 1 if key in key2value: update_element = key2heap_element.pop(key) new_element = HeapElement(update_element.call_num + 1 , i, key) update_element.delete = True heapq.heappush(heap, new_element) key2heap_element[key] = new_element else : new_element = HeapElement(1 , i, key) heapq.heappush(heap, new_element) key2heap_element[key] = new_element key_num += 1 key2value[key] = value else : key = op[1 ] if key in key2value: update_element = key2heap_element.pop(key) new_element = HeapElement(update_element.call_num + 1 , i, key) update_element.delete = True heapq.heappush(heap, new_element) key2heap_element[key] = new_element res.append(key2value.get(key, -1 )) return res
这个方法有个不好的点就是这边的堆在需要更新元素频率时是插入新元素并把旧元素标记为删除的。这在插入时依然能保持\(O(\log
n)\) 的复杂度,但是当需要从堆中弹出元素删除时,可能需要多次弹出被删除的元素,每次堆的弹出操作需要\(O(\log
n)\) 的时间复杂度,所以最坏情况下可能会超过\(O(\log n)\) 的时间复杂度。
题解的做法是维护了一个最小频率的队列,但是从这个队列里面删除元素需要遍历一遍这个队列,假设所有已有元素频率都是1,那么更新一个键的频率的复杂度也可能达到\(O(n)\) ,似乎没办法总是保证插入和获取键的最坏时间复杂度控制在\(O(\log n)\) 。该方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 class HeapElement : def __init__ (self, call_num: int , call_time: int , key ) -> None : self.call_num = call_num self.call_time = call_time self.key = key self.delete = False def __lt__ (self, another ): if not isinstance (another, HeapElement): raise Exception(f"无法比较的类型: {type (self)} , {type (another)} " ) if self.call_num == another.call_num: return self.call_time < another.call_time else : return self.call_num < another.call_num class Solution : def LFU (self , operators: List [List [int ]], k: int ) -> List [int ]: freq2keys = {} key2freq = {} min_freq = 1 key2value = {} capacity, key_num = k, 0 res = [] for i, op in enumerate (operators): if op[0 ] == 1 : key, value = op[1 ], op[2 ] if key_num == capacity and key not in key2value: remove_key = freq2keys[min_freq].pop(0 ) key2value.pop(remove_key) key2freq.pop(remove_key) key_num -= 1 if key in key2value: freq = key2freq[key] freq2keys[freq].remove(key) freq = freq + 1 if freq not in freq2keys: freq2keys[freq] = [key] else : freq2keys[freq].append(key) key2freq[key] = freq if len (freq2keys[min_freq]) == 0 : min_freq = freq else : freq = 1 if freq not in freq2keys: freq2keys[freq] = [key] else : freq2keys[freq].append(key) key2freq[key] = freq min_freq = freq key_num += 1 key2value[key] = value else : key = op[1 ] if key in key2value: freq = key2freq[key] freq2keys[freq].remove(key) freq = freq + 1 if freq not in freq2keys: freq2keys[freq] = [key] else : freq2keys[freq].append(key) key2freq[key] = freq if len (freq2keys[min_freq]) == 0 : min_freq = freq res.append(key2value.get(key, -1 )) return res
结语
到这里牛客的面试TOP101就做完了,感觉做完之后确实也有不小的收获,但是仍然感觉手比较生,有一些问题依旧是比较模糊。
但是后面马上要开题了,精力可能得往写开题报告上转移,可能也没太多时间写面试编程题了。(再说找实习也并不顺利,投了简历半个多月没回应....)
后续(开题报告这阵子事忙完之后)可能我会回顾一下经典的NLP模型论文,尤其是大语言模型LLM路径上的一些关键模型的论文,比如BERT、RoBERTa、GPT、GPT3、T5、OPT、BLOOM、LLAMA等模型的论文,暂定这个系列为“朝花夕拾”吧。(虽然没把握能写完,但先挂在这,以后写不完再想办法)
再之后是代码实现系列,我准备用《三国演义》作为语料,然后实现一个256词表大小的中文nanoGPT,致敬一下Andrej
Karpathy大佬的英文莎士比亚nanoGPT教程 ,也作为GPT这样的Decoder only
transformer和自注意力机制的实现的训练。