算法刷题小结

算法刷题中常见的数据结构、算法技巧与思路总结

算法刷题小结

这里整理刷题过程中反复出现的一些基础结构和常见套路,目标不是展开完整教程,而是把后续做题时最容易复用的判断方式先记下来。

1. 区间更新与查询

1.1 前缀和

适合频繁查询、不频繁更新的场景,典型问题是区间查询、单点更新。

  • 维护一个前缀和数组。
  • 查询时通过前缀和差值拿到区间结果。
  • 时间复杂度:查询 O(1),更新 O(n)

1.2 差分

适合频繁更新、不频繁查询的场景,典型问题是区间更新、单点查询。

  • 维护一个差分数组。
  • 更新时只改边界位置,最后再还原原数组。
  • 时间复杂度:更新 O(1),查询 O(n)

1.3 树状数组

适合频繁更新和查询的场景,常见形式是单点更新、区间查询。

  • 本质是维护一个辅助数组,通过 lowbit 做增量聚合。
  • 代码量通常比线段树更小,适合处理基础动态前缀问题。
  • 时间复杂度:更新和查询均为 O(log n)

1.4 线段树

适合频繁更新和查询的场景,常见形式是单点更新、区间查询。

  • 通过树形结构维护区间信息。
  • 比树状数组更通用,适合处理更复杂的区间合并逻辑。
  • 时间复杂度:更新和查询均为 O(log n)
  • 加上 Lazy Tag 后可以支持区间更新,复杂度仍然保持在 O(log n)

2. 常见算法

2.1 二分查找

适合结果本身不容易直接求,但“给定答案后是否成立”比较容易验证的场景。核心是不断缩小搜索区间,找到目标值或满足条件的边界。

2.2 双指针

适合有序数组,或者需要同时遍历两个序列的场景。常见用途是寻找满足条件的元素对、压缩状态空间,或者维护左右边界。

2.3 滑动窗口

适合寻找满足条件的连续子序列。核心是维护一个动态区间,并在移动过程中更新窗口内的信息。

2.4 递归

适合需要枚举所有可能解的场景。

  • 向下递归时可以携带当前路径信息。
  • 向上回溯时可以基于整条路径做计算。
  • 配合剪枝通常能显著减小搜索空间。

2.5 分治

适合当前问题可以拆成更小子问题,并且子问题结果能够高效合并的场景,例如归并排序、快速排序。

3. 进阶算法

3.1 单调栈

适合需要维护单调递增或单调递减序列的场景,很多题的关键是先判断是否存在局部单调性。时间复杂度通常是 O(n)

3.2 贪心

适合每一步局部最优选择最终能推出全局最优解的场景。难点通常不在实现,而在于证明当前策略为什么成立。

3.3 动态规划

适合最优化问题。核心难点通常是:

  • 根据题意定义状态。
  • 找到状态转移关系。
  • 处理边界条件和状态压缩。

很多题最后能不能做出来,关键就在于能不能先把状态设计对。

3.4 并查集

适合处理元素分组或连通关系问题,时间复杂度通常写作 O(alpha(n))

  • 用一个数组维护每个元素所属集合。
  • 通过合并和查询处理连通性、分组归并等问题。

3.5 数学

常见内容包括排列组合、数论性质、构造、通过数学结论缩小搜索空间等。很多题表面像暴力,真正突破口在性质分析。

3.6 博弈

双人、状态有限、信息完全、没有随机性的博弈题,经常可以通过找必败态和必胜态来分析。

  • 常见做法是先从最小状态举例,逐步往上推规律。
  • 有些题还能用“先手窃取策略”做反证。

下面给一个两堆糖果的例子:

两人从两堆糖果中轮流取糖果,最后取走最后一颗的人获胜。每次只能从任意一堆中取任意数量,但不能同时从两堆取。

  • 当状态是 (0, k)(k, 0) 时,当前玩家直接取完获胜。
  • 当状态是 (1, 1) 时,当前玩家无论取哪一堆,都会把状态交给对手的 (0, 1)(1, 0),因此 (1, 1) 是必败态。
  • 当状态是 (2, 2) 时,当前玩家无论怎么取,都会把一个“不相等”的状态交给对手;对手总能再把状态调整回 (1, 1),因此 (2, 2) 也是必败态。
  • 当状态是 (2, 3) 时,当前玩家可以直接取成 (2, 2),把必败态交给对手,因此 (2, 3) 是必胜态。

继续往下推,可以发现:

  • 两堆数量相等时,当前玩家处于必败态。
  • 两堆数量不相等时,当前玩家总能把状态调整成相等,从而把必败态交给对手。
Licensed under CC BY-NC-SA 4.0