描述C++中的std::exclusive_scan和std::inclusive_scan算法。

讲座主题:C++中的std::exclusive_scan和std::inclusive_scan算法

引言:扫描你的数据,像魔法一样!

今天,我们来聊聊两个听起来有点复杂的家伙——std::exclusive_scanstd::inclusive_scan。如果你对C++的STL(Standard Template Library)有一定了解,那你一定知道STL中有许多强大的算法工具,它们就像魔法师手中的魔杖,能让你的数据处理变得简单又高效。

std::exclusive_scanstd::inclusive_scan是C++17引入的两个新算法,它们属于“扫描”类算法。这些算法的核心思想是从一个序列中生成一个新的序列,其中每个元素都是前面所有元素的某种累积结果。听起来是不是有点抽象?别担心,接下来我们会用通俗易懂的语言和代码示例来解释它们。


第一课:什么是扫描?

在数学中,“扫描”是一种常见的操作,它通常用于计算序列的前缀和或前缀积。例如,给定一个数组 [1, 2, 3, 4],我们可以计算它的前缀和为 [0, 1, 3, 6][1, 3, 6, 10],具体取决于我们是否包括当前元素。

在C++中,std::exclusive_scanstd::inclusive_scan 就是用来实现这种“扫描”操作的工具。


第二课:std::inclusive_scan —— 包容一切的扫描

定义

std::inclusive_scan 是一个包容性的扫描算法。它会从输入序列的第一个元素开始,依次计算累积值,并将结果存储到输出序列中。换句话说,每个输出元素都包含当前元素的值。

形式化定义

假设我们有一个输入序列 [a0, a1, a2, ..., an-1],并且使用加法作为二元操作符,则 std::inclusive_scan 的输出序列为:

[b0, b1, b2, ..., bn-1]

其中:

  • b0 = a0
  • b1 = a0 + a1
  • b2 = a0 + a1 + a2
  • bn-1 = a0 + a1 + ... + an-1

示例代码

#include <iostream>
#include <vector>
#include <numeric> // std::inclusive_scan

int main() {
    std::vector<int> input = {1, 2, 3, 4};
    std::vector<int> result(input.size());

    // 使用默认加法操作
    std::inclusive_scan(input.begin(), input.end(), result.begin());

    for (int value : result) {
        std::cout << value << " ";
    }
    // 输出: 1 3 6 10
}

参数说明

  • 输入范围[first, last) 指定了要扫描的输入序列。
  • 输出迭代器result 指定了存储结果的位置。
  • 二元操作符(可选):如果不指定,默认使用加法操作。

第三课:std::exclusive_scan —— 排除当下的扫描

定义

std::inclusive_scan 不同,std::exclusive_scan 是一种排他性的扫描算法。它不会将当前元素包含在累积值中,而是只考虑之前的元素。

形式化定义

假设我们有一个输入序列 [a0, a1, a2, ..., an-1],并且使用加法作为二元操作符,则 std::exclusive_scan 的输出序列为:

[b0, b1, b2, ..., bn-1]

其中:

  • b0 = init (初始值)
  • b1 = init + a0
  • b2 = init + a0 + a1
  • bn-1 = init + a0 + a1 + ... + an-2

注意:init 是一个用户指定的初始值。

示例代码

#include <iostream>
#include <vector>
#include <numeric> // std::exclusive_scan

int main() {
    std::vector<int> input = {1, 2, 3, 4};
    std::vector<int> result(input.size());

    int init = 0; // 初始值
    std::exclusive_scan(input.begin(), input.end(), result.begin(), init);

    for (int value : result) {
        std::cout << value << " ";
    }
    // 输出: 0 1 3 6
}

参数说明

  • 输入范围[first, last) 指定了要扫描的输入序列。
  • 输出迭代器result 指定了存储结果的位置。
  • 初始值init 是累积操作的起始值。
  • 二元操作符(可选):如果不指定,默认使用加法操作。

第四课:比较与选择

为了让两者的关系更清晰,我们可以通过一个表格来对比它们的区别:

特性 std::inclusive_scan std::exclusive_scan
是否包含当前元素
初始值 默认为第一个元素 需要显式指定
输出序列长度 与输入序列相同 与输入序列相同
典型用途 前缀和 累积历史值

第五课:实际应用场景

场景1:计算前缀和

无论是 std::inclusive_scan 还是 std::exclusive_scan,都可以用来计算前缀和。选择哪个取决于你是否需要包含当前元素。

场景2:动态规划

在动态规划问题中,这两个算法可以帮助我们快速计算状态转移方程中的累积值。

场景3:并行计算

由于扫描操作可以被分解为多个子任务并行执行,std::inclusive_scanstd::exclusive_scan 在并行计算中也非常有用。


结语:扫描的力量

通过今天的讲座,我们了解了 std::inclusive_scanstd::exclusive_scan 的基本概念、使用方法以及它们的区别。希望你能将这些知识应用到实际编程中,让代码更加简洁高效!

最后,引用一段来自国外技术文档的话:“扫描算法不仅仅是简单的累加操作,它们是现代算法设计的重要组成部分。” 所以,拿起你的魔杖(代码编辑器),开始扫描你的数据吧!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注