十六届蓝桥杯C++组备赛必看:高频算法与核心知识点梳理

在这里插入图片描述
制作不易,感谢浏览。

导语:蓝桥杯省赛在即,你是否还在为算法知识点零散而焦虑?本文系统梳理C++组高频核心考点,助你快速构建算法知识体系。


一、避开那些"送分题"的坑

1.1 数据类型与极值的边界

在编程竞赛中,数据类型的边界问题常常是导致错误的主要原因之一。以下是几个关键点:

  • int型变量范围int类型的取值范围为-2^312^31-1(约±21亿)。在处理累加问题(如前缀和、动态规划)时,若数据规模较大,务必检查是否需要使用long long(范围为-2^632^63-1)。
    注意事项:累加溢出是一个常见的陷阱,尤其是在动态规划问题中,状态转移可能涉及多个大数相加。建议在不确定时直接使用long long,避免因溢出导致错误。

  • 浮点数精度:浮点数存储基于IEEE 754标准,存在精度损失问题。因此,比较两个浮点数时,不能直接用==判断,而应通过设定一个极小的阈值(如1e-6)来判断是否相等。例如:

    if (abs(a - b) < 1e-6) {
          
            
        // 视为相等  
    }  
    
  • ASCII技巧:字符与数字的转换是竞赛中的常见操作。例如,ch - '0'可以将字符'0''9'转换为对应的数字,而'A' + 3可以得到字符'D'。这种技巧常用于密码类题目或字符映射问题。

1.2 STL容器使用速查表

C++ STL提供了丰富的容器,以下是几个常用容器及其典型应用场景:

容器 常用方法 典型应用场景
vector push_back(), resize(),迭代器遍历 动态数组、邻接表存储图
string substr(), find(), replace() 字符串匹配、分割操作
map count(key), lower_bound() 数据映射、离散化处理
set insert(), erase(), find() 去重、维护有序集合
priority_queue push(), pop(), top() 哈夫曼编码、求前K大元素

详细STL知识点可以跳转这里进行系统性学习。

实战建议

  • vector是动态数组的首选,但在频繁插入和删除时可能需要手动resize()以优化性能。
  • mapset基于红黑树实现,时间复杂度为O(log n),适合需要频繁查找和排序的场景。
  • priority_queue默认为大根堆,若需小根堆,可自定义比较函数或存储负值。

1.3 C++11/14/17新特性速览 (慎用高级语法)

C++11/14/17引入了许多新特性,蓝桥杯还是老老实实用简单语法吧,不然就会像往年学长一样暴毙

  • auto关键字:自动类型推导,减少冗长的类型声明。例如:

    vector<int> v = {
          
          1, 2, 3};  
    for(auto x : v) cout << x << " ";  
    
  • lambda表达式:简化函数对象定义,常用于STL算法。例如:

    sort(v.begin(), v.end(), [](int a, int b){
          
           return a > b; });  
    
  • 智能指针unique_ptrshared_ptr管理动态内存,避免内存泄漏。例如:

    unique_ptr<int> ptr(new int(10));  
    

二、暴力算法的蜕变

蓝桥杯叫暴力杯不是没有原因的,暴力打满就可以哪个很不错的成绩(要相信水货比较多),相信自己,大胆暴力。

2.1 搜索结果与剪枝艺术

在竞赛中,暴力搜索往往无法通过大规模测试用例,因此剪枝优化是必不可少的技巧。以下是几个关键点:

  • DFS模板(以全排列为例):

    void dfs(int step) {
          
            
        if(step == n) {
          
            
            // 输出结果  
            return;  
        }  
        for(int i=1; i<=n; ++i) {
          
            
            if(!vis[i]) {
          
            
                vis[i] = 1;  
                path[step] = i;  
                dfs(step+1);  
                vis[i] = 0;  
            }  
        }  
    }  
    

    应用场景:排列组合、状态枚举、路径搜索等问题。

  • BFS关键点:队列维护状态、层次遍历求最短步数(如迷宫最短路)。
    优化技巧:使用双端队列(deque)实现双向BFS,可显著降低时间复杂度。

  • 剪枝策略

    • 可行性剪枝:当前路径不可能达成目标时终止(如八皇后问题)。
    • 最优性剪枝:当前路径已劣于已知最优解(如旅行商问题)。

2.2 动态规划(DP)的使用

