CC++ & Algorithm

数值算法在竞赛中的综合应用

极难2
语言版本:通用
概述:综合运用accumulate、inner_product、partial_sum、iota和bitset解决典型竞赛问题,提升实战能力。

让数值算法变成你的竞赛利器:积累、内积、前缀和、iota 和 bitset 的组合运用

这是干什么的?

在编程竞赛中,你常常需要快速处理大量数字或集合问题。比如:老师想知道全班同学每周零花钱的总和是多少?或者,你和同桌一起做选择题,要统计你们有多少题答案一样?又或者,学校要记录每个同学参加了哪些社团,想知道两个社团有哪些共同成员?这些问题看起来不同,但背后都可以用 STL 里几个“数值算法工具箱”里的工具来解决。

我们之前学过 accumulateinner_productpartial_sumiotabitset,它们就像乐高积木,每一块都很小,但组合起来就能搭出强大的程序。今天,我们就通过三个真实的竞赛例题,看看怎么把这些工具用活,让你在考场上又快又准地写出代码。


1. 前缀和与区间查询:像统计零花钱一样简单

生活小故事:小明每个星期都会把零花钱存进小猪存钱罐里。他记录下每天存了多少元(比如周一存 5 元,周二存 3 元,周三存 8 元……)。有一天妈妈问:“从周一到周三你一共存了多少钱?”小明很快就能用“周一 + 周二 + 周三”算出来。但如果妈妈问:“从周二到周五一共存了多少?”他只需要把前五天总数减去前一天的数就行。这个思路就是 前缀和:先算出到每一天为止的总和,然后任意区间的和 = 两个前缀和之差。

竞赛问题:给你一个长度为 N 的整数数组 a,有 Q 次询问,每次问从第 l 个到第 r 个元素的和(下标从 1 开始)。N 和 Q 都可能很大(比如 10 万),每次查询必须 O(1) 回答。

解法思路:用 partial_sum 快速算出前缀和数组 pre,其中 pre[i] 表示前 i 个数的和(pre[0]=0)。那么区间 [l, r] 的和 = pre[r] - pre[l-1]

C++ 实现(带详细注释)

#include <iostream>
#include <vector>
#include <numeric>  // partial_sum

int main() {
    int N, Q;                     // N: 数组长度, Q: 询问次数
    std::cin >> N >> Q;
    std::vector<long long> a(N);  // 用 long long 防止求和溢出
    for (int i = 0; i < N; ++i) std::cin >> a[i];

    // 创建前缀和数组,大小 N+1,pre[0]=0
    std::vector<long long> pre(N + 1, 0);
    // 用 partial_sum 把 a 的部分和存入 pre[1] 开始的位置
    std::partial_sum(a.begin(), a.end(), pre.begin() + 1);
    // 现在 pre[0]=0, pre[1]=a[0], pre[2]=a[0]+a[1], ...

    while (Q--) {
        int l, r;                 // 询问的区间(1-based)
        std::cin >> l >> r;
        long long sum = pre[r] - pre[l - 1];  // 关键公式
        std::cout << sum << std::endl;
    }
    return 0;
}

说明partial_sum 默认做加法,它会逐个元素累加并输出到目标位置。我们利用 pre.begin()+1 跳过了 pre[0],这样 pre[0] 自然就是 0。如果不习惯,也可以自己写循环,但 partial_sum 更简洁。

Python 实现(带注释)

import itertools

N, Q = map(int, input().split())
a = list(map(int, input().split()))
# 利用 itertools.accumulate 生成前缀和,前面加一个 0
pre = [0] + list(itertools.accumulate(a))  # pre[0]=0, pre[1]=a[0], ...
for _ in range(Q):
    l, r = map(int, input().split())
    print(pre[r] - pre[l - 1])

Python 的 accumulate 和 C++ 的 partial_sum 功能一样,都返回累加结果。

新手容易犯的错误

  • 下标从 0 还是 1? 题目通常用 1-based 下标,但 C++ 数组默认从 0 开始。所以 pre[1] 对应 a[0]pre[r] - pre[l-1] 中的 l-1r 都是基于 1 的,容易混淆。建议写成 pre[r] - pre[l-1],并确保 pre 足够大。
  • 忘记用 long long:如果数组元素很大(比如 10^9 级别),前缀和可能超过 int 范围,必须用 long long
  • partial_sum 目标位置写错std::partial_sum(a.begin(), a.end(), pre.begin()+1),第三个参数是目标迭代器的起始位置,要确保 pre 的大小至少是 a.size()+1

