排序算法总结

参考资料

排序方法 平均情况 最好情况 最坏情况 空间 稳定性
冒泡 O(n^2) O(n) O(n^2) O(1) 稳定
简单选择排序 O(n^2) O(n^2) O(n^2) O(1) 不稳定
直接插入排序 O(n^2) O(n) O(n^2) O(1) 稳定
希尔排序 O(nlogn) ~ O(n^2) O(n1.3) O(n^2) O(1) 不稳定
快速排序 O(nlogn) O(nlogn) O(n^2) O(logn)~O(n) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
  • 冒泡排序:两层循环,外循环确定起始比较位置,内循环不断移动,与后一位比较判断是否交换
  • 选择排序:两层循环,外循环控制选择次数,内循环遍历取得最值
  • 插入排序:两层循环,外循环控制选择插入的对象,内循环向前遍历插入位置
  • 希尔排序:不断迭代缩小步距的插入排序,通过大步距减少插入排序中向前遍历的次数
  • 快速排序:确定序列中的关键值,根据关键值大小划分序列,子序列继续快排
  • 归并排序:迭代到两两排序,再逐层合并排序结果
  • 桶排序
  • 基数排序
  • 计数排序

C++语言基础知识

STL 容器(AI 辅助生成、待检错)

1. vector

  • 头文件: <vector>
  • 描述: 动态数组,能够自动调整大小
  • 底层结构: 数组
  • 插入操作: emplace_back, insert
  • 访问操作: front, back, operator[], at
  • 删除操作: pop_back, erase
  • 应用场景: 当需要快速随机访问元素且不需要频繁在中间位置插入或删除元素时
  • string,头文件,使用类 vector 的数据结构

2. list

  • 头文件: <list>
  • 描述: 双向链表
  • 底层结构: 双向链表
  • 插入操作: emplace_front, emplace_back, insert
  • 访问操作: front, back
  • 删除操作: pop_front, pop_back, remove, erase
  • 应用场景: 需要频繁地在列表的任意位置进行插入和删除操作时

3. queue

  • 头文件: <queue>
  • 描述: 队列,先进先出(FIFO)的数据结构。
  • 底层结构: 通常由适配器实现,底层使用其他容器如 dequelist
  • 插入操作: push
  • 访问操作: front, back
  • 删除操作: pop
  • 应用场景: 实现 FIFO 操作逻辑,例如任务调度。

4. priority_queue

  • 头文件: <queue>
  • 描述: 优先级队列,总是弹出具有最高优先级的元素。
  • 底层结构: 通常由适配器实现,底层使用堆数据结构,如 std::vector 作为基础容器。
  • 插入操作: push
  • 访问操作: top
  • 删除操作: pop
  • 应用场景: 需要快速获取最高优先级元素的场景。

5. deque (double-ended queue)

  • 头文件: <deque>
  • 描述: 双端队列,可以在两端进行高效插入和删除
  • 底层结构: 分配的连续内存块组成的数组
  • 插入操作: emplace_front, emplace_back, push_front, push_back, insert
  • 访问操作: front, back, operator[], at
  • 删除操作: pop_front, pop_back, erase
  • 应用场景: 当需要从两端高效地添加或移除元素时

6. stack

  • 头文件: <stack>
  • 描述: 栈,后进先出(LIFO)的数据结构。
  • 底层结构: 通常由适配器实现,底层使用其他容器如 vectordeque
  • 插入操作: push
  • 访问操作: top
  • 删除操作: pop
  • 应用场景: 实现 LIFO 操作逻辑,例如表达式求值。

7. set

  • 头文件: <set>
  • 描述: 关联容器,存储唯一键值,自动排序
  • 底层结构: 平衡二叉树 (通常为红黑树)
  • 插入操作: emplace, insert
  • 访问操作: find, lower_bound, upper_bound
  • 删除操作: erase
  • 应用场景: 当需要维护一个有序集合且不允许重复元素时

8. map

  • 头文件: <map>
  • 描述: 关联容器,存储键值对,键值唯一并自动排序
  • 底层结构: 平衡二叉树 (通常为红黑树)
  • 插入操作: emplace, insert
  • 访问操作: find, lower_bound, upper_bound
  • 删除操作: erase
  • 应用场景: 当需要通过键来高效查找、插入和删除数据时

