1. LeetCode刷题方法论精要
作为程序员群体中广为人知的算法训练平台,LeetCode的每日刷题计划已成为技术人提升核心竞争力的必修课。今天要分享的这套Day10训练方案,是我在连续三年坚持每日刷题过程中总结出的高效实践框架,特别适合已经掌握基础数据结构、准备系统提升算法思维的中级开发者。
这套训练方案最显著的特点是采用"问题类型矩阵"的编排方式——每天固定练习四种不同解题范式(如动态规划+回溯+贪心+双指针),通过刻意练习形成肌肉记忆。以Day10为例,我们重点突破以下四个经典问题类型:
- 二维动态规划的空间优化(72.编辑距离)
- 回溯算法的剪枝技巧(51.N皇后)
- 贪心算法的反证思路(134.加油站)
- 双指针的滑动窗口变种(76.最小覆盖子串)
关键认知:持续有效的算法训练不在于刷题数量,而在于每道题都能挖掘出三种以上解法,并清楚每种解法的时间/空间复杂度trade-off
2. 动态规划专题:编辑距离的降维打击
2.1 问题重述与暴力解法
LeetCode 72题要求计算将word1转换成word2所需的最少操作次数(允许插入、删除、替换字符)。我们先建立最直观的DP解法:
python复制def minDistance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1):
dp[i][0] = i
for j in range(n+1):
dp[0][j] = j
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
return dp[m][n]
这个二维DP解法时间复杂度O(mn),空间复杂度O(mn)。观察状态转移方程会发现,当前状态只与上一行和当前行的前一个状态相关,这就引出了优化可能。
2.2 空间优化技巧
我们可以将二维DP数组压缩为一维数组,关键点在于需要保存左上角的值(dp[i-1][j-1]):
python复制def minDistance(word1, word2):
m, n = len(word1), len(word2)
dp = [j for j in range(n+1)]
for i in range(1, m+1):
pre = dp[0] # 保存左上角的值
dp[0] = i
for j in range(1, n+1):
temp = dp[j]
if word1[i-1] == word2[j-1]:
dp[j] = pre
else:
dp[j] = 1 + min(dp[j], dp[j-1], pre)
pre = temp
return dp[n]
优化后空间复杂度降为O(n)。这个技巧适用于大多数二维DP问题,特别是当状态转移只依赖有限的前置状态时。
3. 回溯算法实战:N皇后问题的剪枝艺术
3.1 标准回溯解法
LeetCode 51题要求在N×N棋盘上放置N个皇后,使其互不攻击。基础回溯解法如下:
python复制def solveNQueens(n):
def backtrack(row, cols, diag1, diag2, path):
if row == n:
res.append(['.'*col + 'Q' + '.'*(n-col-1) for col in path])
return
for col in range(n):
d1, d2 = row-col, row+col
if col not in cols and d1 not in diag1 and d2 not in diag2:
backtrack(row+1, cols|{col}, diag1|{d1}, diag2|{d2}, path+[col])
res = []
backtrack(0, set(), set(), set(), [])
return res
这个解法使用三个集合分别记录已被占用的列、主对角线和副对角线,时间复杂度为O(N!),因为每行有N种可能的选择。
3.2 位运算优化
使用位图可以大幅提升判断效率:
python复制def solveNQueens(n):
def backtrack(row, cols, diag1, diag2, path):
if row == n:
res.append(['.'*col + 'Q' + '.'*(n-col-1) for col in path])
return
available = ((1 << n) - 1) & ~(cols | diag1 | diag2)
while available:
col = available & -available
backtrack(row+1, cols | col, (diag1 | col) << 1, (diag2 | col) >> 1, path + [col.bit_length()-1])
available &= available - 1
res = []
backtrack(0, 0, 0, 0, [])
return res
位运算版本将集合操作转换为位操作,常数时间更优。特别是对于N>12的大规模问题,性能提升明显。
4. 贪心算法精析:加油站的环形之旅
4.1 问题建模
LeetCode 134题给出环形路线上的加油站gas和耗油量cost数组,判断从哪个加油站出发可以完成环形旅行。暴力解法O(n^2)明显不够高效。
贪心算法的核心观察是:
- 如果总gas >= 总cost,必定有解
- 从某个站出发如果油量不够到达下一站,那么起点到当前站之间的所有站都不能作为起点
4.2 实现代码
python复制def canCompleteCircuit(gas, cost):
total = current = start = 0
for i in range(len(gas)):
diff = gas[i] - cost[i]
total += diff
current += diff
if current < 0:
start = i + 1
current = 0
return start if total >= 0 else -1
这个解法只需一次遍历,时间复杂度O(n)。关键在于理解当累计油量current为负时,之前的所有站点都不能作为起点。
5. 双指针进阶:最小覆盖子串的滑动窗口
5.1 问题分析
LeetCode 76题要求在字符串s中找到包含字符串t所有字符的最短子串。滑动窗口的标准解法:
python复制def minWindow(s, t):
from collections import defaultdict
need = defaultdict(int)
for c in t:
need[c] += 1
need_cnt = len(t)
res = (0, float('inf'))
left = 0
for right, c in enumerate(s):
if need[c] > 0:
need_cnt -= 1
need[c] -= 1
if need_cnt == 0:
while True:
c = s[left]
if need[c] == 0:
break
need[c] += 1
left += 1
if right - left < res[1] - res[0]:
res = (left, right)
need[s[left]] += 1
need_cnt += 1
left += 1
return s[res[0]:res[1]+1] if res[1] != float('inf') else ""
5.2 优化技巧
- 使用collections.defaultdict简化计数逻辑
- need_cnt变量避免每次检查整个need字典
- 移动左指针时,只有当need[c]==0时才停止,确保窗口最小
这个模板可以解决大多数子串匹配问题,时间复杂度O(n),空间复杂度O(k)(k为字符集大小)。
6. 刷题实战建议
- 计时训练:每道题限制在25分钟内完成,模拟面试场景
- 三色标记法:
- 绿色:15分钟内独立AC
- 黄色:需要查看提示
- 红色:完全没思路
- 错题本机制:记录每道题的思维盲点和优化空间
- 同类型强化:对薄弱题型集中练习(如连续3天刷动态规划)
我个人的经验是,坚持每天按这个模式训练2小时,三个月后算法能力会有质的飞跃。特别是在处理边界条件和优化空间复杂度方面,会形成条件反射式的思维模式。