动态规划是竞赛中的核心算法之一,其关键在于状态定义和转移方程的设计。以下是解题步骤:

  1. 定义状态:明确dp[i][j]的含义。
  2. 推导转移方程:如何从前一个状态转移到当前状态。
  3. 确定边界条件:初始状态如何初始化。

经典问题

  • 背包问题:01背包(逆序枚举)、完全背包(正序枚举)。
    实战技巧:通过状态压缩将二维DP优化为一维,节省空间。

  • 最长公共子序列

    dp[i][j] = (s1[i] == s2[j]) ? dp[i-1][j-1] + 1 : max(dp[i-1][j], dp[i][j-1]);  
    

    应用扩展:可结合二分法优化为O(n log n)复杂度。

  • 矩阵链乘法:区间DP的典型应用,需明确状态定义与转移顺序。
    动态规划问题一般比较难,能放掉还是放掉吧

2.3 贪心算法的使用

贪心算法的核心思想是局部最优推导全局最优,但需要严格证明其正确性。
贪心其实就是对于某一种情况只考虑对他最优的情况,从而满足对于整个答案的最优解。一般比较考验思维,但是一般不会出很难得贪心题。可以多花点时间在这上面,争取拿满分。

2.4 图论算法模板速记

Dijkstra算法

Dijkstra算法用于求单源最短路径,其核心思想是利用优先队列维护当前最短路径。以下是代码模板:

void dijkstra(int s) {
    
      
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;  
    dist[s] = 0;  
    pq.push({
    
    0, s});  
    while(!pq.empty()) {
    
      
        auto [d, u] = pq.top(); pq.pop();  
        if(vis[u]) continue;  
        vis[u] = true;  
        for(auto &[v, w] : G[u]) {
    
      
            if(dist[v] > dist[u] + w) {
    
      
                dist[v] = dist[u] + w;  
                pq.push({
    
    dist[v], v});  
            }  
        }  
    }  
}  

关键点

  • 使用优先队列维护当前最短路径,确保每次扩展的节点是最优的。
  • 对于负权边,需改用Bellman-Ford或SPFA算法。
Kruskal算法(最小生成树)

Kruskal算法通过按边权排序并使用并查集维护连通性,求解最小生成树。以下是代码模板:

int kruskal() {
    
      
    sort(edges.begin(), edges.end(), [](Edge a, Edge b) {
    
     return a.w < b.w; });  
    int res = 0, cnt = 0;  
    for(auto &e : edges) {
    
      
        int x = find(e.u), y = find(e.v);  
        if(x != y) {
    
      
            fa[x] = y;  
            res += e.w;  
            cnt++;  
            if(cnt == n-1) break;  
        }  
    }  
    return cnt == n-1 ? res : -1;  
}  
Floyd算法(多源最短路)

Floyd算法通过动态规划的思想,求解所有节点对之间的最短路径。以下是代码模板:

void floyd() {
    
      
    for(int k=1; k<=n; ++k)  
        for(int i=1; i<=n; ++i)  
            for(int j=1; j<=n; ++j)  
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);  
}  

图论知识多而难,这里只给出了一部分皮毛,主播也还在学习当中,如果省赛的时候出了那就祈祷大家都不会吧哈哈哈

2.5 分治与归并排序

分治算法的核心思想是将问题分解为子问题,分别解决后合并结果。以下是归并排序的模板:

void merge_sort(int l, int r) {
    
      
    if(l >= r) return;  
    int mid = (l + r) >> 1;  
    merge_sort(l, mid);  
    merge_sort(mid+1, r);  
    merge(l, mid, r); // 合并两个有序区间  
}  

实战应用:逆序对统计、大规模数据排序。
排序一般都是用sort直接写了,但是归并排序里面的思想很好,在求逆序对的时候有所妙用。


三、常用数学思路

3.1 数论必备公式

数论是竞赛中的重要模块,以下是几个关键算法:

  • 快速幂算法(求a^b mod p):

    ll qpow(ll a, ll b, ll p) {
          
            
        ll res = 1;  
        while(b) {
          
            
            if(b & 1) res = res * a % p;  
            a = a * a % p;  
            b >>= 1;  
        }  
        return res;  
    }  
    
  • 应用场景:大数求幂。

  • 欧拉筛法(线性时间复杂度求质数):

    vector<int> primes;  
    bool is_prime[N];  
    void euler() {
          
            
        memset(is_prime, true, sizeof is_prime);  
        for(int i=2; i<N; ++i) {
          
            
            if(is_prime[i]) primes.push_back(i);  
            for(int p : primes) {
          
            
                if(i*p >= N) break;  
                is_prime[i*p] = false;  
                if(i % p == 0) break;  
            }  
        }  
    }  
    

    优化点:通过最小质因数标记,避免重复筛除,确保每个合数仅被筛除一次。