9. unordered_set

  • 头文件: <unordered_set>
  • 描述: 哈希表实现的容器,存储唯一键值
  • 底层结构: 哈希表
  • 插入操作: emplace, insert
  • 访问操作: find, count
  • 删除操作: erase
  • 应用场景: 当需要快速查找和插入操作而不关心顺序时

10. unordered_map

  • 头文件: <unordered_map>
  • 描述: 哈希表实现的容器,存储键值对,键值唯一
  • 底层结构: 哈希表
  • 插入操作: emplace, insert
  • 访问操作: find, count
  • 删除操作: erase
  • 应用场景: 当需要通过键来快速查找数据而不关心顺序时

11. pair

  • 头文件: <utility>
  • 描述: 存储两个元素的简单容器
  • 底层结构: 两个成员变量
  • 插入操作: 构造函数初始化
  • 访问操作: first, second
  • 删除操作: N/A (不可删除)
  • 应用场景: 当需要同时处理两个相关数据时

12. bitset

  • 头文件: <bitset>
  • 描述: 存储位序列
  • 底层结构: 内部实现细节依赖于实现
  • 插入操作: 构造函数初始化, set
  • 访问操作: test, operator[]
  • 删除操作: reset
  • 应用场景: 高效处理位操作,例如用于压缩算法、位图等

13. array

  • 头文件: <array>
  • 描述: 固定大小的数组容器。
  • 底层结构: 数组
  • 插入操作: N/A (不支持插入操作)
  • 访问操作: operator[], at, front, back
  • 删除操作: N/A (不支持删除操作)
  • 应用场景: 当需要固定大小的数组且不需要动态调整大小时。

无符号和有符号

1
2
unsigned short int i=(unsigned short int)(-1234);
printf("%hu\n",i);

大疆笔试

基础知识考察

题型分析

  1. 20230806 题型:10 单选 + 10 多选 + 10  判断
  2. 2025 题型:一个半小时,20 选择 + 10 多选 + 10 判断
Read more

Img-Diff

  • Paper: Img-Diff: Contrastive Data Synthesis for Multimodal Large Language Models

  • Authors: Qirui Jiao, Daoyuan Chen, Yilun Huang, Yaliang Li, Ying Shen

  • Code & Dataset: GitHub

  • Application: 提出的组件和端到端构建工作流程在 Data-Juicer 中作为数据处理算子和可配置文件来实现

专业名词

  • Multimodal Large Language Models (MLLMs)
  • Visual Question Answering (VQA)
  • Image Difference Captioning (IDC) 专注于图像之间的细微差别而不仅仅是物体描述

背景

  • 提升多模态大语言模型的表现通常有两种路径,一种是提升模型架构,另一种则是提升数据质量

  • 多数多模态大语言模型进行两阶段训练,第一阶段训练实现图像-文字数据对的模态对齐,第二阶段则注重通过 Instruction Tuning 数据集微调提升模型的问答能力

  • 通过提升图像-文字数据对的数量可以提升模型的语义对齐能力,但有可能影响模型的问答能力,因此更多工作集中于研究 Instruction Tuning 数据集的增强

  • 提出方法与 InstructPix2Pix 类似,采用 Prompt-toPrompt 技术以及生成模型 Stable-Diffusion-XL 来生成相似图像对,在生成阶段结合了多个筛选阶段来确保数据质量,强调模型关注特定区域而不是整个图像的差异

数据合成方法

利用对比学习的原理来生成 MLLM 图像-文本数据。该方法侧重于替换图像对中的对象,引导 MLLM 识别特定区域的相似性和差异

  • Step1. 创建相似图像并形成图像对,这些图像对之间的唯一区别是图中的对象
  • Step2. 提出差异区域生成器(Difference Area Generator),提取包含图像对之间的对象差异的边界框
  • Step3. 提出差异描述生成器,通过 MLLM 为具有对象差异的区域生成描述性文本,并创建问答对,例如“该区域中哪些对象发生了变化?”

