云计算百科
云计算领域专业知识百科平台

【万字总结】C++算法竞赛核心基础算法全家桶:从二分到拟阵,收藏这一篇就够了!

摘要:还在为算法基础不牢而苦恼?面对题目不知道用什么算法?本文将 algorithm 竞赛中最核心的基础算法一网打尽!从最常用的二分、尺取,到数据处理神器前缀和、差分、离散化,再到进阶的倍增、ST表,最后深入探讨贪心算法背后的数学原理——拟阵。无论你是蓝桥杯、考研机试还是 ACMer,这篇文章都是你的兵器库。建议收藏反复研读!


目录

  • 搜素与遍历的艺术
    • 尺取法(双指针):像虫子一样蠕动
    • 二分法:不仅是查找,更是答案的判定
    • 三分法:凸函数的极值克星
  • 数据的魔法处理
    • 前缀和与差分:区间操作的

      O

      (

      1

      )

      O(1)

      O(1) 秘密

    • 离散化:让

      10

      9

      10^9

      109 的数据乖乖排队

    • 排列与排序:STL 的骚操作
  • 分治与倍增
    • 分治法:大事化小,各个击破
    • 倍增法与 ST 表:预处理的

      2

      k

      2^k

      2k 智慧

  • 贪心的数学之美
    • 贪心法:局部最优能否推出全局最优?
    • 拟阵(Matroid):贪心算法的理论基石(硬核!)
  • 总结与刷题路线

  • 第一部分:搜索与遍历的艺术

    1. 尺取法(双指针 / Two Pointers)

    核心思想:利用区间的单调性,通过两个指针(

    L

    ,

    R

    L, R

    L,R)像毛毛虫一样在数组上蠕动,避免

    O

    (

    N

    2

    )

    O(N^2)

    O(N2) 的暴力枚举。

    模版代码:

    // 寻找满足条件的最短区间长度
    void solve(int n, int S, vector<int>& a) {
    int l = 0, r = 0, sum = 0, res = n + 1;
    while (true) {
    while (r < n && sum < S) { // 1. 右指针不断进食
    sum += a[r++];
    }
    if (sum < S) break; // 无法满足,结束
    res = min(res, r l); // 更新答案
    sum -= a[l++]; // 2. 左指针收缩
    }
    }

    相应例题:P1638 逛画展

    2. 二分法(Binary Search)

    痛点:很多人写二分死循环。 秘籍:认准一个模版(while(l < r)),不要混用。

    整数二分模版(查找左边界):

    // 检查 x 是否满足某种性质
    bool check(int x) { /* … */ }

    int bsearch_1(int l, int r) {
    while (l < r) {
    int mid = l + r >> 1;
    if (check(mid)) r = mid; // 答案在左边,包含 mid
    else l = mid + 1; // 答案在右边
    }
    return l;
    }

    进阶:二分不仅查下标,更多时候用于二分答案(例如:最大化最小值)。

    3. 三分法(Ternary Search)

    场景:二分解决单调函数,三分解决凸函数/凹函数(求极值)。 原理:取两个点

    m

    1

    ,

    m

    2

    m1, m2

    m1,m2 将区间三等分,比较函数值舍去一段。

    double l = 0, r = 1000;
    while (r l > 1e-7) {
    double m1 = l + (r l) / 3;
    double m2 = r (r l) / 3;
    if (f(m1) < f(m2)) l = m1; // 求最大值时,m1 肯定不是峰值
    else r = m2;
    }


    第二部分:数据的魔法处理

    1. 前缀和与差分

    这是一对互逆运算,专门处理区间问题。

    • 前缀和:快速求区间和

      O

      (

      1

      )

      O(1)

      O(1)

      • 公式:sum[i] = sum[i-1] + a[i]
      • 查询 [L, R]:sum[R] – sum[L-1]
    • 差分:快速进行区间修改

      O

      (

      1

      )

      O(1)

      O(1)

      • 操作:给区间 [L, R] 全部加上 k。
      • 核心:diff[L] += k; diff[R+1] -= k;
      • 最后做一次前缀和还原原数组。

    2. 离散化(Discretization)

    场景:数值很大(如

    10

    9

    10^9

    109),但数据个数很少(如

    10

    5

    10^5

    105),且我们只关心他们的大小关系,不关心具体数值。

    STL 三步走模版:

    vector<int> a; // 存原始数据
    vector<int> b = a; // 备份
    sort(b.begin(), b.end()); // 1. 排序
    b.erase(unique(b.begin(), b.end()), b.end()); // 2. 去重

    // 3. 查找 x 离散化后的值(排名)
    int get_id(int x) {
    return lower_bound(b.begin(), b.end(), x) b.begin() + 1;
    }

    3. 排序与排列

    除了 sort,C++ STL 还有一个神器:next_permutation(生成下一个字典序排列)。 全排列模版:

    vector<int> p = {1, 2, 3};
    do {
    // 处理当前排列
    } while (next_permutation(p.begin(), p.end()));

    有感兴趣的可以参考我之前的一篇文章算法随笔:洛谷P1036 选数


    第三部分:分治与倍增

    1. 分治法(Divide and Conquer)

    口诀:分而治之,合二为一。 经典案例:归并排序求逆序对、快速幂。

    • 快速幂:

      a

      b

      a^b

      ab 可以在

      O

      (

      log

      b

      )

      O(\\log b)

      O(logb) 内算出。long long qpow(long long a, long long b) {
      long long res = 1;
      while (b) {
      if (b & 1) res = res * a;
      a = a * a;
      b >>= 1;
      }
      return res;
      }

    2. 倍增法与 ST 表

    倍增思想:任何数字都可以表示为

    2

    2

    2 的幂次之和。 ST 表(Sparse Table):解决 RMQ(区间最值查询) 问题,不支持修改,但查询是

    O

    (

    1

    )

    O(1)

    O(1) 的!

    核心转移方程:

    f

    [

    i

    ]

    [

    j

    ]

    =

    max

    (

    f

    [

    i

    ]

    [

    j

    1

    ]

    ,

    f

    [

    i

    +

    (

    1

    <

    <

    (

    j

    1

    )

    )

    ]

    [

    j

    1

    ]

    )

    f[i][j] = \\max(f[i][j-1], f[i + (1 << (j-1))][j-1])

    f[i][j]=max(f[i][j1],f[i+(1<<(j1))][j1]) 意思是:以

    i

    i

    i 开始长度为

    2

    j

    2^j

    2j 的最大值,等于左半边的最大值和右半边的最大值取 max。 相应例题:P3865 【模板】ST 表 & RMQ 问题


    第四部分:贪心的数学之美

    1. 贪心法(Greedy)

    定义:顾名思义就是在每一步选择中都采取当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。 难点:证明。很多时候直觉是错的,需要通过“交换论证法”或“反证法”来证明。

    2. 拟阵(Matroid):贪心的理论基石

    你是否疑惑过:为什么最小生成树(Kruskal)可以用贪心?为什么硬币找零问题在某些面额下贪心会失效? 这一切的背后,都有一个数学结构在支撑——拟阵。

    什么是拟阵? 简单来说,拟阵

    M

    =

    (

    S

    ,

    I

    )

    M = (S, I)

    M=(S,I) 是一个二元组,其中

    S

    S

    S 是有限集合,

    I

    I

    I

    S

    S

    S 的子集族(称为独立集),满足以下三个公理:

  • 非空性:空集属于

    I

    I

    I

  • 遗传性:如果你是独立的,那你的子集也是独立的。
  • 交换性(扩充性):如果两个独立集

    A

    A

    A

    B

    B

    B,且

    A

    <

    B

    |A| < |B|

    A<B,那么一定存在一个元素

    x

    B

    A

    x \\in B-A

    xBA,使得

    A

    {

    x

    }

    A \\cup \\{x\\}

    A{x} 也是独立的。

  • 结论:

    如果一个最优化问题可以被建模为“在拟阵中寻找权值最大的独立集”,那么贪心算法一定能得到全局最优解。

    例子:

    • 图的生成树就是一个拟阵(图形拟阵)。所以 Kruskal 算法(贪心)是正确的。
    • 线性基也是一个拟阵(向量拟阵)。

    第五部分:总结

    基础算法是地基,地基不牢,地动山摇。

    • 遇到

      10

      9

      10^9

      109 数据?先想离散化。

    • 求区间最值?静态用 ST表,动态用线段树(进阶)。
    • 判定答案容易,求答案难?二分答案。
    • 贪心拿不准?试着往拟阵的交换性上想一想。

    希望这篇总结能成为你刷题路上的灯塔!

    评论区打卡:你觉得最难理解的基础算法是哪个?

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【万字总结】C++算法竞赛核心基础算法全家桶:从二分到拟阵,收藏这一篇就够了!
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!