3.2 组合数学与概率

组合数学是解决排列组合问题的核心工具:

  • 排列数P(n, k) = n!/(n-k)!
  • 组合数C(n, k) = n!/(k!*(n-k)!)

卢卡斯定理:用于大组合数取模(模数为质数)。

ll C(ll n, ll k, ll p) {
    
      
    if(k > n) return 0;  
    return fac[n] * invfac[k] % p * invfac[n-k] % p;  
}  

实战建议:预处理阶乘及其逆元,可快速计算组合数。
组合数学常用作在求组合数的题里面,实际上也是将模板记住就不是很难,难点在于你会不会用。
对于组合数大家可以看看主播的这篇博客
蓝桥杯应该不考(我猜的)

3.3 矩阵快速乘法

矩阵快速乘法是解决线性递推问题的高效方法,例如斐波那契数列的快速计算。以下是代码模板:

// 定义一个2x2矩阵结构体
struct Matrix {
    
      
    ll m[2][2];  // 矩阵的元素,使用二维数组存储

    // 重载乘法运算符,实现矩阵乘法
    Matrix operator*(const Matrix &b) const {
    
      
        Matrix c;  // 创建一个新的矩阵c,用于存储乘法结果
        // 遍历矩阵c的每个元素
        for(int i = 0; i < 2; ++i) {
    
      
            for(int j = 0; j < 2; ++j) {
    
      
                // 计算矩阵c的元素c[i][j],使用矩阵乘法公式
                // c[i][j] = (当前矩阵的第i行第0列 * b的第0行第j列) + (当前矩阵的第i行第1列 * b的第1行第j列)
                c.m[i][j] = (m[i][0] * b.m[0][j] + m[i][1] * b.m[1][j]) % MOD;  
            }
        }  
        return c;  // 返回结果矩阵c
    }  
};

// 快速幂函数,用于计算矩阵的幂次
Matrix qpow(Matrix a, ll b) {
    
      
    // 初始化结果矩阵为单位矩阵(对角线为1,其余为0)
    Matrix res = {
    
    {
    
    1, 0}, {
    
    0, 1}};  
    while(b) {
    
      
        // 如果b的当前二进制位为1,将当前矩阵a乘到结果中
        if(b & 1) res = res * a;  
        // 矩阵a自乘,相当于计算a的平方
        a = a * a;  
        // 右移b的二进制位,处理下一位
        b >>= 1;  
    }  
    return res;  // 返回最终的幂次结果
}

四、竞赛中的技巧

4.1 位运算的奇巧

位运算是竞赛中优化代码的重要工具,以下是几个关键技巧:

  • lowbit操作x & -x 快速获取二进制最后一位1。
    应用场景:二进制分组、Fenwick树(树状数组)实现。

  • 状态压缩:用整数二进制位表示状态(如n皇后问题)。
    优化技巧:通过位运算实现高效的状态转移,减少内存占用。

  • 快速交换a ^= b; b ^= a; a ^= b;
    注意事项:仅适用于整数类型,浮点数需谨慎使用。

  • 是否为2的n次幂(x & (x-1))
    原理:2的n次幂的二进制形式为1000000,减一后为111111,两者相与结果为0。

4.2 二分法

二分法是解决有序数组查找问题的经典算法,以下是寻找第一个≥target的位置的模板:

int l=0, r=n;  
while(l < r) {
    
      
    int mid = (l+r) >> 1;  
    if(a[mid] >= target) r = mid;  
    else l = mid + 1;  
}  
return l;  

大家可以手写二分好好理解一下其中的过程,手写一下还是可以更好理解二分法的妙处的,能够有效的将O(n^2)时间复杂度降为O(nlogn)的复杂度。

关键点

  • l < r循环条件确保最终收敛到唯一解。
  • mid = (l + r) >> 1避免溢出问题。
  • lower_bound() 和 upper_bound()的使用一定要熟悉,写题中非常好用。

4.3 前缀和与差分数组