过程中应用的大模型

  • Vicuna-1.5-13B 魔塔

    由 LMSYS 研发的,基于 Llama 2 微调的 Transformer 架构自回归语言模型

  • Stable-Diffusion-XL Hugging Face

    由文字生成图像的潜在空间扩散模型:1)UNet 的大小是原来的 3 倍,引入文本编码器编码器(OpenCLIP ViT bigG/14)与原始文本编码器相结合;2)引入大小和裁剪条件,以防止训练数据被丢弃,并更好地控制生成的图像应如何裁剪;3)引入两阶段模型过程,基础模型(也可以作为独立模型运行)生成图像作为优化模型的输入,该模型添加了额外的高质量细节

  • CLIP Github

    CLIP (Contrastive Language-Image Pre-Training) ,一个在图像-文字数据对上预训练的模型,可以通过给定图片判断最相关的文字片段

  • FastSAM Github

    SAM 作者提出的具有更快推理速度的分割模型

  • BLIP

    BLIP(Bootstrapping Language-Image Pre-training),一个统一视觉语言理解与生成的预训练模型,可以通过图像生成图像标注

  • LLaVA-NEXT Github

    多模态大模型

全流程梳理

流程图

模块一:创建相似数据对

Image Pairs Generation
  1. 从 MS COCO 获取 118K 图像描述
  2. 使用 Vicuna-1.5-13B 替换图像描述中的对象
    • 输入 Prompt:“这里有一个句子:‘INPUT’。请仅将这句话中的一个宾语替换为另一个宾语。”
    • INPUT 为原始描述,LLM 生成替换后的描
  3. 使用生成的描述文本对,利用图像生成模型 Stable-Diffusion-XL 和图像编辑技术 Prompt-to-Prompt 生成仅替换少量对象的图像对

模块二:差异区域生成器(Difference Area Generator)

该模型用于识别图像对之间对象差异的位置 由于预训练的目标检测模型有检测类别的限制,所以生成器基于分割方法和图像相似度比较识别差异位置,如下图所示

Difference Area Generator
  1. 通过 Image Similarity Filter 获得相似度高但不完全相同的图像对
  2. 使用 FastSAM 分割每张图像
  3. 根据分割获得的边界框信息裁剪图像,并使用 Image-text Matching Filter 判断裁剪后的子图像是否存在有效对象
  4. 使用 Difference Detector 来确定图像对的边界框区域之间是否确实存在差异,并进行 IoU 过滤以去除重叠的边界框,最终获得有效的边界框信息

Filter1:Image Similarity Filter

根据图像相似度过滤图像对

  • 计算流程

    • 该模块首先使用 CLIP 提取图像特征,然后计算特征间的余弦相似度
    • 若余弦相似度在预设阈值内,则图像对将被视为有效
  • 应用阶段

    • 在使用 FastSAM 进行分割之前,使用该模块来确保图像对高度相似但不完全相同
    • 在 Difference Detector 阶段,根据边界框信息裁剪子图像后,使用该模块过滤子图像对并仅保留不同的子图像对

Filter2:Image-text Matching Filter

确定图像是否包含有效对象(即已替换或正在替换的对象)

  • 计算流程

    • 该模块首先使用 BLIP 提取图像特征,然后将其与对象名称的文本特征进行比较
    • 当图文匹配分数落在预设阈值内时,则认为图像包含有效对象
  • 应用阶段

    • 根据分割信息进行子图像裁剪后,使用该模块来确定子图像是否包含有效对象并获得相应的有效边界框

Filter3:Difference Detector

确定图像对的边界框区域之间是否存在差异

  • 计算流程

    • 根据边界框从图像 A 和 B 中裁剪两个子图像
    • 通过 Image Similarity Filter 过滤子图像对,仅当差异足够显著时才认为边界框有效
    • 处理完所有边界框后,使用 IoU 方法过滤重叠的边界框,仅保留差异程度较高的边界框,最终输出所有有效边界框

模块三:差异描述生成器(Difference Captions Generator)

获得有效的边界框区域后,使用 Difference Captions Generator 生成有关这些区域内容的差异描述 图像对可能包含多个差异,而单个描述无法完全捕获所有差异,因此每个描述仅关注一个图像对中的一个边界框

该模块分为两个阶段,如下图所示

Difference Captions Generator
  1. 第一阶段,模型为边界框区域生成内容描述,然后使用 Image-text Matching Filter 和 Captions Similarity Filter 筛选带内容描述的边界框
  2. 第二阶段,模型使用内容描述和用红框标注的图像生成差异描述