2. inner_product:快速比较两个序列——像核对答案一样

生活小故事:你和同桌做了一套选择题,每道题有 A、B、C、D 四个选项。你们想快速知道有多少道题答案一样。最直接的办法就是一道一道题看,如果一样就计 1 分。这个“逐位置比较并计数”的过程,就是 内积 的典型应用:对应位置相乘(这里相等为 1,不等为 0),然后求和。

竞赛问题:给定两个长度相同的字符串 st,只包含大小写字母。问有多少个位置 i 使得 s[i]t[i] 相同(区分大小写)。例如 s="abc"t="adc",输出 2(位置 0 和 2 相同)。

C++ 实现(用 inner_product 自定义操作)

#include <iostream>
#include <string>
#include <numeric>  // inner_product

int main() {
    std::string s, t;
    std::cin >> s >> t;

    // inner_product(s.begin(), s.end(), t.begin(), 初始值, 累加操作, 对应位置操作)
    int same = std::inner_product(s.begin(), s.end(), t.begin(), 0,
                                   std::plus<int>(),       // 累加:把结果加起来
                                   [](char a, char b) {    // 对应位置:相等返回1,不等返回0
                                       return a == b ? 1 : 0;
                                   });
    std::cout << same << std::endl;
    return 0;
}

说明inner_product 默认是“对应位置相乘再求和”,但我们可以通过后两个参数定制:第一个自定义操作替换“乘法”,第二个自定义操作替换“加法”。这里我们把“乘法”改成“比较并返回 0/1”,把“加法”保留为 std::plus<int>()。Lambda 表达式 [](char a, char b) { return a == b ? 1 : 0; } 就是自定义的对应位置操作。

Python 实现

s = input().strip()
t = input().strip()
# 用 zip 配对,生成器表达式求和
same = sum(1 for a, b in zip(s, t) if a == b)
print(same)

Python 没有内积函数,但 zip 加列表推导式非常直观,性能也不差。

新手容易犯的错误

  • 忘记字符串长度可能不同inner_productzip 都会以短的那一个为准,但题目一般保证相等,否则要先检查长度。
  • 自定义操作返回类型错误:C++ 中 inner_product 的第四参数(初始值)类型决定了累加结果的类型,这里初始值 0int,所以 std::plus<int>() 也是 int。如果字符串长度很大可能超过 int,要改成 0LL(long long)。
  • Lambda 不熟悉:可以换成普通函数或函数对象,但 Lambda 更简洁。记得捕获 [] 为空,因为不需要外部变量。

3. bitset:用位来管理集合——就像班级签到表

生活小故事:学校有 1000 个学生,编号从 1 到 1000。每个班级要统计哪些学生参加了数学竞赛。如果给每个班级准备一张纸,上面列出所有学号,打勾表示参加,那这张纸就是一张“位图”。如果两个班级要找出共同参加了竞赛的学生,只需要把两张纸上下对齐,看哪些位置两个勾都打了。这个“对齐比较”操作,计算机里就是 按位与。用 bitset 来做,又快又省内存:每个学生只占 1 位(bit),1000 个学生只需要 125 字节!

竞赛问题:有 N 个班级(N ≤ 1000),每个班级有一些学生参加了数学竞赛,学生编号从 1 到 M(M ≤ 10^5)。现在老师想知道:哪些学生至少参加了一个班级的竞赛?另外,给出两个班级的编号 x 和 y,求这两个班级的参赛学生交集。要求高效处理。

思路:每个班级用一个 bitset 表示,第 i 位为 1 表示学号 i 参加了。那么“至少参加一个班级”就是所有 bitset 的按位或(OR),“两个班级的交集”就是按位与(AND)。C++ 的 std::bitset 在编译期固定大小,这里假设 M=10000 便于演示;如果 M 是变量,可以用 std::vector<bool> 或动态 bitset 库(如 boost::dynamic_bitset),但竞赛中常用固定大小。

C++ 实现(带注释)

#include <iostream>
#include <bitset>
#include <vector>

const int MAXM = 10000;  // 假设最大学生编号为 10000

