第四章 前缀和,差分

目录

1 前缀和

1.1 什么是前缀和?

1.2 前缀和的作用

注意事项:

1.3 一维前缀和

1.3.1 代码模板

1.3.2 例题

分析: 

 代码:

1.4 二维前缀和

 1.4.1 代码模板

2 差分

2.1 什么是差分?

2.2 差分的作用

注意事项:

 2.3 一维差分

2.3.1 代码模板

 2.4 二维差分

2.4.1 代码模板

3 前缀和与差分比较

4 例题

 蓝桥杯省赛 3514 子串简写

分析:

代码:

 蓝桥杯省赛 3533 棋盘

分析:

代码:


1 前缀和

1.1 什么是前缀和?

前缀和概念源于数列知识,其核心思想是将数列的前n项和视为一个新的数组,这个数组即被称为前缀和数组。通过前缀和数组,我们可以高效地处理与数列部分和相关的计算问题

1.2 前缀和的作用

前缀和是求解区间和的高效工具。其预处理过程的时间复杂度为O(n),即遍历一次原数组即可得到前缀和数组。一旦前缀和数组构建完成,查询任意区间和的时间复杂度可降至O(1)

然而,需要注意的是,前缀和算法适用于数组元素不变的情况。若数组频繁变动,则需重新构建前缀和数组,这将再次引入O(n)的时间复杂度。

注意事项:

  • 从数组的第一个元素开始遍历以构建前缀和数组。(建议角标从1开始)
  • 考虑到大数问题,建议使用long long类型存储前缀和及计算结果

1.3 一维前缀和

一维前缀和的本质是在一维坐标轴上计算线段长度(即区间和)。

1.3.1 代码模板

  • a[i] 为原数组,𝑠[𝑖]为前缀和数组。
  • 𝑠𝑢𝑚是数组 𝑎中区间 [𝑙,𝑟]的和。
  • 一维前缀和的递推公式:𝑠[𝑖]=𝑠[𝑖−1]+𝑎[𝑖]
for(int i = 1; i <= n; i ++) 
{
    s[i] = s[i - 1] + a[i];
}

int sum = s[r] - s[l - 1];

1.3.2 例题

蓝桥杯省赛 2080 求和

问题描述

给定 𝑛个整数 𝑎1,𝑎2,⋅⋅⋅,𝑎𝑛,求它们两两相乘再相加的和,即:

𝑆=𝑎1⋅𝑎2+𝑎1⋅𝑎3+⋯+𝑎1⋅𝑎𝑛+𝑎2⋅𝑎3+⋯+a_{n-2}a_{n-1}+a_{n-2}a_{n}+a_{n-1}a_{n}

输入格式

输入的第一行包含一个整数 𝑛。

第二行包含 𝑛n 个整数 𝑎1,𝑎2,⋯,𝑎𝑛​。

输出格式

输出一个整数 𝑆,表示所求的和。请使用合适的数据类型进行运算。

样例输入

4
1 3 6 9

样例输出

117
分析: 

 代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 3;
#define int long long
int a[N], s[N];

signed main()
{
    int n;
    cin >> n;
    long long ans = 0;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        s[i] = s[i - 1] + a[i];
    }
    for (int i = 1; i <= n; i++)
    {
        ans += (a[i] * (s[n] - s[i]));
    }
    cout << ans;
    return 0;
}

1.4 二维前缀和

二维前缀和则用于在二维平面上快速计算指定矩形区域内的元素总和(面积大小)。

 1.4.1 代码模板

  • a[i][j] 为原数组,𝑠[𝑖][𝑗]为前缀和数组。
  • 𝑠𝑢𝑚为 (𝑥1,𝑦1)到𝑥(2,𝑦2)的子矩阵和。
  • 二维前缀和的递推公式:𝑠[𝑖][𝑗]=𝑠[𝑖−1][𝑗]+𝑠[𝑖][𝑗−1]−𝑠[𝑖−1][𝑗−1]+𝑎[𝑖][𝑗]。