阶段一:对象标注和筛选

  • 对于每个图像对,首先选择图像之间相似度最低的 N 个边界框区域(本项目中 N 设置为 5)作为候选区域
  • 对于每个边界框,我们使用 MLLM LLaVA-NEXT 来描述其相应的区域
  • 第一个筛选过程,通过 Image-text Matching Filter 检查区域与描述是否对应
  • 第二个筛选过程,通过 Captions Similarity Filter 评估描述之间是否存在差异,使用 CLIP 来获取文本特征并计算它们之间的余弦相似度,当分数足够低时,可认为两个标题不同
  • 筛选完成后,获得的有效的边界框和描述将用于后续的差异描述生成

阶段二:差异描述生成

对于每个图像对的各个有效边界框生成差异描述

  • 根据边界框信息在图像中绘制两个红色框,突出显示差异以便于定位
  • 为 MLLM LLaVA-NEXT 提供边界框区域的描述,并引导模型根据内容描述和红色框生成差异描述

图算法题目集合

两个顶点的最短路径

leetcode433 最小基因变化

基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A'、'C'、'G' 和 'T' 之一。 假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。 例如,"AACCGGTT" --> "AACCGGTA" 就是一次基因变化。 另有一个基因库 bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank 中) 给你两个基因序列 start 和 end ,以及一个基因库 bank ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 。 注意:起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。

题解

BFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int minMutation(string startGene, string endGene, vector<string>& bank) {

if (bank.size() == 0) return -1;

int total_diff = 0;
vector<bool> visit(bank.size(), false);

queue<string> genes;
genes.push(startGene);

while (genes.size()) {

total_diff ++;
for (int n = genes.size(); n > 0; n --) {

string gene = genes.front();
genes.pop();
for (int i = 0; i < bank.size(); i ++) {

if (visit[i]) continue;
int count_diff = 0;
for (int j = 0; j < gene.size(); j ++) {
if (gene[j] != bank[i][j]) count_diff ++;
}
if (count_diff == 1) {
if (bank[i] == endGene) return total_diff;
genes.push(bank[i]);
visit[i] = true;
}
}
}
}

return -1;
}

leetcodeLCP35 电动车游城市

小明的电动车电量充满时可行驶距离为 cnt,每行驶 1 单位距离消耗 1 单位电量,且花费 1 单位时间。小明想选择电动车作为代步工具。地图上共有 N 个景点,景点编号为 0 ~ N-1。他将地图信息以 [城市 A 编号,城市 B 编号,两城市间距离] 格式整理在在二维数组 paths,表示城市 A、B 间存在双向通路。初始状态,电动车电量为 0。每个城市都设有充电桩,charge[i] 表示第 i 个城市每充 1 单位电量需要花费的单位时间。请返回小明最少需要花费多少单位时间从起点城市 start 抵达终点城市 end。

思路

通过 Dijkstra 方法,更新起点到各个顶点的最短花费单位时间 代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
int electricCarPlan(vector<vector<int>>& paths, int cnt, int start, int end, vector<int>& charge) {

int n = charge.size();

// 记录每条相邻边,以及边的距离
vector<vector<pair<int, int>>> edges(n);
for (auto p : paths) {
edges[p[0]].emplace_back(p[1], p[2]);
edges[p[1]].emplace_back(p[0], p[2]);
}

// 计算以不同油量到达各个端点的最短单位时间
vector<vector<int>> cost(n, vector<int>(cnt + 1, INT_MAX));
cost[start][0] = 0;

// 优先队列按照单位时间排序 (消耗时间, 节点, 油量)
priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<>> q;
q.emplace(0, start, 0);

while (q.size()) {

// 获取当前消耗时间最短的下一条路径
auto [cost1, n1, o1] = q.top(); q.pop();

// 若现有消耗时间比已有记录还大,跳过判断
if (cost1 > cost[n1][o1]) continue;

// 若已到达终点,则跳出循环
if (n1 == end) return cost1;

// 考虑不同的充电状态
if (o1 < cnt) {
int new_cost = cost1 + charge[n1];
if (new_cost < cost[n1][o1 + 1]) {
cost[n1][o1 + 1] = new_cost;
// 为节点新增不同的油量状态
q.emplace(new_cost, n1, o1 + 1);
}
}

// 遍历当前节点的相邻边
for (auto [n2, cost2] : edges[n1]) {
// 若油量不足往下走,则跳过
if (o1 < cost2) continue;
int new_cost = cost1 + cost2;
int new_o = o1 - cost2;
// 若从当前节点走到n2且油量为new_o,比已有起点到n2且油量
// 为new_o的时间短,则更新
if (new_cost < cost[n2][new_o]) {
cost[n2][new_o] = new_cost;
// 新增到达n2的时间,并减去消耗的油量
q.emplace(new_cost, n2, new_o);
}
}

}

return -1;

}