int main() {
    int N;  // 班级数量
    std::cin >> N;
    std::vector<std::bitset<MAXM>> classes(N);  // 每个班级一个 bitset

    for (int i = 0; i < N; ++i) {
        int k;  // 该班级人数
        std::cin >> k;
        for (int j = 0; j < k; ++j) {
            int stu;  // 学生编号
            std::cin >> stu;
            classes[i].set(stu - 1);  // 学生编号从1开始,位索引0对应学生1
        }
    }

    // 所有班级的并集:至少参加一个班级
    std::bitset<MAXM> all;  // 初始全0
    for (const auto& b : classes) {
        all |= b;           // 按位或
    }
    std::cout << "至少参加一个班级的学生人数: " << all.count() << std::endl;

    // 查询两个班级的交集
    int x, y;  // 班级编号(1-based)
    std::cin >> x >> y;
    auto inter = classes[x - 1] & classes[y - 1];  // 按位与
    std::cout << "两班交集人数: " << inter.count() << std::endl;

    // 还可以枚举交集的学生
    std::cout << "交集学生学号: ";
    for (size_t i = 0; i < MAXM; ++i) {
        if (inter.test(i)) {
            std::cout << i + 1 << " ";
        }
    }
    std::cout << std::endl;

    return 0;
}

注意std::bitset 的大小必须是编译期常量。如果 M 是运行时输入的,可以用 std::vector<bool>,但它的位操作(按位与、或)不像 bitset 那么直观,需要手动遍历。另一个常用技巧是:用 unsigned long long 数组模拟大 bitset,但较复杂。

Python 实现(用集合替代,但仅作概念演示)

N = int(input())
classes = []
for _ in range(N):
    data = list(map(int, input().split()))
    k = data[0]
    students = set(data[1:])   # 用 set 存储该班级的学生学号
    classes.append(students)

# 所有班级的并集
all_set = set()
for s in classes:
    all_set |= s
print("至少参加一个班级的学生人数:", len(all_set))

x, y = map(int, input().split())
inter = classes[x-1] & classes[y-1]
print("两班交集人数:", len(inter))
print("交集学生:", sorted(inter))

Python 的 set 虽然直观,但每个学生占用内存远大于 1 位(约 72 字节),当 M=10^5 时,一个班级的 set 可能占用几 MB。而 C++ 的 bitset<100000> 只占 12.5 KB。这就是 bitset 的威力。

新手容易犯的错误

  • bitset 索引从 0 开始:学生编号若从 1 开始,要记得 set(stu-1)
  • 忘记 clear():如果需要重复使用同一个 bitset,要用 .reset() 清空,否则上次的数据会残留。
  • 位操作优先级a & b | c 实际上等于 (a & b) | c,因为 & 优先级高于 |。建议用括号明确,如 (a & b) | c
  • M 太大导致 bitset 声明失败:比如 bitset<1000000>(一百万个位)是允许的,但若 M=10^7,bitset 可能超过栈内存(约 1.25MB 栈,可能不够),这时需要动态分配(如 vector<bool>)。

4. 综合应用:用 iota 生成编号,用 accumulate 求和,用 bitset 做统计

为了让你看看这些工具如何协同工作,我们设计一个综合小问题:

题目:小明在期末考试中,语、数、英三科的成绩分别存储在三个数组 chinese[100]math[100]english[100](每个学生一科成绩)。现在要统计所有学生中总分大于等于 270 分的学生人数,并且输出这些学生的编号(从 1 到 100)。

提示:可以用 iota 生成学生编号数组,用 transforminner_product 或手算总分,再用 accumulate 计数?实际更好的做法是循环,但为了展示组合,我们可以这样:

#include <iostream>
#include <numeric>  // iota, accumulate
#include <vector>
#include <bitset>