for(int i = 1; i <= n; i ++) 
{
    for(int j = 1; j <= m; j ++) 
    {
        s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
    }
}

int sum = s[x2][y2]-s[x2][y1 - 1] - s[x1 - 1][y2] + S[x1 - 1][y1 - 1];

2 差分

2.1 什么是差分?

差分也是对数列的一种操作,可以通过维护一个差分数组来高效地处理原数组的区间增减操作

通过对差分数组进行特定的修改,可以间接地反映到原数组的区间上,而不需要直接修改原数组的每一个元素。

2.2 差分的作用

差分的主要作用是优化区间修改操作的效率。对于频繁需要对数组进行区间增减操作并查询单点或区间值的问题,使用差分可以在O(1)时间复杂度内完成区间修改,并在O(n)时间复杂度内通过前缀和还原出修改后的原数组,进而进行单点或区间查询。

注意事项

  • 差分操作适用于区间修改、单点或区间查询的场景。
  • 差分数组在初始时通常根据原数组构造,但之后的操作都是直接对差分数组进行的。
  • 查询时需要先通过差分数组还原出修改后的原数组(或部分),再进行查询。

 2.3 一维差分

一维差分的作用是给一维的某区间(线段)每个元素加或减一个数。

2.3.1 代码模板

  • b[i]是差分数组,𝑎[𝑖]是原数组,也是𝑏[𝑖]的前缀和数组。
  • 差分数组的递推公式:𝑏[𝑖]=𝑎[𝑖]−𝑎[𝑖−1]。
for(int i = 1; i <= n; i ++) 
{
    b[i] = a[i] - a[i - 1];
}

//对a数组的[l,r]区间都加上一个C
b[l] += c;
b[r + 1] -= c;

 2.4 二维差分

二维差分相当于在二维区间里(平面内)每个元素加或减一个数。

2.4.1 代码模板

  • 𝑏[𝑖][𝑗]是差分数组。

  • 𝑎[𝑖][𝑗]是原数组,也是𝑏[𝑖][𝑗]的前缀和数组。

  • 二维差分数组的递推公式:𝑏[𝑖][𝑗]=𝑎[𝑖][𝑗]−𝑎[𝑖−1][𝑗]−𝑎[𝑖][𝑗−1]+𝑎[𝑖−1][𝑗−1]。

for(int i = 1; i <= n; i ++) 
{
    for(int j = 1; j <= m; j ++) 
    {
        b[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1];
    }
}
//将(x1, y1) -> (x2, y2)的子矩阵都加上一个常数c
b[x1][y1]+=c;
b[x2 + 1][y1]-=c;
b[x1][y2 + 1]-=c;
b[x2 + 1][y2 + 1]+=c;

3 前缀和与差分比较

  1. 数据处理工具:差分与前缀和都是对原始数据进行处理的有效工具,用于简化区间查询或修改操作的复杂度。
  2. 互逆操作:差分与前缀和在一定程度上可以视为互逆操作。差分是将原数组转化为差分数组,而前缀和则是通过差分数组还原原数组或计算区间和。

4 例题

 蓝桥杯省赛 3514 子串简写

问题描述

程序猿圈子里正在流行一种很新的简写方法:对于一个字符串,只保留首尾字符,将首尾字符之间的所有字符用这部分的长度代替。例如 internation-alization 简写成 i18n,Kubernetes (注意连字符不是字符串的一部分)简写成 K8s, Lanqiao 简写成 L5o 等。

在本题中,我们规定长度大于等于 𝐾 的字符串都可以采用这种简写方法(长度小于 𝐾 的字符串不配使用这种简写)。

给定一个字符串 𝑆 和两个字符 𝑐1 和𝑐2,请你计算𝑆有多少个以𝑐1开头𝑐2结尾的子串可以采用这种简写?

输入格式

第一行包含一个整数 𝐾。