前缀和和差分数组是处理区间问题的高效工具:

  • 前缀和:快速计算区间和。

    int pre_sum[100005];  
    pre_sum[0] = 0;  
    for(int i=1; i<=n; ++i) pre_sum[i] = pre_sum[i-1] + a[i];  
    int sum(int l, int r) {
          
           return pre_sum[r] - pre_sum[l-1]; }  
    
  • 差分数组:高效处理区间更新问题。

    int diff[100005];  
    void add(int l, int r, int val) {
          
            
        diff[l] += val;  
        diff[r+1] -= val;  
    }  
    for(int i=1; i<=n; ++i) diff[i] += diff[i-1]; // 恢复原数组  
    

4.4 线段树与树状数组

没时间就不要看线段树和树状数组了,看来也写不出来。

线段树和树状数组是处理区间查询和更新问题的高效数据结构:

  • 线段树:支持区间查询和单点/区间更新,适用于大规模数据。

    struct SegmentTree {
          
            
        int l, r;  
        int sum, add;  
    } tree[4*MAXN];  
    
  • 树状数组:支持单点更新和前缀和查询,适用于频繁查询和更新的场景。

    int c[MAXN];  
    int lowbit(int x) {
          
           return x & -x; }  
    void update(int x, int val) {
          
            
        while(x <= n) {
          
            
            c[x] += val;  
            x += lowbit(x);  
        }  
    }  
    int query(int x) {
          
            
        int res = 0;  
        while(x > 0) {
          
            
            res += c[x];  
            x -= lowbit(x);  
        }  
        return res;  
    }  
    

五、常考算法补充

6.1 字符串处理

KMP算法(模式匹配)

KMP算法通过预处理模式串的next数组,避免暴力匹配中的重复比较。以下是代码模板:

void kmp(string s, string p) {
    
      
    int n = s.size(), m = p.size();  // 获取主字符串和模式串的长度
    vector<int> next(m, 0);  // 创建 next 数组,用于存储模式串的前缀函数值

    // 第一部分:预处理模式串,生成 next 数组
    for (int i = 1, j = 0; i < m; ++i) {
    
      
        // j 表示当前字符之前的最长公共前后缀长度
        while (j && p[i] != p[j])  // 如果当前字符不匹配,回退到前一个位置的最长公共前后缀长度
            j = next[j - 1];  
        if (p[i] == p[j])  // 如果当前字符匹配,最长公共前后缀长度加 1
            j++;  
        next[i] = j;  // 将当前字符的最长公共前后缀长度存入 next 数组
    }  

    // 第二部分:主循环,匹配主字符串和模式串
    for (int i = 0, j = 0; i < n; ++i) {
    
      
        // j 表示当前匹配到模式串的第几个字符
        while (j && s[i] != p[j])  // 如果当前字符不匹配,回退到前一个位置的最长公共前后缀长度
            j = next[j - 1];  
        if (s[i] == p[j])  // 如果当前字符匹配,继续匹配下一个字符
            j++;  
        if (j == m) {
    
      // 如果匹配完成(j 等于模式串长度)
            cout << "Pattern found at index " << i - m + 1 << endl;  // 输出匹配的起始位置
            j = next[j - 1];  // 回退到前一个位置的最长公共前后缀长度,继续匹配
        }  
    }  
}

6.2 二分图匹配

匈牙利算法

匈牙利算法用于求解二分图的最大匹配问题。以下是代码模板:

// 邻接表表示二分图的结构
vector<int> g[MAXN];  // MAXN是图中节点的最大数量

// match数组记录每个节点的匹配情况,初始化为-1表示未匹配
// vis数组用于标记在DFS过程中访问过的节点
int match[MAXN], vis[MAXN];  

// 深度优先搜索函数,用于寻找增广路径
bool dfs(int u) {
    
      
    // 遍历所有与u相连的节点v
    for(int v : g[u]) {
    
      
        // 如果v未被访问过
        if(!vis[v]) {
    
      
            vis[v] = 1;  // 标记v为已访问
            // 如果v未匹配,或者v的匹配节点可以找到新的匹配
            if(match[v] == -1 || dfs(match[v])) {
    
      
                // 将u和v匹配
                match[v] = u;  
                return true;  // 找到增广路径,返回true
            }  
        }  
    }  
    // 未找到增广路径,返回false
    return false;  
}  