leetcode909 蛇梯棋

给你一个大小为 n x n 的整数矩阵 board ,方格按从 1 到 n2 编号,编号遵循 转行交替方式 ,从左下角开始 (即,从 board[n - 1][0] 开始)每一行交替方向。

玩家从棋盘上的方格 1 (总是在最后一行、第一列)开始出发。

每一回合,玩家需要从当前方格 curr 开始出发,按下述要求前进:

选定目标方格 next ,目标方格的编号符合范围 [curr + 1, min(curr + 6, n2)] 。 该选择模拟了掷 六面体骰子 的情景,无论棋盘大小如何,玩家最多只能有 6 个目的地。 传送玩家:如果目标方格 next 处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格 next 。 当玩家到达编号 n2 的方格时,游戏结束。 r 行 c 列的棋盘,按前述方法编号,棋盘格中可能存在 “蛇” 或 “梯子”;如果 board[r][c] != -1,那个蛇或梯子的目的地将会是 board[r][c]。编号为 1 和 n2 的方格上没有蛇或梯子。

注意,玩家在每回合的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。

举个例子,假设棋盘是 [[-1,4],[-1,3]] ,第一次移动,玩家的目标方格是 2 。那么这个玩家将会顺着梯子到达方格 3 ,但 不能 顺着方格 3 上的梯子前往方格 4 。 返回达到编号为 n2 的方格所需的最少移动次数,如果不可能,则返回 -1。

思路

将棋盘的各个格子看作图顶点,顶点之间的移动步数作为边长,可将问题转换为最短路径问题。 在使用 Dijkstra 算法时可能会遇到一些细节问题:

  1. 在建立连接路径时

    • 建立 6 步跳跃关系时,可以通过编号循环累加
    • 建立梯子和蛇的关系时,可以利用编号与坐标的对应关系,通过二维数组判断当前是否可以跳跃
  2. 不能连续跳跃,且必须跳跃

    • 在判断下一个节点时,处理跳跃关系(具体在代码中体现),这样就避免了连跳的可能性
  3. 可以不使用优先队列

    • 由于每次遍历时,每条路径的增长均为 1,所以不存在更短的路径

leetcode 公交路线

给你一个数组 routes ,表示一系列公交线路,其中每个 routes[i] 表示一条公交线路,第 i 辆公交车将会在上面循环行驶。

例如,路线 routes[0] = [1, 5, 7] 表示第 0 辆公交车会一直按序列 1 -> 5 -> 7 -> 1 -> 5 -> 7 -> 1 -> ... 这样的车站路线行驶。 现在从 source 车站出发(初始时不在公交车上),要前往 target 车站。 期间仅可乘坐公交车。

求出 最少乘坐的公交车数量 。如果不可能到达终点车站,返回 -1 。

抽象路线为图节点

因为只需要统计路线数,所以不需要考虑经过节点数量 因此,可以将路线作为节点,有重合站点的路线建立边,再统计起始线路到达目标线路的经过路线数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {
// 若起点和重点一致则无需坐公交
if (source == target) return 0;
int n = routes.size();
unordered_map<int, set<int>> stationRoute; // 记录站点对应的线路
queue<int> iterRoutes; // 遍历各条路线
vector<int> dis(n, 10e7); // 记录到达该线路的最短经过线路数
for (int i = 0; i < n; i ++) {
auto route = routes[i];
bool find = false;
for (auto station : route) {
// 记录车站对应的线路
stationRoute[station].insert(i);
if (station == source) {
find = true;
}
}
if (find) {
// 记录起始点所在线路并初始化经过线路数
iterRoutes.emplace(i);
dis[i] = 1;
}
}
vector<int> visited(n, false);
while (iterRoutes.size()) {
// 取当前的最近线路
int curRoute = iterRoutes.front(); iterRoutes.pop();
// 判断是否已计算过该线路
if (visited[curRoute]) continue;
visited[curRoute] = true;
// 遍历线路中的各个站点,寻找可能的下一条线路
for (auto station : routes[curRoute]) {
// 若重点就在本线路,则返回线路数
if (station == target) return dis[curRoute];
// 遍历站点对应的多条线路
for (auto route : stationRoute[station]) {
if (visited[route]) continue;
// 更新经过线路的最短距离
if (dis[curRoute] + 1 < dis[route]) {
dis[route] = dis[curRoute] + 1;
iterRoutes.emplace(route);
}
}
}
}
return -1;
}