第二行包含一个字符串 𝑆 和两个字符 𝑐1​ 和 𝑐2。

输出格式

一个整数代表答案。

样例输入

4
abababdb a b

样例输出

6

样例说明

符合条件的子串如下所示,中括号内是该子串:

[abab]abdb

[ababab]db

[abababdb]

ab[abab]db

ab[ababdb]

abab[abdb]

分析:

首先,考虑暴力做法。我们可以两层循环枚举 (𝑖,𝑗∈[1,𝑛]),若 𝑆𝑖等于𝑐1并且𝑆𝑗等于𝑐2并且𝑗−𝑖+1≥𝑘,则说明找到一种合法情况,这样做的复杂度为 𝑂(𝑛2),可以拿到部分分数。

仔细思考上述暴力做法的过程,当𝑆𝑗等于𝑐2时,我们想知道在区间[0,j+𝑘−1]存在多少个𝑐1,因为这些𝑐1是可以和当前的𝑐2合法配对的。

设区间[0,j+𝑘−1]中𝑐1出现次数为𝑥,则第 𝑖 位对答案的贡献即为𝑥。

因为涉及到一个静态查询区间某个数的出现次数,我们可以使用 前缀和 算法进行优化。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 3;
#define int long long
int a[N], b[N];

signed main()
{
    int k;
    string s;
    char c1, c2;
    cin >> k >> s >> c1 >> c2;
    a[0] = (s[0] == c1);
    for (int i = 1; i <= (int)s.size(); i++)
    {
        if (s[i] == c1)
            a[i] = a[i - 1] + 1;
        else
            a[i] = a[i - 1];
    }
    int ans = 0;
    for (int i = 0; i < (int)s.size(); i++)
    {
        if (s[i] != c2)
            continue;
        if (i - k + 1 >= 0)
            ans += a[i - k + 1];
    }
    cout << ans;
    return 0;
}

 蓝桥杯省赛 3533 棋盘

问题描述

小蓝拥有 𝑛×𝑛大小的棋盘,一开始棋盘上全都是白子。小蓝进行了 𝑚 次操作,每次操作会将棋盘上某个范围内的所有棋子的颜色取反(也就是白色棋子变为黑色,黑色棋子变为白色)。请输出所有操作做完后棋盘上每个棋子的颜色。

输入格式

输入的第一行包含两个整数 𝑛,𝑚,用一个空格分隔,表示棋盘大小与操作数。

接下来 𝑚 行每行包含四个整数 𝑥1​,𝑦1​,𝑥2​,𝑦2,相邻整数之间使用一个空格分隔,表示将在 𝑥1​ 至 𝑥2 行和 𝑦1​ 至 𝑦2 列中的棋子颜色取反。

输出格式

输出 𝑛 行,每行 𝑛 个 0 或 1 表示该位置棋子的颜色。如果是白色则输出 0,否则输出 1。

样例输入

3 3
1 1 2 2
2 2 3 3
1 1 3 3

样例输出

001
010
100

分析:

我们先按照题意模拟,数据范围中约束 1≤𝑥1≤𝑥2≤𝑛 且 1≤𝑦1≤𝑦2≤𝑛,实际上给出的点就是操作矩形的左上端点和右下端点,利用它可以很方便地遍历子矩阵。

但这样做的时间复杂度是 𝑂(𝑚×𝑛2),在约束下无法通过本题。考虑使用二维差分优化。

首先需要理解的一点是棋盘中的每一个格子,如果最终被操作了偶数次,那它一定是白色;如果被操作了奇数次,那它一定是黑色。

所以我们维护每个格子被操作的次数,最后根据奇偶性输出。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 2003;
int a[N][N], b[N][N];

int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        b[x1][y1]++;
        b[x2 + 1][y2 + 1]++;
        b[x1][y2 + 1]--;
        b[x2 + 1][y1]--;
    }
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
            a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
    }
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            cout << a[i][j] % 2;
        }
        cout << endl;
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值