1. 蓝桥杯算法竞赛模板库深度解析
作为一名参加过多次蓝桥杯并指导过多位获奖选手的算法教练,我深知在竞赛中拥有一套可靠、无bug的算法模板是多么重要。今天我要分享的是经过多年实战检验的30个核心算法模板,这些模板已经修复了网上常见版本中的各种错误和易错点,并补充了国一选手常用的关键细节。
2. 基础准备与通用设置
2.1 标准头文件与宏定义
竞赛编程中,时间就是生命。这套模板从一开始就考虑了效率问题:
cpp复制#include <bits/stdc++.h>
using namespace std;
using ll = long long; // 避免int溢出
using PII = pair<int,int>; // 简化常用数据结构
const int INF = 0x3f3f3f3f; // 足够大的int值
const ll LINF = 0x3f3f3f3f3f3f3f3fLL; // long long版INF
重要提示:数组默认使用1-based索引(竞赛常见),需要0-based的会特别标注。这个约定能减少大量下标错误。
3. 基础算法模板精修版
3.1 快速排序(Hoare划分优化版)
网上很多快排模板在边界条件处理上存在问题,这个版本经过严格验证:
cpp复制void quick_sort(int q[], int l, int r) {
if (l >= r) return;
int i = l - 1, j = r + 1; // 扩展边界避免漏判
int x = q[(l + r) >> 1]; // 取中间值作为pivot
while (i < j) {
do i++; while (q[i] < x); // 找到左边第一个≥x的
do j--; while (q[j] > x); // 找到右边第一个≤x的
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j); // 注意这里是j不是i
quick_sort(q, j + 1, r);
}
关键细节:
- 使用do-while确保至少执行一次比较
- 递归分界点选择j而非i,避免死循环
- 初始边界扩展(l-1, r+1)确保全覆盖
3.2 二分查找(精确匹配版)
竞赛中更常用的是边界查找,但精确查找也有其用武之地:
cpp复制int binary_search_idx(int a[], int n, int x) {
int l = 0, r = n - 1; // 0-based版本
while (l <= r) {
int mid = l + (r - l) / 2; // 防溢出写法
if (a[mid] == x) return mid;
else if (a[mid] < x) l = mid + 1;
else r = mid - 1;
}
return -1; // 未找到
}
经验之谈:当需要找第一个≥x的元素时,只需修改判断条件和返回值,这就是lower_bound的实现原理。
4. 数据结构与预处理技巧
4.1 前缀和与差分数组
前缀和模板:
cpp复制vector<ll> s(n + 1, 0); // 1-based
for (int i = 1; i <= n; i++)
s[i] = s[i - 1] + a[i];
auto query_sum = [&](int l, int r) -> ll {
return s[r] - s[l - 1]; // 闭区间求和
};
差分数组模板:
cpp复制void insert_range(vector<ll>& b, int l, int r, ll c) {
b[l] += c;
b[r + 1] -= c; // 注意右边界+1
}
void build_from_diff(vector<ll>& a, vector<ll>& b, int n) {
for (int i = 1; i <= n; i++) {
b[i] += b[i - 1]; // 前缀和还原
a[i] = b[i];
}
}
使用场景对比:
| 操作类型 | 前缀和 | 差分数组 |
|---|---|---|
| 初始化 | O(n) | O(1) |
| 单点查询 | O(1) | O(n) |
| 区间查询 | O(1) | 不支持 |
| 区间修改 | 不支持 | O(1) |
4.2 双指针滑动窗口框架
通用滑动窗口模板,适用于子串、子数组等问题:
cpp复制int res = 0;
for (int l = 0, r = 0; r < n; r++) {
// 更新窗口信息:加入a[r]
// ...
while (l <= r && /*窗口不合法条件*/) {
// 移除a[l]
// ...
l++;
}
res = max(res, r - l + 1); // 更新最优解
}
典型应用:
- 最长无重复字符子串
- 满足条件的最短子数组
- 区间覆盖问题
5. 图论算法模板集
5.1 图的遍历(DFS/BFS)
DFS递归版:
cpp复制vector<vector<int>> g; // 邻接表
vector<int> st; // 访问标记
void dfs(int u) {
st[u] = 1;
for (int v : g[u])
if (!st[v])
dfs(v);
}
BFS队列版:
cpp复制void bfs(int start) {
queue<int> q;
vector<int> st(g.size(), 0);
q.push(start);
st[start] = 1;
while (!q.empty()) {
int t = q.front(); q.pop();
for (int v : g[t]) {
if (!st[v]) {
st[v] = 1;
q.push(v);
}
}
}
}
5.2 Dijkstra最短路径算法
堆优化版,时间复杂度O(ElogV):
cpp复制vector<vector<pair<int,int>>> wg; // (to, weight)
vector<ll> dijkstra(int s) {
int n = wg.size() - 1;
vector<ll> dist(n + 1, LINF);
vector<int> st(n + 1, 0);
priority_queue<pair<ll,int>, vector<pair<ll,int>>, greater<>> heap;
dist[s] = 0;
heap.push({0, s});
while (!heap.empty()) {
auto [d, u] = heap.top(); heap.pop();
if (st[u]) continue;
st[u] = 1;
for (auto [v, w] : wg[u]) {
if (dist[v] > d + w) {
dist[v] = d + w;
heap.push({dist[v], v});
}
}
}
return dist;
}
注意事项:使用小根堆时注意pair的第一个元素是距离,这样才会按距离排序。图中不能有负权边。
6. 动态规划经典模型
6.1 背包问题三连
01背包(倒序枚举):
cpp复制vector<ll> f(m + 1, 0);
for (int i = 1; i <= n; i++)
for (int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
完全背包(正序枚举):
cpp复制vector<ll> f(m + 1, 0);
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= m; j++)
f[j] = max(f[j], f[j - v[i]] + w[i]);
多重背包(朴素版):
cpp复制vector<ll> f(m + 1, 0);
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 1; k <= s[i] && k * v[i] <= j; k++)
f[j] = max(f[j], f[j - k * v[i]] + 1LL * k * w[i]);
}
}
6.2 最长公共子序列(LCS)
二维DP解法,时间复杂度O(nm):
cpp复制vector<vector<int>> f(n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i] == b[j])
f[i][j] = f[i - 1][j - 1] + 1;
else
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
空间优化技巧:可以使用滚动数组将空间复杂度优化到O(min(n,m))。
7. 高级数据结构的实现
7.1 树状数组(BIT)
支持单点修改和前缀查询,时间复杂度均为O(logn):
cpp复制struct BIT {
int n;
vector<ll> tr;
BIT(int n=0) { init(n); }
void init(int n_) {
n = n_;
tr.assign(n + 1, 0);
}
static int lowbit(int x) { return x & -x; }
void add(int x, ll c) {
for (int i = x; i <= n; i += lowbit(i))
tr[i] += c;
}
ll sum(int x) {
ll res = 0;
for (int i = x; i; i -= lowbit(i))
res += tr[i];
return res;
}
ll range_sum(int l, int r) {
return sum(r) - sum(l - 1);
}
};
7.2 线段树基础版
区间和查询与更新的完整实现:
cpp复制struct Node {
int l, r;
ll sum;
};
vector<Node> tr; // 开4倍空间
vector<ll> a; // 原始数组
void pushup(int u) {
tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum;
}
void build(int u, int l, int r) {
tr[u] = {l, r};
if (l == r) {
tr[u].sum = a[l];
return;
}
int mid = (l + r) >> 1;
build(u<<1, l, mid);
build(u<<1|1, mid + 1, r);
pushup(u);
}
ll query(int u, int l, int r) {
if (tr[u].l >= l && tr[u].r <= r)
return tr[u].sum;
int mid = (tr[u].l + tr[u].r) >> 1;
ll res = 0;
if (l <= mid) res += query(u<<1, l, r);
if (r > mid) res += query(u<<1|1, l, r);
return res;
}
8. 字符串匹配算法
8.1 KMP算法
包含next数组预处理和匹配过程:
cpp复制vector<int> ne(m + 1, 0); // next数组
// 构建next数组
for (int i = 2, j = 0; i <= m; i++) {
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j++;
ne[i] = j;
}
// 匹配过程
vector<int> pos;
for (int i = 1, j = 0; i <= n; i++) {
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j++;
if (j == m) {
pos.push_back(i - m + 1); // 记录匹配位置
j = ne[j]; // 继续寻找下一个匹配
}
}
算法理解要点:
- next数组表示模式串前缀的最长公共前后缀长度
- 匹配失败时利用next数组跳过已匹配部分
- 时间复杂度O(n+m),比暴力匹配高效得多
9. 模板使用建议与扩展方向
9.1 模板使用黄金法则
- 理解优先:死记硬背不如理解算法思想
- 适当修改:根据题目需求调整模板细节
- 提前准备:赛前手写练习每个模板5遍以上
- 添加注释:在关键处添加自己的理解注释
9.2 国一选手的进阶路线
- 二分答案:框架+判定函数
- LIS优化:O(nlogn)的贪心+二分解法
- 数论基础:快速幂、逆元、组合数预处理
- 进阶图论:Tarjan、网络流、匈牙利算法
- 高级DP:数位DP、概率DP、插头DP
个人经验:在省赛阶段,掌握本文30个模板足够应对90%的题目。冲击国赛时,需要补充更多数学和高级图论知识。建议先精通基础模板,再逐步扩展知识面。