判断图中节点可达

leetcode399 除法求值

给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。 另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。 返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。 注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。 注意:未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
vector<double> result;
map<string, int> nodes;
for (auto eq : equations) {
string a = eq[0], b = eq[1];
if (! nodes.count(a)) {
nodes[a] = nodes.size();
}
if (! nodes.count(b)) {
nodes[b] = nodes.size();
}
}
int nodeNum = nodes.size();
vector<vector<double>> connect(nodeNum, vector<double>(nodeNum, -1.0));
// 构建自身算式连接
for (int i = 0; i < nodeNum; i ++) {
connect[i][i] = 1.0;
}
// 构建已有算式连接
for (int i = 0, len = equations.size(); i < len; i ++) {
auto eq = equations[i];
double val = values[i];
string a = eq[0], b = eq[1];
int ia = nodes[a], ib = nodes[b];
connect[ia][ib] = val;
connect[ib][ia] = 1.0 / val;
}
// 构建可达连接
for (int k = 0; k < nodeNum; k ++) {
for (int i = 0; i < nodeNum; i ++) {
for (int j = 0; j < nodeNum; j ++) {
// 已有连接不重复赋值(在此题背景下不同路径结果相同)
if (connect[i][j] == -1 && connect[i][k] != -1 && connect[k][j] != -1) {
connect[i][j] = connect[i][k] * connect[k][j];
}
}
}
}
// 值查询
for (auto query : queries) {
string a = query[0], b = query[1];
int ia = nodes.find(a) != nodes.end() ? nodes[a] : -1;
int ib = nodes.find(b) != nodes.end() ? nodes[b] : -1;
if (ia != -1 && ib != -1) {
result.push_back(connect[ia][ib]);
}
else {
result.push_back(-1);
}
}
return result;
}

判断有向图存在环路

leetcode207 课程表

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> edges(numCourses);
vector<int> father(numCourses, 0);
// 通过二维向量构建有向图
for (int i = 0, len = prerequisites.size(); i < len; i ++) {
int a = prerequisites[i][0];
int b = prerequisites[i][1];
edges[a].push_back(b);
father[b] ++;
}
// 初始化BFS队列
queue<int> learn;
for (int i = 0; i < numCourses; i ++) {
if (! father[i]) learn.push(i);
}
// 计算遍历图的节点访问次数
int count = 0;
while (learn.size()) {
count ++;
int node = learn.front(); learn.pop();
for (auto edge : edges[node]) {
// 保证被访问节点只访问一次
if (-- father[edge] == 0) {
learn.push(edge);
}
}
}
// 判断节点访问次数与节点数是否相同
return count == numCourses;
}

二分法算法题目集合

leetcode300 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

题解

常规做法是通过一维数组存储当前位置作为结尾的最大子序列长度。 而根据推理可知,若在从左向右遍历时,当前元素不能与已有元素构成更长的子序列,那么替换更小的元素将有更大的可能性构成更长子序列。

1
2
3
4
5
6
7
8
9
10
11
int lengthOfLIS(vector<int>& nums) {
auto end = nums.begin();
for (int x : nums) {
auto it = lower_bound(nums.begin(), end, x);
*it = x;
if (it == end) {
end ++;
}
}
return end - nums.begin();
}

排序序列找区间或值

leetcode34 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target,返回 [-1, -1]。 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

思路

  1. 直接定位,向两个方向找到区间边缘
  2. 定位左边界,再定位右边界

leetcode74 搜索二维矩阵