int main() {
    const int N = 100;
    std::vector<int> chinese(N), math(N), english(N);
    // 假设已输入成绩,这里省略

    // 用 iota 生成编号 1..100
    std::vector<int> ids(N);
    std::iota(ids.begin(), ids.end(), 1);  // ids = [1,2,...,100]

    // 计算每个学生的总分,存入 total 数组
    std::vector<int> total(N);
    for (int i = 0; i < N; ++i) {
        total[i] = chinese[i] + math[i] + english[i];
    }

    // 用 bitset 标记总分 >=270 的学生(位索引对应学生编号-1)
    std::bitset<100> excellent;
    for (int i = 0; i < N; ++i) {
        if (total[i] >= 270) {
            excellent.set(i);  // 注意:位索引 i 对应学生 id = i+1
        }
    }

    std::cout << "优秀学生人数: " << excellent.count() << std::endl;
    std::cout << "优秀学生编号: ";
    for (size_t i = 0; i < N; ++i) {
        if (excellent.test(i)) {
            std::cout << i + 1 << " ";
        }
    }
    std::cout << std::endl;

    // 还可以用 accumulate 求总分总和(所有学生)
    int sum_all = std::accumulate(total.begin(), total.end(), 0);
    std::cout << "所有学生总分总和: " << sum_all << std::endl;

    return 0;
}

这个例子展示了 iota 生成序列、bitset 做标记、accumulate 求总和。当然,实际竞赛中你可能会用更直接的方法,但理解这些工具的灵活组合会让你思路更开阔。


总结要点和常见错误

工具典型用途常见错误
partial_sum / accumulate求前缀和、区间和;求总和忘记前缀和数组大小 N+1;没考虑 int 溢出
inner_product对应位置比较、计算内积自定义操作返回类型不对;初始值类型不匹配
iota生成连续的序列(如编号)忘记指定起始值;误用于非整数类型
bitset集合运算、状态压缩索引从 0 开始;大小编译期常量;位操作优先级

竞赛实战小贴士

  • long long 做数值累加:尤其在求和、前缀和中,用 long long 避免溢出。
  • bitset 的大小要足够:如果 M 超过 10^6,考虑用 vector<bool> 或手动位运算。
  • 优先使用标准算法partial_suminner_product 等比自己写的循环更少 bug,且代码更短。
  • 不要滥用函数式风格:如果逻辑复杂(如 inner_product 的 lambda 很长),不如写循环可读性更好。
  • C++ 与 Python 对比:C++ 性能高,适合大数据;Python 开发快,适合小数据或原型。在大型竞赛中,C++ 是主流。

相关知识点继续学习

  • STL 算法库:除了数值算法,还有 sortfindcopy 等,它们与数值算法组合使用能解决很多问题。
  • vector<bool> 特化:它也是一个类似 bitset 的位容器,但接口略有不同。
  • 大整数与位运算:Python 的整数可以无限位,适合模拟大 bitset;C++ 可以用 unsigned long long 数组实现 64 位的 bitset 拼接。

掌握了这些数值算法,你就拥有了一个强大的数学工具箱。下一次面对区间查询、集合运算、序列比较等问题时,不要急着手写循环,先想想工具箱里的工具能不能直接拿来用。快打开你的编译器,动手试试吧!

例题精讲

1单选题

在C++中,以下关于数值算法与bitset的叙述中,正确的是?

A`accumulate`函数可以用于计算一个bitset对象中1的个数,因为bitset提供了迭代器。
B`inner_product`函数可以计算两个bitset的内积,但必须先将bitset转换为数组或vector。
C`partial_sum`函数可以就地计算bitset的前缀和,但要求bitset具有随机访问迭代器。
D`iota`函数不能用于bitset,因为bitset不支持赋值操作。
2判断题

`std::partial_sum`可以用于计算一个整型数组的前缀和,但不能用于计算前缀积,因为其默认操作是加法。

3填空题
给定一个长度为N的整数数组a,请使用`std::partial_sum`函数计算其前缀和并存储在数组prefix中,要求prefix[0]=a[0],prefix[1]=a[0]+a[1],依此类推。补全代码:`std::partial_sum(a, a+N, ___);`
4单选题

考虑使用`std::bitset`和`std::iota`解决以下问题:需要生成一个长度为16的bitset,其中第i位(从0开始)的值为1当且仅当i是3的倍数。下列哪个代码片段能正确实现?

A`std::bitset<16> b; std::iota(b.begin(), b.end(), 0);`
B`std::bitset<16> b; for(int i=0;i<16;i+=3) b.set(i);`
C`std::bitset<16> b; std::partial_sum(b.begin(), b.end(), b.begin());`
D`std::bitset<16> b; std::accumulate(b.begin(), b.end(), 0);`
5填空题
使用`std::inner_product`计算两个长度为n的整数数组a和b的点积(内积),结果存入变量dot。补全代码:`int dot = std::inner_product(a, a+n, b, ___);`