Summary of Algorithm Problems and Techniques
数组
- 二分查找:适用于快速缩小范围。可调整返回值和分支条件来实现不同目标
- 双指针:起始位置、移动速度、终止条件
- 滑动窗口:使用前提为不存在两个局部最优解,其中一个被另一个真包含。先扩展到符合要求,再收缩直到不符合,最后比较。需要维护窗口信息(如上一次出现元素的位置、最大最小值、已经匹配条件的位置数等)
- 子序列的和可以等价于前缀和之差,用于动态规划
- 牛顿迭代法:适用于求函数的近似零点,用切线逼近
链表
- 辅助节点
- 通常可用递归和迭代两种方式求解
- 双指针:起始位置、移动速度、终止条件。有时需要利用链表中节点的距离关系不变性
哈希表
- 可用作计数器、visited 集合、反向索引
- 键较简单时(字符),可以使用数组来替代,取得更高效率
- 如果只需要 0-1 计数器,则可退化为集合(判断重复)
- n 数之和:反向索引、预排序+双指针、迭代确定起点+跳过元素去重
- 桶排序
字符串
- KMP:考察前缀与后缀相等的长度
- 重复子串问题:尝试在末尾拼接原字符串
- 回文串问题:如果不修改原字符串,则可以动态规划判断所有子串是否为回文串;如果要修改原字符串,则可以尝试直接比较,处理异常位置
- 可以使用 trie 来加快匹配速度
struct Trie {
bool terminal = false;
unordered_map<char, Trie *> children;
void addPath(const string &word) {
Trie *tmp = this;
for (const char c : word) {
if (!tmp->children.count(c))
tmp->children[c] = new Trie();
tmp = tmp->children[c];
}
tmp->terminal = true;
}
};
- 字符串的暴力匹配/搜索有时并不会带来较大的开销
栈与队列
- 可用的数据结构:栈、队列、双端队列(滑动窗口最值)、优先队列
- 最大的 k 个元素:小顶堆、快速切分
二叉树
- 前序遍历、后序遍历、中序遍历:都基于 DFS 思路实现,使用栈进行迭代
- 通用迭代方式:标记父节点
- 前序遍历迭代:正常顺序的入栈(遍历问题的常见解答)
- 中序遍历迭代:先沿左侧往下依次入栈,弹出栈顶并处理,移到右子树
- 层序遍历:BFS 两层循环,内循环使用队列的长度。可以通过控制入队顺序来实现功能
- 递归:自顶向下(传参数),自底向上(返回值)
- 二叉树中每个节点的位置可以用一个二进制数表示(Huffman)
- 使用哈希表构造指向父节点的反向链表
- 二叉树的构造:可以使用队列、栈(单调栈)等结构来辅助,维护顺序
- 在遍历中加入 NULL 即可使前序后序遍历结果与树一一对应
回溯算法
- 通用解答:当前路径记录+递归,终止条件+剪枝+选不选当前元素
- 递归思路:
- 每次考察结果序列中的下一元素,判断要如何选择。选择即立刻输出结果
- 每次考察原始序列中的下一元素,判断要不要添加到结果中。当遍历完原始序列后输出结果
- 优化思路:剪枝、启发式搜索(更快找到有效解)
- 去重:主动查找(不选时跳过相等元素),被动判断(如果之前选择了相等元素则必须选择)
- 主动查找:…., x…, xx.., xxx., xxxx
- 被动查找:…., …x, ..xx, .xxx, xxxx
- 元素无序时需要预排序才可以去重
- 全子集问题:使用二进制位向量表示
- 结果有序时需要添加额外的 visited 数组(可用位向量替代)来追踪路径
贪心算法
- 区间是连续变化的,只需考虑最远位置即可
- 排序+依次按贪心原则分配
- 负和清零:累计和为负时,表明以此为终点的子序和都为负,从零开始
- 重叠区间:排序后,从第一个区间开始,后续区间有添加(或新建区间)/替换(或更新区间)上一个两种选择
- 按区间起止点排序会得到不同策略:区间交集/贪心选择
- 从结果反推
- 如何选择第一个区间(最左侧)?选择一个区间终点最小的即可,否则总是可以将它替换。
- 如何选择下一个?忽略区间起点落在所有已选择区间的最大终点左侧的区间(按这种方式选择,其左侧不可能存在完整区间),选择剩余区间中终点最小的。
单调栈
- 单调栈的选择:单调性、原数组迭代方向、存储索引/值
- 一次遍历中,使用一个单调栈可以同时得到两组数值(入栈、出栈时计算):(单调递增栈)上一个小于它的元素、下一个小于等于它的元素
- 核心思路:已被弹出的元素不会再被使用到(大小关系被覆盖)
- 可以在入栈或出栈时更新结果,需要的迭代方向相反,但栈的单调性相同
- 同时要求左右两侧的最大值时(接雨水)无需单调栈(可用双指针简化)
- 同时要求左右两侧的下一个更大值时,可以使用一次遍历解决(条件略有差异)
动态规划
- 最优子结构(递推公式)、重叠子问题(无后效性)
- 滚动数组优化:减小空间复杂度。对于一般的动态规划问题,分析 dp 数组的依赖关系后,通常可以通过调整运算顺序的方式优化空间
- 0-1 背包/完全背包:二维 dp 数组(重量、物品编号,内容为最大价值),可优化为一维数组。0-1 背包需要倒序遍历,完全背包需要正序遍历
- 有选择顺序的完全背包:交换内外层迭代顺序
- 正负号反转/选择的求和问题可以转化为子集求和问题
- 摆动序列/股票问题:讨论所有可能的状态,分别维护各自的 dp 数组,递推求解
- 回文串:动态规划 dp 仅取决于对角线元素,可滚动优化
图论
- 建立隐式的边:寻找节点之间可能存在的关系(如字符串中的 wildcard 字符相当于建立了一个等价类集合,其中节点是全连接的)
- 并查集的优化:仅连接根节点、均衡并查集规模、修剪树高度
- 删冗余边:分析目标图的特点(节点入度、无环)
- 最小生成树:Prim,Kruskal
- 常用解题思路:通用搜索算法 $O(n+m)$(栈/队列)、$O(m\log m)$(优先队列,如果可以修正队列元素则为$O((n+m)\log n)$)、$O(n^2)$(距离数组);负权最短路径(Bellman-Ford,Floyd-Warshall)