给你一个满足下述两条属性的 m x n 整数矩阵: 每行中的整数从左到右按非严格递增顺序排列。 每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。

思路

  • 二分定位行
  • 二分定位元素

堆题目集合

最小堆

排列组合 TopK 问题

优先队列 priority_queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <queue>
using namespace std;

int main()
{
priority_queue<int,vector<int>,less<int>> p1; //默认大根堆
p1.push(2); p1.push(1); p1.push(3); //输出 3 2 1
while(p1.empty()== false){
cout<<p1.top()<<" ";
p1.pop();
}

priority_queue<int,vector<int>,greater<int>> p2; //小根堆
p2.push(2); p2.push(1); p2.push(3); //输出 1 2 3
while(p2.empty()== false){
cout<<p2.top()<<" ";
p2.pop();
}

// 基本数据类型-pair-pair类型默认先比较第一个元素,第一个相等比较第二个
priority_queue<pair<int, int> > a;
a.push(pair<int,int>{1,2});
a.push(pair<int,int>{1,3});
a.push(pair<int,int>{2,5});
while (a.empty()== false){
cout<<a.top().first<<" "<< a.top().second<<endl;
a.pop();
}
//输出:
//2 5
//1 3
//1 2

return 0;
}

多路归并

每次迭代有 N 个选择,将 N 个选择放入堆得到排序后的值

  • 题型
    • 一维
      • 根据题意队列初始化 1 个值
    • 二维
      • 根据题意队列按序初始化 1 个列表
      • 初始化左上角值,set 判断是否重复插入
  • 注意去重的问题

leetcode264 丑数 II(一维)

给你一个整数 n ,请你找出并返回第 n 个 丑数 。 丑数 就是质因子只包含 2、3 和 5 的正整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int nthUglyNumber(int n) {
long times_num[] = {2, 3, 5};
set<long> record;
priority_queue<long, vector<long>, greater<long>> q;
q.push(1);
int index = 1;
while (index <= n) {
int cur_num = q.top();
if (index == n) return cur_num;
q.pop();
for (long t : times_num) {
long new_num = cur_num * t;
if (record.contains(new_num)) continue;
q.push(new_num);
record.insert(new_num);
}
index ++;
}
return 0;
}

leetcode373 查找和最小的 K 对数字(二维)

给定两个以 非递减顺序排列 的整数数组 , 以及一个整数 。 定义一对值 ,其中第一个元素来自 ,第二个元素来自 。 请找到和最小的 k 个数对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {

vector<vector<int>> result;
int l1 = nums1.size(), l2 = nums2.size();

auto cmp = [&](const auto& a, const auto& b) {
return nums1[a.first] + nums2[a.second] > nums1[b.first] + nums2[b.second];
};
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> q(cmp);
for (int i = 0; i < l1; i ++) {
q.push({i, 0});
}

while (result.size() < k) {
auto [ia, ib] = q.top();
result.push_back({nums1[ia], nums2[ib]});
q.pop();
if (ib + 1 < l2) q.push(pair<int, int>{ia, ib + 1});
}

return result;
}

字符串题目集合

字符串输入流

  • 库 sstream
  • 类 stringstream

leetcode58 最后一个单词的长度

给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。

1
2
3
4
5
6
int lengthOfLastWord(string s) {
stringstream ss(s);
string word;
while (ss >> word) ;
return word.size();
}

KMP 算法

在匹配之前需要计算匹配字符串各个位置的最长重合前后缀。初始化第一个位置为 0,后缀末尾位置从 1 开始,若当前前缀的末尾与后缀末尾相同,则更新长度;否则根据当前前缀寻找新的重合前缀,这个阶段是最难理解的部分,如下图所示。

kmp

leetcode28 找出字符串中第一个匹配项的下标

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int strStr(string haystack, string needle) {
// next数组表示到该位置的字串的最大相同前后缀的长度
vector<int> next(needle.size());
int j = 0;
next[0] = 0;
for (int i = 1, len = needle.size(); i < len; i ++) {
// 末尾不相同则查找以当前前缀的最大重复长度
while (j && needle[i] != needle[j]) j = next[j - 1];
// 末尾相同则记录长度
if (needle[i] == needle[j]) j ++;
next[i] = j;
}
// 通过回溯重合位置减少遍历次数
for (int i = 0, j = 0, len = haystack.size(); i < len; i ++) {
// 若当前位置不匹配则变换比对位置
while (j && haystack[i] != needle[j]) j = next[j - 1];
// 若比对成功则向后比对
if (haystack[i] == needle[j]) j ++;
// 判断是否已完成比对
if (j == needle.size()) return i - needle.size() + 1;
}
return -1;
}

