截至 0412(最短路 II)。学到哪写到哪 —— 这张表回答一个问题:看到一道题,我该抓哪个工具?每个工具给「什么时候用、多快、代码骨架、踩过的坑」,细节点回对应课件。
| 算法 | 解决什么 | 复杂度 | 一句话记住它 | 课件 |
|---|---|---|---|---|
| BFS | 无权图的最短路、层序遍历 | O(n + m) | 一圈一圈往外扩 | 0408 |
| DFS | 连通块、路径枚举、回溯 | O(n + m) | 一条道走到黑,走不动再退 | 0408 |
| 拓扑排序 | 有依赖的任务排序、判 DAG | O(n + m) | 先把没前置的点拿出来 | 0409 |
| 并查集 | 连通性、合并集合、Kruskal 的前半段 | 近 O(1) | 只关心「是不是一家」 | 0410 |
| Dijkstra(堆) | 单源最短路,边权非负 | O(m log m) | 每轮挑最近的盖章 | 0411 |
| Bellman-Ford | 单源最短路 + 负环,边权可为负 | O(n·m) | 不盖章,全体松弛 n−1 轮 | 0412 |
| SPFA | 同上,但平均更快(可能被卡) | 最坏 O(n·m) | Bellman-Ford 加个队列 | 0412 |
| Floyd | 任意两点最短路,n 小 | O(n³) | 五行,k 必须最外层 | 0412 |
| 归并 / 快排 | 排序(要稳定选归并) | O(n log n) | 分两半,各自排好再合 | 0407 |
| 堆(优先队列) | 反复取最小值、Top-K | 插入/弹出 O(log n) | 完全二叉树 + 数组存 | 0406 |
| 二分答案 | 答案有单调性(0414) | O(log 值域) | 先想「能不能只判对错」 | 0414 待学 |
| 前缀和 / 差分 | 区间求和、区间加(0415) | O(1) 查询 | 预处理一次,后面白拿 | 0415 待学 |
| 结构 | 关键操作 | 什么时候选它 | 课件 |
|---|---|---|---|
数组 / vector | 随机访问 O(1) | 元素个数确定、按下标访问最多 | 0401 |
| 链表 | 插入删除 O(1)(已拿到位置) | 要频繁在中间插删,且顺序重要 | 0402 |
栈 stack | push / pop / top | 括号匹配、逆序、单调栈 | 0011 |
队列 queue | push / pop / front | BFS、按顺序处理任务 | 0011 |
堆 priority_queue | 取最小/最大 O(log n) | 只关心极值,不关心全部顺序 | 0406 |
| 二叉搜索树 | 查/插/删 均摊 O(log n) | 要同时「查找 + 有序遍历」 | 0405 |
| 并查集 | 合并 + 查同根,近 O(1) | 只问「是不是一家」 | 0410 |
map / set | 插入/查找 O(log n) | 要键值对、要自动去重排序 | STL 速查 |
| 存法 | 内存 | 适合 | 缺点 |
|---|---|---|---|
邻接矩阵 g[u][v] | O(n²) | n ≤ 500、稠密图、Floyd | n = 105 直接 40 GB |
邻接表 vector<vector<...>> | O(n + m) | 默认就选它 | 判两点是否相邻要扫 |
边数组 vector<Edge> | O(m) | Bellman-Ford、Kruskal | 不能直接查邻居 |
// 有权图的邻接表(最常用)
vector<vector<pair<int,int>>> g(n + 1); // g[u] = {v, w}
g[u].push_back({v, w});
for (auto [v, w] : g[u]) { /* 遍历 u 的出边 */ }
注意 P3385 那类题:「w ≥ 0 表示双向边,w < 0 只有单向」,一条输入可能要 push 两次。
两者都是 O(n + m),区别只在「先扩一圈」还是「先钻到底」。
| BFS(队列) | DFS(递归/栈) | |
|---|---|---|
| 能求 | 无权图最短步数、层序 | 连通块、路径方案、回溯 |
| 空间 | 最宽一层 O(n) | 最深一条链 O(n)(可能爆栈) |
| 标记时机 | 入队时就标记 | 进函数就标记 |
// BFS:d[v] = 起点到 v 的最少边数
void bfs(int s, const vector<vector<int>>& g, vector<int>& d) {
queue<int> q;
d[s] = 0; q.push(s);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : g[u])
if (d[v] == -1) { // 没访问过才进来
d[v] = d[u] + 1;
q.push(v); // 入队前就标记(d[v] 已经不等于 -1)
}
}
}
// DFS:标记连通块
void dfs(int u, const vector<vector<int>>& g, vector<bool>& vis) {
vis[u] = true;
for (int v : g[u])
if (!vis[v]) dfs(v, g, vis);
}
实测(图:1→2、1→3、2→4、3→4,起点 1):d1=0 d2=1 d3=1 d4=2,DFS 访问 4 个点。
什么时候能用:有向图、没有环(DAG)。典型题面是「先修课」「任务依赖」「谁必须在谁前面」。
// Kahn 算法:反复取出入度为 0 的点
vector<int> topoSort(int n, const vector<vector<int>>& g) {
vector<int> deg(n + 1, 0), res;
for (int u = 1; u <= n; u++)
for (int v : g[u]) deg[v]++; // 统计入度
queue<int> q;
for (int i = 1; i <= n; i++) if (deg[i] == 0) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
res.push_back(u);
for (int v : g[u])
if (--deg[v] == 0) q.push(v); // 前置都做完了,可以入队
}
return res;
}
顺手判环:如果 res.size() != n,说明剩下的点互相依赖 —— 图里有环。
实测(同上那张图):输出 1 2 3 4。
什么时候用:题目只问「两个点在不在同一个连通块」「把两个集合并起来」,从不问你路径长什么样。加了路径压缩之后,单次操作几乎 O(1)。
int findRoot(vector<int>& fa, int x) {
return fa[x] == x ? x : fa[x] = findRoot(fa, fa[x]); // 路径压缩
}
void unite(vector<int>& fa, int a, int b) {
a = findRoot(fa, a); b = findRoot(fa, b);
if (a != b) fa[a] = b;
}
// 初始化:每个点自成一个集合
vector<int> fa(n + 1);
iota(fa.begin(), fa.end(), 0); // fa[i] = i
两种优化别混:路径压缩(把链拍平)和按秩/按大小合并(小的挂到大的下面)。后者写错不会错答案,只会慢 —— 0410 那题里很多人写成「比较根的编号」,就是这个坑。
实测:unite(1,2)、unite(2,3) 之后,1 和 3 同根、1 和 4 不同根。
| 算法 | 边权 | 复杂度 | 能判负环 | 什么时候选 |
|---|---|---|---|---|
| Dijkstra(堆) | 必须 ≥ 0 | O(m log m) | 不能 | 默认选它 |
| Bellman-Ford | 可负 | O(n·m) | 能 | 有负权、n·m 不大 |
| SPFA | 可负 | 最坏 O(n·m) | 能 | 想赌它快(会被卡) |
| Floyd | 可负 | O(n³) | 能(f[i][i]<0) | 任意两点、n ≤ 500 |
// Dijkstra:堆里存 {距离, 点},小的先出
vector<long long> dijkstra(int n, const vector<vector<pair<int,int>>>& g, int s) {
const long long INF = 4e18;
vector<long long> dis(n + 1, INF);
priority_queue<pair<long long,int>, vector<pair<long long,int>>, greater<>> pq;
dis[s] = 0; pq.push({0, s});
while (!pq.empty()) {
auto [du, u] = pq.top(); pq.pop();
if (du > dis[u]) continue; // 懒删除:这是旧快照
for (auto [v, w] : g[u])
if (dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
pq.push({dis[v], v}); // 只压入,不修改堆里的旧值
}
}
return dis;
}
// Bellman-Ford:不盖章,全体边松弛 n-1 轮;第 n 轮还能改 → 负环
bool bellmanFord(int n, const vector<Edge>& edges, int s, vector<long long>& dis) {
const long long INF = 4e18;
dis.assign(n + 1, INF);
dis[s] = 0;
for (int round = 1; round <= n - 1; round++) {
bool changed = false;
for (auto& e : edges) {
if (dis[e.u] == INF) continue; // 不可达的点不能拿去加
if (dis[e.u] + e.w < dis[e.v]) { dis[e.v] = dis[e.u] + e.w; changed = true; }
}
if (!changed) break; // 整轮没变化就收工
}
for (auto& e : edges)
if (dis[e.u] != INF && dis[e.u] + e.w < dis[e.v]) return false; // 有负环
return true;
}
// SPFA:Bellman-Ford 的队列版,用「路径边数 ≥ n」判负环
bool spfa(int n, const vector<vector<pair<int,int>>>& g, int s, vector<long long>& dis) {
const long long INF = 4e18;
dis.assign(n + 1, INF);
vector<int> cnt(n + 1, 0);
vector<bool> inq(n + 1, false);
deque<int> q;
dis[s] = 0; q.push_back(s); inq[s] = true;
while (!q.empty()) {
int u = q.front(); q.pop_front(); inq[u] = false;
for (auto [v, w] : g[u])
if (dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) return false; // 绕了 n 条边还在变短 → 负环
if (!inq[v]) { q.push_back(v); inq[v] = true; }
}
}
return true;
}
// Floyd:k 是最外层(DP 的阶段),不能换位置
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++) {
if (f[i][k] == INF) continue; // 省掉一半加法,也防溢出
for (int j = 1; j <= n; j++)
if (f[i][k] + f[k][j] < f[i][j]) f[i][j] = f[i][k] + f[k][j];
}
实测(1→2=2、2→3=2、2→4=1、1→3=5、3→4=3、1→4=4,从 1 出发):四种算法给出的距离都是 0 2 4 3,d(1,4)=3。
| 算法 | 平均 | 最坏 | 稳定 | 什么时候自己写 |
|---|---|---|---|---|
| 冒泡 | O(n²) | O(n²) | 是 | 只在 n 很小、或教学演示 |
| 选择 | O(n²) | O(n²) | 否 | 交换次数要最少时 |
| 插入 | O(n²) | O(n²) | 是 | 数据几乎有序、或 n 很小 |
| 归并 | O(n log n) | O(n log n) | 是 | 要稳定 / 顺手数逆序对 |
| 快排 | O(n log n) | O(n²) | 否 | 平时不用,sort 就是它 |
std::sort | O(n log n) | O(n log n) | 否 | 默认就该用它 |
sort(a.begin(), a.end()); // 升序
sort(a.begin(), a.end(), greater<int>()); // 降序
// 多关键字:先按第一个降序,再按第二个升序
sort(a.begin(), a.end(), [](const P& x, const P& y) {
if (x.first != y.first) return x.first > y.first;
return x.second < y.second;
});
// 要稳定排序用 stable_sort(相等元素保持原来的相对顺序)
优先队列是「堆」不是「排序」:只保证队首是极值,其余元素内部无序。要全部有序还是得 sort。
while (l < r) { mid; if (ok(mid)) r = mid; else l = mid + 1; }dis[v] = min(dis[v], dis[u] + w))。INT_MAX + INT_MAX 变负数,比任何距离都小 → 用 0x3f3f3f3f 或加 >= INF 判断。dis / vis / 邻接表每组重建,P3385 是典型。ios::sync_with_stdio(false); cin.tie(nullptr); —— 实测 10 万点读入从 351.9 ms 降到 33.9 ms。if (du > dis[u]) continue;,不然一堆过期快照照样往外松弛。k 没放最外层:换位置能过样例,真出错(实测给出 10,真值 3)。fa[i] = i,忘了就是全域乱连。res.size() != n 说明有环,别把残缺顺序当答案输出。| 复杂度 | 1 秒大约能处理 | 典型场景 |
|---|---|---|
| O(n) | n ≈ 108 | 扫一遍、前缀和 |
| O(n log n) | n ≈ 106 – 107 | sort、堆 Dijkstra |
| O(n²) | n ≈ 5000 | 朴素最短路、Floyd 以外的小 n |
| O(n³) | n ≈ 300 – 500 | Floyd |
| O(2n) | n ≤ 20 – 25 | 状压、子集枚举 |
这张表是经验值,常数因子差很多:同样的 O(n log n),手写堆和 std::sort 能差几倍。看题目限制时留一倍余量。
g++ -O2 -std=c++17 -Wall -Wextra 编译(零警告),并跑出文中那几组实测输出(BFS 的 0 1 1 2、拓扑的 1 2 3 4、四种最短路一致的 0 2 4 3)。想补哪个算法、或者哪一段看不懂,直接问。
对应课件:0408 图的遍历 · 0409 连通性与拓扑排序 · 0410 并查集 · 0411 最短路 I · 0412 最短路 II · STL 速查 · 刷题清单 · 返回资源列表