目录
九、LC 124. 二叉树中的最大路径和
9.1 题求
9.2 求解
法一:DFS
# 72ms - 89.25%
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
def dfs(node):
nonlocal max_path_sum
# 最大左、右子树路径
left_val = dfs(node.left) if node.left else float('-inf')
right_val = dfs(node.right) if node.right else float('-inf')
max_path = max(left_val, right_val)
max_val = max(node.val, max_path, node.val + left_val + right_val)
# 若当前子树超过最大值, 则更新
if max_val > max_path_sum:
max_path_sum = max_val
# 返回当前子树的最大路径
return node.val + max(0, max_path)
max_path_sum = float('-inf')
final = dfs(root)
return max(max_path_sum, final)
官方说明
# 72ms - 89.25%
class Solution:
def __init__(self):
self.maxSum = float("-inf")
def maxPathSum(self, root: TreeNode) -> int:
def maxGain(node):
if not node:
return 0
# 递归计算左右子节点的最大贡献值
# 只有在最大贡献值大于 0 时,才会选取对应子节点
leftGain = max(maxGain(node.left), 0)
rightGain = max(maxGain(node.right), 0)
# 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
self.maxSum = max(self.maxSum, node.val + leftGain + rightGain)
# 返回节点的最大贡献值
return node.val + max(leftGain, rightGain)
maxGain(root)
return self.maxSum
参考资料:
十、LC 17. 电话号码的字母组合
10.1 题求
10.2 求解
法一:回溯
# 28ms - 90.13%
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
hashmap = {'2': "abc", '3': "def", '4': "ghi", '5': "jkl",
'6': "mno", '7': "pqrs", '8': "tuv", '9': 'wxyz'}
res = []
def traverse(arr, idx):
if len(arr) == len(digits):
res.append(''.join(arr))
return
idx += 1
for c in hashmap[digits[idx]]:
arr.append(c)
traverse(arr, idx)
arr.pop()
traverse([], -1)
return res
参考资料:
十一、LC 486. 预测赢家 ☆
11.1 题求
11.2 求解
官方说明
# 5460ms - 7.26%
class Solution:
def PredictTheWinner(self, nums: List[int]) -> bool:
def total(start: int, end: int, turn: int) -> int:
if start == end:
return nums[start] * turn
scoreStart = nums[start] * turn + total(start + 1, end, -turn)
scoreEnd = nums[end] * turn + total(start, end - 1, -turn)
return max(scoreStart * turn, scoreEnd * turn) * turn
return total(0, len(nums)-1, 1) >= 0
# 36ms - 78.35%
class Solution:
def PredictTheWinner(self, nums: List[int]) -> bool:
length = len(nums)
# dp[i,j] 表示下标范围 [i, j] 中, 当前玩家与另一玩家的分数之差的最大值
dp = [[0] * length for _ in range(length)]
for i, num in enumerate(nums):
# i = j 时当前玩家必须选该唯一数字
dp[i][i] = num
# 之所以从 len-2 开始, 是因为 dp[len-1][len-1] 的值已经确定为 num[len-1]
for i in range(length-2, -1, -1): # 先左
for j in range(i+1, length): # 后右
# 状态转移 - 选左 nums[i] - dp[i+1][j] - 选右 nums[j] - dp[i][j-1]
# 之所以是 -dp[][] 是因为假设玩家 1 相对于玩家 2 的分数差为 d,
# 则玩家 2 相对于玩家 1 的分数差为 -d, 可见二者是互为相反数的关系
dp[i][j] = max(nums[i] - dp[i+1][j], nums[j] - dp[i][j-1]) # ☆
# 先手玩家 (针对全范围 [0, len-1] 选择) 相对于后手玩家的分数之差不小于 0, 则先手玩家获胜
return dp[0][length-1] >= 0
# 32ms - 89.17%
class Solution:
def PredictTheWinner(self, nums: List[int]) -> bool:
length = len(nums)
dp = [0] * length
for i, num in enumerate(nums):
dp[i] = num
for i in range(length - 2, -1, -1):
for j in range(i + 1, length):
# 更新本轮的 dp[j] 需要上一轮的 dp[j] 和本轮的 dp[j-1]
dp[j] = max(nums[i] - dp[j], nums[j] - dp[j-1]) # 先后顺序一定要正确
return dp[length-1] >= 0 # 右边界
参考资料:
十二、LC 5. 最长回文子串
12.1 题求
12.2 求解
法一:动态规划
# 6240ms - 37.71%
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n < 2:
return s
# 最长回文串的左、右下标
left = right = 0
# dp[i][j]:表示 s[i: j+1] 是否为回文串
dp = [[False for _ in range(n)] for _ in range(n)]
# i == j 的单个字符串总为回文串
for i in range(n):
dp[i][i] = True
for i in range(n-2, -1, -1):
for j in range(i+1, n):
# 当前长度
length = j - i
# 状态转移
if s[i] == s[j]:
dp[i][j] = True if length < 3 else dp[i+1][j-1]
# 最长更新
if dp[i][j] and length > right-left:
left, right = i, j
return s[left: right+1]
官方说明
# 7620ms - 19.26%
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n < 2:
return s
max_len = 1
begin = 0
# dp[i][j] 表示 s[i..j] 是否是回文串
dp = [[False] * n for _ in range(n)]
# i == j 的单个字符串总为回文串
for i in range(n):
dp[i][i] = True
# 枚举子串长度 L
for L in range(2, n+1):
# 枚举左边界 i
for i in range(n):
# 由 子串长度 L 和 左边界 i 确定右边界 j (闭区间)
j = i + L - 1
# 右边界越界, 则退出当前层循环
if j >= n:
break
if s[i] != s[j]:
dp[i][j] = False
else:
if j - i < 3:
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1]
# 只要 dp[i][L] == true 成立, 就表示子串 s[i..L] 是回文
# 此时记录最大回文串长度 max_len 及其起始位置 begin
if dp[i][j] and j - i + 1 > max_len:
max_len = j - i + 1
begin = i
return s[begin: begin+max_len]
# 740ms - 80.6%
class Solution:
def expandAroundCenter(self, s, left, right):
# 下标在范围内, 且两端始终对称
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
# 返回当前最大回文串边界
return left + 1, right - 1
def longestPalindrome(self, s: str) -> str:
# 最长回文串左、右边界
start, end = 0, 0
# 枚举左边界/起点 i
for i in range(len(s)):
# 边界条件 - 单字符 (奇数回文串)
left1, right1 = self.expandAroundCenter(s, i, i)
# 边界条件 - 双字符 (偶数回文串)
left2, right2 = self.expandAroundCenter(s, i, i+1)
# 比较与更新
if right1 - left1 > end - start:
start, end = left1, right1
if right2 - left2 > end - start:
start, end = left2, right2
# 返回整体最大回文串
return s[start: end+1]
参考资料:
十三、LC 121. 买卖股票的最佳时机
13.1 题求
13.2 求解
法一:动态规划 - 基本
# 2252ms - 5.08%
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
# 持有与否 k:0/1 | 最大允许交易次数 j:0/1 | 交易天数 i: n+1
dp = [[[[] for _ in range(2)] for _ in range(2)] for _ in range(n+1)]
#### 初始状态 - 规律 第 0 天一开始就持有股票是不可能的,一开始不持有且应利润为 0
dp[0][0][0] = 0 # 第 0 天, 不持有股票, 利润 0
dp[0][0][1] = float('-inf') # 第 0 天, 自带持有股票, 不可能
for i in range(n+1):
dp[i][1][0] = 0 # 第 i 天, 尚未交易, 利润 0
dp[i][1][1] = float('-inf') # 第 i 天, 尚未交易便持有股票, 不可能
# 状态转移
for i in range(1, n+1):
dp[i][0][0] = max(dp[i-1][0][0], dp[i-1][0][1]+prices[i-1])
dp[i][0][1] = max(dp[i-1][0][1], dp[i-1][1][0]-prices[i-1])
return dp[n][0][0]
法二:动态规划 - 降维
由于第 i 天仅和第 i-1 天有关,因此可使用变量代替天数)
# 340ms - 15.16%
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
# 持有与否 k:0/1 | 最大允许交易次数 j:0/1
dp = [[[] for _ in range(2)] for _ in range(2)]
# 初始状态
dp[0][0] = 0
dp[0][1] = float('-inf')
dp[1][0] = 0
dp[1][1] = float('-inf')
# 状态转移
for i in range(n):
# 顺序不能搞反, 先使用到昨天的 dp[0][1], 再更新今天的 dp[0][1]
dp[0][0] = max(dp[0][0], dp[0][1]+prices[i])
dp[0][1] = max(dp[0][1], dp[1][0]-prices[i])
return dp[0][0]
法三:迭代
# 202ms - 71.71%
class Solution:
def maxProfit(self, prices: List[int]) -> int:
min_price = float('inf') # 最小买入价格 (初始为无限大)
max_profit = 0 # 最大利润 = 最大卖出价格 - 最小买入价格
for price in prices:
# 由于必须在未来卖出股票
max_profit = max(max_profit, price-min_price) # 故先比较和确定最大利润
min_price = min(min_price, price) # 再比较和确定最小买入价格
return max_profit
参考资料:
十四、LC 100. 相同的树
14.1 题求
14.2 求解
法一:递归
# 36ms - 49.88%
class Solution:
def isSameTree(self, p: TreeNode, q: TreeNode) -> bool:
if not p and not q:
return True
elif not p or not q or p.val != q.val:
return False
return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)
法二:迭代
# 24ms - 97.74%
class Solution:
def isSameTree(self, p: TreeNode, q: TreeNode) -> bool:
deque_p = collections.deque()
deque_p.append(p)
deque_q = collections.deque()
deque_q.append(q)
while deque_p and deque_q:
nums = len(deque_p)
if nums != len(deque_q):
return False
for _ in range(nums):
p = deque_p.popleft()
q = deque_q.popleft()
if not p and not q:
continue
if not p or not q or p.val != q.val:
return False
deque_p.append(p.left)
deque_p.append(p.right)
deque_q.append(q.left)
deque_q.append(q.right)
return True
参考资料:
十五、LC 468. 验证 IP 地址
15.1 题求
15.2 求解
法一:迭代
# 28ms - 87.44%
class Solution:
def validIPAddress(self, IP: str) -> str:
if '.' in IP:
ipv4 = IP.split('.')
if len(ipv4) != 4:
return "Neither"
for add in ipv4:
if (not add) or (add[0] == '0' and len(add) > 1) or (not add.isdigit()) or (int(add) > 255):
return "Neither"
return "IPv4"
elif ':' in IP:
ipv6 = IP.split(':')
if len(ipv6) != 8:
return "Neither"
hex_digits = '0123456789abcdefABCDEF'
for add in ipv6:
if (not add) or (len(add) > 4) or not all(d in hex_digits for d in add):
return "Neither"
return "IPv6"
else:
return "Neither"
参考资料:
十六、LC 638. 大礼包
16.1 题求
16.2 求解
法一:递归
# 132ms - 33.89%
class Solution:
def shoppingOffers(self, price: List[int], special: List[List[int]], needs: List[int]) -> int:
def dfs(need, prices):
# 需求全满足, 判断最小价格并返回
if not any(need):
nonlocal lowest_prices
lowest_prices = min(lowest_prices, prices)
return
# 优惠包购买
for sp in special:
cur_need = need.copy()
buy_sp = True # 是否适合购买优惠包
for i in range(len(cur_need)):
# 购买后未超出需求
if sp[i] <= cur_need[i]:
cur_need[i] -= sp[i]
# 超出需求, 不适合购买, 结束判断
else:
buy_sp = False
break
if buy_sp:
dfs(cur_need, prices+sp[-1])
# 单买 - 即便存在优惠包, 也要尝试单独买
for i in range(len(need)):
prices += price[i] * need[i]
need[i] = 0
dfs(need, prices)
lowest_prices = float('inf')
dfs(needs, 0)
return lowest_prices
官方说明
记忆化递归
# 52ms - 93.33%
class Solution:
def shoppingOffers(self, price: List[int], special: List[List[int]], needs: List[int]) -> int:
from functools import lru_cache
@lru_cache(None)
def dfs(cur_need):
res = 0
# 单买
for i, c in enumerate(cur_need):
res += price[i] * c
# 买包
for sp in special:
# 当前需求列表
cur = []
for i, num in enumerate(sp[:-1]):
# 大于需求 - 停止判断
if num > cur_need[i]:
break
# 满足需求 - 加入列表
cur.append(cur_need[i] - num)
# for 正常结束, else 执行; for 由 break 退出, else 不执行
else:
#针对当前需求, 比较单买还是买包好
res = min(res, dfs(tuple(cur)) + sp[-1])
return res
# 由于使用了 LRU Cache, 需用 hashable 类型 tuple 取代 unhashable 类型 list
return dfs(tuple(needs))
参考资料: