ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

程序员刷题笔记:动态规划与Dijkstra算法实战

程序员刷题笔记:动态规划与Dijkstra算法实战

1. 刷题笔记的价值与意义

作为一名程序员,我坚持刷题已经五年有余。2026年初的这两天,我像往常一样在leetCode和牛客网上刷题,记录下了一些值得分享的心得。刷题笔记对于程序员来说,远不止是简单的解题记录,它更像是一本个人成长日记,记录着我们在算法思维和编程能力上的点滴进步。

好的刷题笔记应该包含以下几个核心要素:题目描述、初始思路、遇到的坑、优化过程和最终解法。我习惯用Markdown来整理笔记,因为它的层级结构清晰,代码块展示方便,还能添加数学公式。比如下面这个简单的模板:

# 题目名称 ## 问题描述 [题目内容] ## 初次尝试 [初始思路和代码] ## 遇到的问题 [运行时错误/边界条件/性能问题] ## 优化过程 [思路调整和算法优化] ## 最终解法 [最优解代码和复杂度分析]

2. 2026年1月25日的刷题记录

2.1 动态规划专题:股票买卖问题

今天重点攻克了动态规划中的股票买卖系列问题。这类问题看似简单,但变种很多,容易混淆。我记录了六种常见变体的解题思路:

  1. 只能买卖一次(LeetCode 121)
  2. 可以买卖无限次(LeetCode 122)
  3. 最多买卖两次(LeetCode 123)
  4. 最多买卖k次(LeetCode 188)
  5. 含冷冻期(LeetCode 309)
  6. 含手续费(LeetCode 714)

以经典的"只能买卖一次"为例,我最初想用暴力解法:

def maxProfit(prices): max_profit = 0 for i in range(len(prices)): for j in range(i+1, len(prices)): profit = prices[j] - prices[i] if profit > max_profit: max_profit = profit return max_profit

这个解法时间复杂度是O(n²),显然不够高效。经过思考,我优化为O(n)的解法:

def maxProfit(prices): min_price = float('inf') max_profit = 0 for price in prices: if price < min_price: min_price = price elif price - min_price > max_profit: max_profit = price - min_price return max_profit

关键点:维护一个最小值变量,同时计算当前价格与最小值的差。

2.2 遇到的边界条件问题

在实现"含冷冻期"的问题时,我遇到了一个有趣的边界条件bug。初始状态转移方程写成了:

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])

但在i=1时,dp[i-2]会越界。经过调试,我意识到需要单独处理前两天的状态:

if i == 0: dp[0][0] = 0 dp[0][1] = -prices[0] continue if i == 1: dp[1][0] = max(dp[0][0], dp[0][1] + prices[1]) dp[1][1] = max(dp[0][1], -prices[1]) continue

这个教训让我明白:动态规划问题一定要仔细考虑初始状态,特别是涉及多天前的状态时。

3. 2026年1月26日的刷题记录

3.1 图论专题:Dijkstra算法实现

今天重点研究了Dijkstra算法的几种实现方式。我对比了三种不同时间复杂度的实现:

  1. 暴力搜索:O(V²)
  2. 二叉堆优先队列:O(E log V)
  3. 斐波那契堆:O(E + V log V)

以二叉堆实现为例,我最初写出的代码存在一个常见错误:

import heapq def dijkstra(graph, start): distances = {node: float('inf') for node in graph} distances[start] = 0 heap = [(0, start)] while heap: current_dist, current_node = heapq.heappop(heap) for neighbor, weight in graph[current_node].items(): distance = current_dist + weight if distance < distances[neighbor]: distances[neighbor] = distance heapq.heappush(heap, (distance, neighbor)) return distances

这个实现的问题在于:同一个节点可能会被多次加入堆中,导致不必要的计算。正确的做法是只处理第一次弹出的距离:

def dijkstra(graph, start): distances = {node: float('inf') for node in graph} distances[start] = 0 heap = [(0, start)] visited = set() while heap: current_dist, current_node = heapq.heappop(heap) if current_node in visited: continue visited.add(current_node) for neighbor, weight in graph[current_node].items(): distance = current_dist + weight if distance < distances[neighbor]: distances[neighbor] = distance heapq.heappush(heap, (distance, neighbor)) return distances

3.2 算法优化中的空间换时间

在解决一道关于字符串匹配的问题时,我深刻体会到了空间换时间的优化策略。题目要求判断一个字符串是否能由另一个字符串通过旋转得到(如"waterbottle"和"erbottlewat")。

最初的想法是生成所有可能的旋转字符串进行比较:

def isRotation(s1, s2): if len(s1) != len(s2): return False for i in range(len(s1)): rotated = s1[i:] + s1[:i] if rotated == s2: return True return False

这个解法时间复杂度是O(n²)。后来我想到一个巧妙的优化:如果s2是s1的旋转,那么s2必定是s1+s1的子串:

def isRotation(s1, s2): return len(s1) == len(s2) and s2 in s1 + s1

这个解法时间复杂度取决于字符串查找的实现,通常是O(n),但需要O(n)的额外空间。这个例子很好地展示了如何通过增加空间复杂度来降低时间复杂度。

4. 刷题方法论与工具链

4.1 我的刷题工作流

