知识公社 | 课程 · 算法竞赛 8–13 讲
ALGORITHM CONTEST · PART 2

算法竞赛 · 从零到精通(第 8–13 讲)

BFS · 贪心 · 动态规划入门 · 图论入门 · 并查集与最小生成树 · 刷题路线图

第 8 讲 · 广度优先搜索(BFS)与网格图

目标:会用队列逐层扩展,解决"最短步数 / 最少操作"类问题。

BFS 与 DFS 的区别:DFS 是"一条路走到黑",BFS 是"一层一层扩"。求最短步数一律优先想 BFS。

struct Node{ int x, y, d; }; // 坐标 + 步数
int bfs(int sx, int sy, int tx, int ty) {
    queue<Node> q;
    q.push({sx, sy, 0}); vis[sx][sy] = true;
    while (!q.empty()) {
        Node u = q.front(); q.pop();
        if (u.x == tx && u.y == ty) return u.d;
        for (int k = 0; k < 4; k++) {
            int nx = u.x + dx[k], ny = u.y + dy[k];
            if (nx >= 0 && nx < n && ny >= 0 && ny < m && !vis[nx][ny]) {
                vis[nx][ny] = true;
                q.push({nx, ny, u.d + 1});
            }
        }
    }
    return -1;
}
小测 · BFS 为什么能求"最少步数"?
练习:迷宫最短路;"骑士移动"(8 个方向);0-1 BFS(边权只有 0/1 时用双端队列)。

第 9 讲 · 贪心:先拿容易的分

目标:识别"每一步取最优即可证明正确"的问题;学会两种常见套路。

两种招牌套路

// 活动安排:最多能参加几个活动
struct A{ int s, e; };
bool cmp(const A&a, const A&b){ return a.e < b.e; } // 按结束时间排序
int cnt = 0, last = -1;
for (auto &x : a)
    if (x.s >= last) { cnt++; last = x.e; }
cout << cnt << endl;

贪心最大的坑是"局部最优 ≠ 全局最优"(例如普通硬币找零)。写之前想一个反例;举不出反例再提交。

小测 · 贪心算法什么时候能用?
练习:合并果子(优先队列);"最少会议室数量"。

第 10 讲 · 动态规划入门:从斐波那契到背包

目标:会设状态、写转移方程;独立做出 0-1 背包。

DP 四步法

// 0-1 背包:容量 V,n 件物品,取或不取,价值最大
int dp[1005] = {0};
for (int i = 1; i <= n; i++)
    for (int v = V; v >= w[i]; v--)   // 逆序!保证每件只用一次
        dp[v] = max(dp[v], dp[v-w[i]] + c[i]);
cout << dp[V] << endl;
小测 · 动态规划的两个关键是什么?
练习:爬楼梯(斐波那契式);最长上升子序列(LIS);再刷 CF "dp" 标签 1200–1400 分题 5 道。

第 11 讲 · 图论入门:存图、遍历与最短路

目标:会用邻接表存图;跑通 BFS 最短路;知道 Dijkstra 的用途。
// 邻接表
vector<int> g[100005];      // g[u] 存 u 的所有邻居
g[u].push_back(v); g[v].push_back(u); // 无向边
// 带权:vector<pair<int,int>> g[]; 存 (邻居, 边权)
// Dijkstra 核心(堆优化)
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;
dis[s] = 0; pq.push({0, s});
while (!pq.empty()) {
    auto [d, u] = pq.top(); pq.pop();
    if (d > 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}); }
}
小测 · 存图最常用的两种方式是?
练习:无向图连通块计数;"单源最短路"模板题各一道。

第 12 讲 · 并查集与最小生成树

目标:背下并查集模板(超高频);理解 Kruskal 求最小生成树。
// 并查集(路径压缩)——控制"谁和谁是一伙的"
int fa[100005];
int find(int x){ return fa[x] == x ? x : fa[x] = find(fa[x]); }
void merge(int a, int b){ fa[find(a)] = find(b); }
// 初始化:for (int i = 1; i <= n; i++) fa[i] = i;
// 判环:若 find(u) == find(v) 说明 u、v 已经连通,再加边就成环

Kruskal 最小生成树(把 n 个点最少花费连起来)

  1. 把所有边按边权从小到大排序;
  2. 依次考虑每条边:两端点还不连通(并查集判断)就选它,否则跳过;
  3. 选满 n−1 条边即完成。
小测 · 并查集最擅长做什么?
练习:朋友圈合并(并查集裸题);最小生成树模板题。

第 13 讲 · 刷题路线图:接下来半年怎么练

目标:拿到一份可执行的训练计划,知道每个阶段做什么、卡住了怎么办。

卡住了怎么办(重要)

小测 · 刷题时卡住了,性价比最高的做法是?
毕业小测:连续一周每天 3 题 + 周末一场比赛复盘。坚持下来你就已经超过大多数人了。