前缀和
文章目录
S i = a 1 + a 2 + . . . + a i Si = a1 + a2 + ... + ai Si=a1+a2+...+ai ( 下标一定要从 1 1 1 开始 )
如何求 Si
f o r i = 1 ; i < = n ; i + + : for ~~i = 1; i <= n; i ++: for i=1;i<=n;i++:
$s[i] = s[ i - 1 ] + a_{i} $
s 0 = 0 s_{0} = 0 s0=0
作用是可以快速求出来原数组中一段数的和,如果没有前缀和数组,要计算 [ l , r ] [ l , r ] [l,r] 内的数之和,时间复杂度为 $O(n) $
但是如果有前缀和数组,则为 S r − S l − 1 S_{r} - S_{l-1} Sr−Sl−1
S r = a 1 + a 2 + . . . + a l − 1 + a l + . . . + a r S_{r} = a_{1}+a_{2}+...+a_{l-1}+a_{l}+...+a_{r} Sr=a1+a2+...+al−1+al+...+ar
S l − 1 = a 1 + a 2 + . . . + a l − 1 S_{l-1}=a_{1}+a_{2}+...+a_{l-1} Sl−1=a1+a2+...+al−1
S r − S l − 1 = a l + . . . + a r Sr - S_{l-1} = a_{l}+...+a_{r} Sr−Sl−1=al+...+ar
时间复杂度为 O ( 1 ) O(1) O(1)
如果求 [ 1 , 10 ] [1,10] [1,10] 则是 S 10 − S 0 S_{10} - S_{0} S10−S0 因为 $ S_{0}$ 已经为 0 0 0 ,所以求的就是 $a_{1} $ 加到 a 10 a_{10} a10
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n,m;
int a[N], s[N];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i]; //前缀和的初始化
while( m -- )
{
int l, r;
scanf("%d%d", &l, &r); //用scanf比cin会快一倍
printf("%d\n", s[r] - s[l - 1]); //区间和的计算
}
return 0;
}
ios::sync_with_stdio(false);
//提高cin的读取速度,但是副作用是不能再使用scanf,但是优化后还是没有scanf快,数据输入的规模如果大于等于一百万,建议用scanf,否则建议用cin
子矩阵的和
O ( n m ) − > O ( 1 ) O(nm) -> O(1) O(nm)−>O(1)
原矩阵
1 | 7 | 2 | 4 |
---|---|---|---|
3 | 6 | 2 | 8 |
2 | 1 | 2 | 3 |
前缀和矩阵
1 | 8 | 10 | 14 |
---|---|---|---|
4 | 17 | 21 | 33 |
6 | 20 | 26 | 41 |
如何计算前缀和矩阵
S x , y = S x − 1 , y + S x , y − 1 − S x − 1 , y − 1 + a x , y S_{x,y} = S_{x-1,y} + S_{x ,y-1} - S_{x-1,y-1} + a_{x,y} Sx,y=Sx−1,y+Sx,y−1−Sx−1,y−1+ax,y
如何利用前缀和矩阵,计算某一个子矩阵的和
S x 2 , y 2 − S x 2 , y 1 − 1 − S x 1 − 1 , y 2 + S x 1 − 1 , y 1 − 1 S_{x_{2},y_{2}} - S_{x_{2},y_{1}-1} - S_{x_{1}-1,y_{2}} + S_{x_{1}-1,y_{1}-1} Sx2,y2−Sx2,y1−1−Sx1−1,y2+Sx1−1,y1−1
#include<iostream>
using namespace std;
int n, m, q;
const int N = 1e3 + 10;
int s[N][N], a[N][N];
int main()
{
scanf("%d%d%d", &n, &m, &q);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
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];
while(q --)
{
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]);
}
return 0;
}
最大加权矩形
利用前缀和解决
s[x] [y] 表示 从 (1, 1) 到 (x, y) 的元素之和 (假设从 下标为 1 开始存储矩阵)
要计算 ( x 1 , y 1 ) (x1, y1) (x1,y1) 到 ( x 2 , y 2 ) (x2, y2) (x2,y2) 的元素之和
那么就是 紫色区域 - 橙色区域 - 绿色区域 + 灰色区域 (灰色区域被减了 2 2 2 遍)
s[x2] [y2] - s[x1] [y1 - 1] - s[x1 - 1] [y2] + s[x1 - 1] [y1 - 1]
对于样例来说,
计算红色区域 ( 2 , 2 ) (2, 2) (2,2) 到 ( 4 , 4 ) (4, 4) (4,4) 中元素之和,
也就是:
( 1 , 1 ) (1, 1) (1,1) 到 ( 4 , 4 ) (4, 4) (4,4) 元素之和 − ( ( 1 , 1 ) - ((1,1) −((1,1) 到 ( 4 , 1 ) (4, 1) (4,1) 元素之和 $) - ((1, 1) $ 到 ( 1 , 4 ) (1, 4) (1,4) 元素之和 ) + ( 1 , 1 ) ) + (1, 1) )+(1,1) 元素
计算s[x] [y]
s[x][y] = s[x - 1][y] + 这一行元素到该点元素之和
这一行元素到该点元素之和 相当于是 这一行元素到 元素 a [ x ] [ y ] a [x] [ y ] a[x][y] 的前缀和
利用 l [ x ] [ y ] l [ x ] [ y ] l[x][y] 数组存储前缀和
递推关系式为
l[x][y] = l[x][y - 1] + a[i][j]
所以可以在边存储矩阵元素的过程中计算前缀和并且得到 s [ x ] [ y ] s [x] [y] s[x][y]
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j++)
{
cin >> a[i][j];
l[i][j] = l[i][j - 1] + a[i][j];
s[i][j] = s[i - 1][j] + l[i][j];
}
因为要得到最大的矩阵元素之和
那就在边计算一个矩阵之和时边跟当前的最大值进行比较,如果比当前最大值还大,那就更新最大值
for(int x1 = 1; x1 <= n; x1++)
for(int y1 = 1; y1 <= n; y1++)
for(int x2 = 1; x2 <= n; x2++)
for(int y2 = 1; y2 <= n; y2++)
{
if(x2 < x1 || y2 < y1) continue;
ans = max(ans, s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
注意边界情况
当 x 2 < x 1 x2 < x1 x2<x1 或者 y 2 < y 1 y2 < y1 y2<y1 时就不用进行计算了
#include<bits/stdc++.h>
using namespace std;
const int N = 150;
int a[N][N], l[N][N];
int n;
int s[N][N], b[N][N];
int ans, square;
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j++)
{
cin >> a[i][j];
l[i][j] = l[i][j - 1] + a[i][j];
s[i][j] = s[i - 1][j] + l[i][j];
}
ans = -2147483647;
for(int x1 = 1; x1 <= n; x1++)
for(int y1 = 1; y1 <= n; y1++)
for(int x2 = 1; x2 <= n; x2++)
for(int y2 = 1; y2 <= n; y2++)
{
if(x2 < x1 || y2 < y1) continue;
ans = max(ans, s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
cout << ans << endl;
return 0;
}
2147483647 2147483647 2147483647 是 i n t int int 的最大值
[NOIP2011 提高] 聪明的质监员
由题意可知,当 W W W 越大时,可以选择的矿石个数就越少, y y y 值也就越小。相反,当 W W W 越小时,可以选择的矿石个数就越多, y y y 值也就越大。
因此
可以把 y y y 看成关于 x x x 的函数,
该函数单调递减且在 s s s 附近不连续,所以还要再判断一下 s s s 附近两个点到 s s s 的距离哪个更近
ans = min(y1 - s, y2 - s)
又考虑到寻找 s s s 附近的两个点,就可以用二分查找的方法 (增加效率)
用 c h e c k ( ) check() check() 函数 记录 y y y
注意数据范围
因为 0 < s < = 1 e 12 0 < s <= 1e12 0<s<=1e12,已经超过 i n t int int 范围,所以开 l o n g l o n g long long longlong
#define LL long long
判断边界:
因为题目中 0 < w i , v i < = 1 e 6 0 < wi, vi <= 1e6 0<wi,vi<=1e6,所以左边界为 0 0 0,右边界为 1 e 6 1e6 1e6
LL l = 0, r = 1e6;
LL mid;
while(l <= r)
{
//先写下面,因为 l = mid + 1 所以这里不需要再加 1
mid = (l + r) >> 1;
//如果 y 值大于 s,将 W 调大,让 y 减小
if(check(mid) > s)
l = mid + 1;
//如果 y 值小于等于 s,将 W 调小,让 y 变大
else r = mid - 1;
//这样找到 s 附近
}
判断 s s s 附近的哪个更小
LL ans = min(abs(check(l) - s), abs(check(r) - s));
c h e c k () check() check() 函数
利用 前缀和 可以降低时间复杂度
y
i
=
∑
j
=
l
i
r
i
[
w
j
≥
W
]
×
∑
j
=
l
i
r
i
[
w
j
≥
W
]
v
j
yi = \sum_{j = li}^{ri}{[wj \geq W]} \times \sum_{j = li}^{ri}{[wj \geq W]}vj
yi=j=li∑ri[wj≥W]×j=li∑ri[wj≥W]vj
公式
y
i
=
∑
j
=
l
i
r
i
[
w
j
≥
W
]
yi = \sum_{j = li}^{ri}{[wj \geq W]}
yi=j=li∑ri[wj≥W]
表示 超过
W
W
W 项的个数
最终的
y
y
y 为
y
=
∑
i
=
1
m
y
i
y = \sum_{i = 1}^{m}yi
y=i=1∑myi
LL check(LL W)
{
LL ans = 0;
for(int i = 1; i <= n; i ++)
{
if(w[i] < W)
//如果w[i] < W,就不用这个矿石
{
num[i] = num[i - 1]; //个数不变
sum[i] = sum[i - 1]; //矿石的价值总和不变
}
else
{
num[i] = num[i - 1] + 1; //个数加 11
sum[i] = sum[i - 1] + v[i]; //矿石的价值由上一次的加上该矿石的价值
}
}
//利用前缀和计算 y 的总和
for(int i = 1; i <= m; i ++)
ans += (num[R[i]] - num[L[i] - 1]) * (sum[R[i]] - sum[L[i] - 1]);
return ans;
}
时间复杂度为 O ( m + n ) O ( m + n ) O(m+n)
完整代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 1e6 + 10;
LL w[N], v[N];
int n, m;
LL s;
LL num[N], sum[N];
int L[N], R[N];
LL check(LL W)
{
LL ans = 0;
for(int i = 1; i <= n; i ++)
{
if(w[i] < W)
{
num[i] = num[i - 1];
sum[i] = sum[i - 1];
}
else
{
num[i] = num[i - 1] + 1;
sum[i] = sum[i - 1] + v[i];
}
}
for(int i = 1; i <= m; i ++)
ans += (num[R[i]] - num[L[i] - 1]) * (sum[R[i]] - sum[L[i] - 1]);
return ans;
}
int main()
{
scanf("%d%d%lld", &n, &m, &s);
for(int i = 1; i <= n; i++)
{
scanf("%lld%lld", &w[i], &v[i]);
}
for(int i = 1; i <= m; i++)
{
scanf("%d%d", &L[i], &R[i]);
}
LL l = 0, r = 1e6;
LL mid;
while(l <= r)
{
mid = (l + r) >> 1;
if(check(mid) > s)
l = mid + 1;
else r = mid - 1;
}
LL ans = min(abs(check(l) - s), abs(check(r) - s));
printf("%lld", ans);
return 0;
}
整体时间复杂度 O ( ( n + m ) ∗ l o g ( m a x w ) ) O((n + m) * log(maxw)) O((n+m)∗log(maxw))
差分
用于快速修改数组中某一段区间的值
d
i
=
a
i
−
a
i
−
1
di = a_{i} - a_{i-1}
di=ai−ai−1
区间
[
l
,
r
]
[ l , r ]
[l,r] 加上
k
k
k
表示成
d
l
=
d
l
+
k
,
d
r
+
1
=
d
r
+
1
−
k
d_{l} = d_{l} + k , d_{r+1} = d_{r+1}-k
dl=dl+k,dr+1=dr+1−k
则
a
i
=
∑
j
=
1
i
d
j
a_{i} = \sum_{j=1}^{i}d_{j}
ai=j=1∑idj
对一个
a
a
a 数组
1
5
3
7
2
8
(1)
\begin{matrix} 1 & 5 & 3 & 7 & 2 & 8 \end{matrix} \tag{1}
153728(1)
存在
b
b
b 数组,使得
b
i
=
a
i
−
a
i
−
1
b_{i} = a_{i} - a_{i - 1}
bi=ai−ai−1
则
b
b
b 数组是
a
a
a 数组的差分数组
b
i
=
a
i
−
a
i
−
1
b
i
−
1
=
a
i
−
1
−
a
i
−
2
.
.
.
b
1
=
a
1
b_{i} = a_{i} - a_{i - 1}\\ b_{i - 1} = a_{i - 1} - a_{i - 2}\\ ...\\ b_{1} = a_{1}
bi=ai−ai−1bi−1=ai−1−ai−2...b1=a1
对于
(
1
)
(1)
(1) ,
b
b
b 数组为
1
4
−
2
4
−
5
6
(2)
\begin{matrix} 1 & 4 & -2 & 4 & -5 & 6 \end{matrix}{\tag 2}
14−24−56(2)
那么 对他们进行累加,可以得到
b
1
+
b
2
+
.
.
.
+
b
i
=
a
i
b_{1} + b_{2} + ... + b_{i} = a_{i}
b1+b2+...+bi=ai
要对 a$ 数组在区间为
[
l
,
r
]
[ l,r ]
[l,r] 中每个数加上
k
k
k
则
b
l
=
b
l
+
k
b
r
+
1
=
b
r
+
1
−
k
b_{l} = b_{l} + k \\ b_{r + 1} = b_{r + 1} - k
bl=bl+kbr+1=br+1−k
例如对
(
1
)
(1)
(1) 数组进行操作
对区间
[
2
,
4
]
[ 2, 4 ]
[2,4] 每个数都加
1
1
1
1
6
4
8
3
8
(3)
\begin{matrix} 1 & 6 & 4 & 8 & 3 & 8 \end{matrix} \tag{3}
164838(3)
而对
b
2
+
1
b2 + 1
b2+1,对
b
5
−
1
b5 - 1
b5−1 之后,
b
b
b 数组变成
1
5
−
2
4
−
5
5
(4)
\begin{matrix} 1 & 5 & -2 & 4 & -5 & 5 \end{matrix}{\tag4}
15−24−55(4)
该结果与再对
(
3
)
(3)
(3) 进行求差分数组结果一致
即对 ( 4 ) (4) (4) 求前缀和所得的结果就是 ( 3 ) (3) (3)
也就是说,要在 a a a 数组一定区间内进行增减,只需要更改 差分数组中 左端点的值 和 右端点后一位 的值
再对更改后的差分数组进行求前缀和,即可得到更改后的 a a a 数组
地毯
暴力做法
#include<bits/stdc++.h>
using namespace std;
const int N = 1050;
int a[N][N], num[N][N];
int n, m;
int x1, y11, x2, y2;
int main()
{
cin >> n >> m;
for(int j = 1; j <= m; j++)
{
cin >> x1 >> y11 >> x2 >> y2;
for(int i = x1; i <= x2; i ++)
for(int k = y11; k <= y2; k++)
num[i][k] ++;
}
for(int j = 1; j <= n; j++)
{
for(int i = 1; i <= n; i++)
{
cout << num[j][i] << " ";
}
cout << endl;
}
return 0;
}
利用差分
当一块区域上有地毯,则对差分数组中左上角的点 + 1 + 1 +1,右下角的点的右侧一位 − 1 - 1 −1
再对差分数组进行求前缀和,则可以得到结果
合格数
时空限制 1 s / 256 M B 1s / 256MB 1s/256MB
利用 差分 + 前缀和
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 2e5 + 10;
int n, k, q;
int s[N], m[N];
//s 来表示每个数被覆盖了多少次,m 表示前面一共有多少个合格数(前缀和)
int main()
{
cin >> n >> k >> q;
for(int i = 1; i <= n; i ++)
{
int l, r;
scanf("%d%d", &l, &r);
s[l] += 1;
s[r + 1] -= 1;
//差分
}
for(int i = 1; i <= N; i ++)
s[i] += s[i - 1];
//差分的前缀和就是原数组
for(int i = 1; i <= N; i ++)
{
m[i] = m[i - 1];
if(s[i] >= k) m[i] ++;
//如果当前端点满足,合格数数量加一
}
while(q --)
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", m[r] - m[l - 1]);
//求出前缀和 O(1)
}
return 0;
}