leetcode767 重构字符串

返回 s 的任意可能的重新排列。若不可行,返回空字符串 "" 。

题解

参考题解

leetcode228 汇总区间

给定一个 无重复元素 的 有序 整数数组 nums 。 返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums 的数字 x 。 列表中的每个区间范围 [a,b] 应该按如下格式输出: "a->b" ,如果 a != b "a" ,如果 a == b

思路

参考leetcode 题解

  • to_string 方法快速将数字转换为字符串
  • 使用 i 和 j 表示区间的端点
  • 在遍历的过程中,若位置 j 与位置 j+1 的值相差 1,则向右拓展区间,从而找到完整区间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<string> summaryRanges(vector<int>& nums) {
vector<string> ans;
// Lambda 函数构建字符串
auto f = [&](int i, int j) {
return i == j ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[j]);
};
// i 在迭代时为上一个区间结尾 + 1
for (int i = 0, j, n = nums.size(); i < n; i = j + 1) {
j = i;
// 若差为 1,右端点移动
while (j + 1 < n && nums[j + 1] == nums[j] + 1) {
++ j;
}
// 添加区间
ans.emplace_back(f(i, j));
}
return ans;
}
};

leetcode820 单词的压缩编码

单词数组 words 的 有效编码 由任意助记字符串 s 和下标数组 indices 组成,且满足: words.length == indices.length 助记字符串 s 以 '#' 字符结尾 对于每个下标 indices[i] ,s 的一个从 indices[i] 开始、到下一个 '#' 字符结束(但不包括 '#')的 子字符串 恰好与 words[i] 相等 给你一个单词数组 words ,返回成功对 words 进行编码的最小助记字符串 s 的长度 。

Trie快速判断末尾重合字串

若字符串s1完全与字符串s2的末尾部分或整体重合,则字符串s1可作为同一部分助记字符串 因此,可以使用Trie构建字符串树 为了正确计算包含关系,需要优先按照字符串长度倒序排序字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution {
private:
// 使用长度为26的数组存储下一个字符
struct Node {
vector<Node*> next;
};
public:
int minimumLengthEncoding(vector<string>& words) {
int n = words.size();
// 初始化根节点
Node* root = new Node();
root->next.resize(26, nullptr);
Node* p = nullptr;
// 按字符串长度排序
sort(words.begin(), words.end(), [&](const string& a, const string& b) {
return a.size() > b.size();
});
int count = 0;
// 遍历各个字符串
for (int i = 0; i < n; i ++) {
bool include = true;
string& w = words[i];
p = root;
// 从字符串尾部开始遍历
for (int j = w.size() - 1; j >= 0; j --) {
int charIndex = w[j] - 'a';
// 若为非空指针则表示已有该字符序列
if (p->next[charIndex]) {
p = p->next[charIndex];
}
// 创建新的字符
else {
include = false; // 标识为新字符串
Node* newNode = new Node();
newNode->next.resize(26, nullptr);
p->next[charIndex] = newNode;
p = newNode;
}
}
// 若为新字符串则加上当前字符串长度以及"#"的长度
if (! include) count += w.size() + 1;
}
return count;
}
};

链表题目集合

leetcode86 分隔链表

思路

  • 遍历链表,分两个链表存储

leetcode23 合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表。

多路归并选择节点

滑动窗口题目集合

leetcode209 长度最小的子数组

题目

给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的子数组[, , ..., , ] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

题解

确定左值,向右扩展直到满足 target,记录当前窗口大小,移动左值,重复上述动作,直到右值不能再拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int size = nums.size();
int left = 0;
int right = 0;
int sum = nums[0];
int minLen = size + 1;
while (left < size && right < size) {
if (sum >= target) {
minLen = min(minLen, right - left + 1);
sum -= nums[left];
left ++;
continue;
}
if (right == size - 1) break;
right ++;
sum += nums[right];
}
if (minLen == size + 1) return 0;
return minLen;
}

};

p