算法心传·前缀和与差分

一、一维前缀和与一维差分

1.一维前缀和

什么是前缀和呢?

前缀和(Prefix Sum) 顾名思义就是构建一个新的数组,其中每个元素表示原始数组从起始位置到当前位置的累加和。是一种重要的数据预处理技术,这种通过空间换时间的思想,将区间求和的时间复杂度从O(n)降到O(1),可以快速求出数组任意区间的和,是解决许多区间/子数组问题的利器。

  • 相比较其他算法而言,前缀和更像是一种解题的技巧,一种优化方式。

引入

输入一个长度为n的整数序列,接下来再输入m个询问,每个询问输入一对l,r。对于每个询问,输出原序列中从第l个数到第r个数的和。

对于次问题,我们不难想到暴力法,代码如下:

const int N = 1e5 + 10;  //科学计数法,即100010
int a[N];
int n,m;
cin >> n >> m;           //输入n,m
for(int i = 1; i <= n; i++)
{
    cin >> a[i];
}
while(m--)
{
    int l, r;
    int sum = 0;
    cin >> l >> r;
    for(int i = l; i <= r; i++)
    { 
        sum += a[i];
    }
    cout << sum << endl;
}

不难发现,这段代码的时间复杂度为O(n * m),如果n和m的稍大一些,代码就会超时。

这时候我们如果想优化,可以使用一维前缀和。

首先做一个预处理,定义一个pref[n + 1]数组,pref[i]代表a数组中前i个数的和。

const int N = 1e5 + 10;
int pref[N], a[N];       //pref[i]=a[1]+a[2]+a[3].....a[i];
for(int i = 1;i <= n; i++)
{ 
    pref[i] = pref[i - 1] + a[i];   
}

对于查询操作代码:

cin >> l >> r;
cout << pref[r] - pref[l-1] << endl;

查询操作优化为cout << pref[r] – pref[l-1] << endl,可以参考下图理解。

一维前缀和时间复杂度:预处理O(n),查询O(1),O(n + m)总时间复杂度为效率比较高效,后续也会有一些其他的解法,比如说线段树,树状数组等,前缀和的运行时间是最短的。

2.一维差分

差分(Difference)可以理解为前缀和的逆运算,就是将数列中每一项分别与前一项做差

那么我们如何得到一维差分数组呢?

一维差分数组的有什么用处呢?

同样的,我们思考一个这样的问题:

有n个整数,给定m个区间[l, r ],让我们把a数组中的[l, r] 区间中的每一个数都加上c,即 a[l] + c , a[l + 1] + c , a[l + 2] + c, …… ,a[r] + c,最后按顺序给出改变后的数组。

暴力法和之前的前缀和基本思路相同,同样的,问题是时间复杂度O(n * m)太高。

这时候我们要考虑差分做法。

a数组是diff数组的前缀和数组,对diff数组的diff[i]的修改,会影响到a数组中从a[i]及往后的每一个数。

首先让差分diff数组中的 diff[l] + c ,通过前缀和运算,a数组从下标l向后都多了一个我们添加的c

即 a[l] + c ,a[l + 1] + c, …… ,a[n] + c;

然后我们把差分diff数组中的 diff[r + 1] – c通过前缀和运算,a数组从下标r向后减去一个c

即 a[r + 1] – c,a[r + 2] – c, …… ,a[n] – c;

此时我们发现,对diff数组做前缀和得到的新数组即为仅改变区间lr且改变量为c的数组。

因此我们得到结论:

要对数组区间 [ l, r ] 的数据都加上c时,我们只需要对差分数组进行操作即可。

  • 即 l到前一个元素的距离+c,r到后一个元素的距离-c
diff[l] += c;
diff[r+1] -= c;

这里给出核心代码:

// 核心1:构建差分数组
// diff[i] = a[i] - a[i-1],其中diff[1] = a[1](因为a[0]=0)
for (int i = 1; i <= n; ++i) {
    diff[i] = a[i] - a[i - 1];
}
// 核心2:执行m次区间加操作
for (int i = 0; i < m; ++i) {
    int l, r, c;
    cin >> l >> r >> c; 
    // 差分数组的核心操作
    diff[l] += c;          // l位置加c,影响后面所有元素
    diff[r + 1] -= c;      // r+1位置减c,抵消r之后的影响
}
// 核心3:通过前缀和还原数组
// 对差分数组求前缀和,得到修改后的原数组
for (int i = 1; i <= n; ++i) {
    diff[i] += diff[i - 1];  // 前缀和:当前值 = 前一个值 + 当前差值
    cout << diff[i] << " ";
}

二、二维前缀和和二维差分

1.二维前缀和

二维前缀和(也称为积分图)是对二维数组(矩阵)的一种预处理方法,可以快速计算任意子矩阵的元素和

引入

输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。对于每个询问输出子矩阵中所有数的和。

同一维前缀和一样,我们先来定义一个二维数组diff[n + 1][m + 1]。

diff[i][j] 表示二维数组中,左上角(1, 1)到右下角(i, j)所包围的矩阵元素的和。

那么二维前缀和的公式就是:pref[i][j] = pref[i – 1][j] + pref[i][j – 1 ] + a[i] [j] – pref[i – 1][j – 1]

可以借助下图理解记忆:

整个外围蓝色矩形面积pref[i][j]

绿色面积pref[i – 1][j] + 黄色面积pref[i][j – 1]  + 小方块的面积a[i][j]重复加的红色的面积pref[i – 1][j – 1];

通过图例我们再次印证了上述二维前缀和公式的合理性与正确性。

有了以上基础,现在若要求某一区域的子矩阵和怎么办?

我们从图例中理解如何求以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和。

分析得出以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
pref[x2, y2]pref[x1 – 1, y2]pref[x2, y1 – 1] + pref[x1 – 1, y1 – 1]

下面给出以上子矩阵问题核心代码:

// 计算前缀和
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
        pref[i][j] = pref[i - 1][j] + pref[i][j - 1] - pref[i - 1][j - 1] + a[i][j];
    }
}
while (q--) {
    int x1, y1, x2, y2;
    cin >> x1 >> y1 >> x2 >> y2;  
    // 计算子矩阵和,#define ll long long
    ll sum = pref[x2][y2] - pref[x1 - 1][y2] - pref[x2][y1 - 1] + pref[x1 - 1][y1 - 1];
    cout << sum << endl;
}

2.二维差分

二维差分是处理二维矩阵区间操作的高效工具,可以看作二维前缀和的逆运算。给定一个 m×n 的原始矩阵 A,其对应的二维差分矩阵 D 是一个 (m+2)×(n+2) 的矩阵,其中超出边界的 A[i][j] 视为 0。

对于一个给定的二维数组 a[][],它的二维差分数组 diff[i][j] 可以用如下公式计算:

diff[0][0]=a[0][0]

diff[0][j]=a[0][j]-a[0][j – 1]

diff[i][0]=a[i][0]-a[i – 1][0]

④ 递推式:diff[i][j]=a[i][j]-a[i – 1][j]-a[i][j – 1]+a[i – 1][j – 1]

上述公式是通过二维数组 a[][]是二维差分数组的前缀和这个条件推导出来的。

借助图例理解:

diff[x₁][y₁]     += c; // 对差分数组的更新,影响从(x1,y1)到右下角的整个区域
diff[x₁][y₂+1]   -= c; // 对差分数组的更新,修正从(x1,y2+1)到右下角的区域
diff[x₂+1][y₁]   -= c; // 对差分数组的更新,修正从(x2+1,y1)到右下角的区域
diff[x₂+1][y₂+1] += c; // 对差分数组的更新,修正从(x2+1,y2+1)到右下角的区域

相关习题推荐我会抽时间具体研究然后列出来链接,我们只做精品,祝天天开心!

三、经典例题

1.一维前缀和

P1114 “非常男女”计划 – 洛谷
P1182 数列分段 Section II – 洛谷

2.一维差分

P1083 [NOIP 2012 提高组] 借教室 – 洛谷

3.二维前缀和

4.二维差分

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