经过多年实践,我总结出一套高效的刷题工作流:

  1. 题目选择:按照tag和频率排序,优先做高频题
  2. 时间管理:使用番茄钟(25分钟专注+5分钟休息)
  3. 解题步骤:
    • 先理解题意,确认输入输出
    • 思考暴力解法
    • 优化时间和空间复杂度
    • 考虑边界条件
  4. 笔记整理:当天晚上回顾并完善笔记
  5. 定期复习:使用Anki进行间隔重复

我常用的工具组合:

  • VS Code + LeetCode插件:方便本地调试
  • draw.io:画图辅助理解复杂算法
  • Notion:整理分类笔记
  • GitHub:版本控制和管理代码

4.2 如何从刷题中最大化收益

单纯的刷题数量并不能保证能力的提升,关键在于方法和反思。我总结了几个提高刷题效率的技巧:

  1. 分类突破:按算法类型集中练习(如一周专攻动态规划)
  2. 五步解题法:
    • 先自己思考15分钟
    • 看题解理解思路
    • 独立实现
    • 对比最优解
    • 给别人讲解
  3. 错题本:记录所有做错的题目,分析错误原因
  4. 模拟面试:用随机题目进行限时练习

特别重要的是要记录自己的思维过程。比如在解决二叉树问题时,我通常会先画出几个测试用例,手动模拟算法运行过程,这比直接写代码更能加深理解。

5. 算法面试的实战技巧

5.1 面试中的沟通策略

算法面试不仅仅是写代码,更重要的是展示解决问题的过程。我总结了几个关键点:

  1. 明确问题:用自己的话复述题目,确认理解正确
  2. 举例说明:用具体例子演示思路
  3. 逐步推进:从暴力解法开始,逐步优化
  4. 边界检查:主动讨论特殊情况
  5. 复杂度分析:解释时间和空间复杂度

例如,当面试官问"如何判断链表是否有环"时,好的回答流程应该是:

"这个问题是检测链表中是否存在循环。我先举一个例子:1→2→3→4→2,这个链表在节点4后又指向了节点2,形成了环。

最直观的解法是用哈希表记录访问过的节点,时间复杂度O(n),空间复杂度O(n)。不过我们可以用快慢指针法优化空间复杂度到O(1):让快指针每次走两步,慢指针每次走一步,如果存在环,它们最终会相遇。"

5.2 白板编程的注意事项

现场白板编程与平时在IDE中写代码有很大不同,需要特别注意:

  1. 书写工整:保持代码清晰可读
  2. 分段实现:先写框架,再填充细节
  3. 及时解释:边写边说明思路
  4. 预留空间:为可能的修改留出空白
  5. 测试用例:写出几个典型测试案例

我习惯按照这个顺序写白板代码:

  1. 函数签名和注释
  2. 主要变量定义
  3. 算法主体逻辑
  4. 返回结果
  5. 在旁边写出测试用例

6. 刷题进阶路线图

6.1 从初学者到高手的成长路径

根据我的经验,算法能力的提升可以分为几个阶段:

  1. 基础阶段(1-3个月):

    • 掌握基本数据结构:数组、链表、栈、队列、哈希表
    • 理解基础算法:排序、二分查找、递归
    • 完成LeetCode Easy难度100题
  2. 进阶阶段(3-6个月):

    • 掌握高级数据结构:堆、树、图
    • 理解经典算法:DFS/BFS、动态规划、贪心
    • 完成LeetCode Medium难度150题
  3. 精通阶段(6-12个月):

    • 深入理解算法优化技巧
    • 熟练解决Hard难度问题
    • 研究竞赛级算法
  4. 大师阶段(1年以上):

    • 参与算法竞赛
    • 原创算法解决方案
    • 教授他人算法知识

6.2 推荐的学习资源

经过多年筛选,我认为这些资源最有价值:

书籍:

  • 《算法导论》:理论基础
  • 《剑指Offer》:面试必备
  • 《编程珠玑》:算法思维

在线课程:

  • 算法Part1/Part2(普林斯顿大学Coursera)
  • 数据结构与算法(MIT OpenCourseWare)

刷题平台:

  • LeetCode:面试准备
  • Codeforces:算法竞赛
  • TopCoder:挑战性问题

可视化工具:

  • VisuAlgo:算法可视化
  • Algorithm Visualizer:交互式学习

7. 常见问题与解决方案

7.1 刷题遇到的典型困难

在长期刷题过程中,我遇到了各种困难,以下是几个典型问题及解决方法:

问题1:看题没思路

  • 解决方法:先暴力解法,再逐步优化;画图辅助理解;分解问题为子问题

问题2:总是忽略边界条件

  • 解决方法:先写测试用例;考虑空输入、极值、特殊数据;使用TDD方法

问题3:知道算法但实现不出来

  • 解决方法:先写伪代码;分模块实现;多练习手写代码

问题4:学完就忘

  • 解决方法:做笔记;定期复习;尝试给别人讲解

7.2 调试技巧分享

高效的调试能大幅提高刷题效率,我的调试方法论:

  1. 小黄鸭调试法:向无生命的对象解释代码
  2. 增量调试:分步验证代码片段
  3. 可视化调试:用画图展示数据结构变化
  4. 日志调试:在关键点打印变量状态
  5. 测试驱动:先写测试用例再写代码

例如,调试二叉树相关代码时,我会先画出树的结构,然后手动模拟代码执行过程,在每一步记录变量的值。对于递归算法,我会缩进打印递归深度,帮助理解调用栈。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进