// 匈牙利算法主函数,计算二分图的最大匹配数
int hungarian() {
    
      
    // 初始化match数组为-1,表示所有节点初始时未匹配
    memset(match, -1, sizeof match);  
    int res = 0;  // 初始化匹配数为0
    // 遍历所有左侧节点u(假设左侧节点编号从1到n)
    for(int u = 1; u <= n; ++u) {
    
      
        // 每次调用dfs前重置vis数组
        memset(vis, 0, sizeof vis);  
        // 尝试为u找到增广路径
        if(dfs(u)) res++;  // 如果找到增广路径,匹配数加1
    }  
    // 返回最大匹配数
    return res;  
}  

6.3 最大流与最小割

Dinic算法

Dinic算法是求解最大流问题的高效算法,基于分层图的思想。以下是代码模板:

// 定义边结构体,包含目标节点、反向边索引和容量
struct Edge {
    
      
    int to, rev, cap;  // to: 目标节点,rev: 反向边的索引,cap: 边的容量
};  

// 图的邻接表表示,每个节点对应一个边的列表
vector<Edge> g[MAXN];  // MAXN是图中节点的最大数量

// 添加一条边到图中
void add_edge(int from, int to, int cap) {
    
      
    // 添加正向边
    g[from].push_back((Edge){
    
    to, (int)g[to].size(), cap});  
    // 添加反向边,容量初始化为0
    g[to].push_back((Edge){
    
    from, (int)g[from].size()-1, 0});  
}  

// 用于Dinic算法的层级数组和迭代器数组
int level[MAXN], iter[MAXN];  

// BFS函数,用于构建分层图
void bfs(int s) {
    
      
    // 初始化层级数组为-1
    memset(level, -1, sizeof level);  
    queue<int> q;  // 使用队列进行广度优先搜索
    level[s] = 0;  // 源点的层级设为0
    q.push(s);  
    while(!q.empty()) {
    
      
        int v = q.front(); q.pop();  
        // 遍历所有与v相连的边
        for(Edge &e : g[v]) {
    
      
            // 如果边的容量大于0且目标节点未被访问过
            if(e.cap > 0 && level[e.to] < 0) {
    
      
                level[e.to] = level[v] + 1;  // 设置目标节点的层级
                q.push(e.to);  // 将目标节点加入队列
            }  
        }  
    }  
}  

// DFS函数,用于在分层图中寻找增广路径
int dfs(int v, int t, int f) {
    
      
    // 如果到达汇点,返回当前流量
    if(v == t) return f;  
    // 遍历所有与v相连的边(从上次遍历的位置开始)
    for(int &i=iter[v]; i<g[v].size(); ++i) {
    
      
        Edge &e = g[v][i];  // 获取当前边
        // 如果边的容量大于0且目标节点的层级高于当前节点
        if(e.cap > 0 && level[v] < level[e.to]) {
    
      
            // 递归寻找增广路径,取最小的剩余容量
            int d = dfs(e.to, t, min(f, e.cap));  
            // 如果找到增广路径
            if(d > 0) {
    
      
                e.cap -= d;  // 减少正向边的容量
                g[e.to][e.rev].cap += d;  // 增加反向边的容量
                return d;  // 返回增广的流量
            }  
        }  
    }  
    return 0;  // 未找到增广路径,返回0
}  

// 计算最大流的主函数
int max_flow(int s, int t) {
    
      
    int flow = 0;  // 初始化最大流为0
    while(true) {
    
      
        bfs(s);  // 构建分层图
        // 如果汇点不可达,返回当前最大流
        if(level[t] < 0) return flow;  
        // 重置迭代器数组
        memset(iter, 0, sizeof iter);  
        int f;  
        // 在分层图中不断寻找增广路径
        while((f = dfs(s, t, INF)) > 0) {
    
      
            flow += f;  // 累加增广的流量
        }  
    }  
}  

六、狂刷真题

用魔法打败魔法,用真题打败真题。打开蓝桥真题库,库库刷起来吧。
在这里插入图片描述
这里有很多题,多写写找找题感,将会在赛场上大放异彩。


七、结语

算法学习没有捷径,但正确的备考方向能让你事半功倍。建议将本文提到的每个算法模板手写实现一遍,深入理解其中的细节。刷题过程中遇到问题,欢迎在评论区留言讨论!

立即行动:将本文提到的每个算法模板手写实现一遍,你会发现不一样的细节!