目录
引言
昨天写二分题写的我非常的郁闷,差点否认了自己,幸好昨天死磕找回了信心,今天做前缀和使信心大增,感觉还是很好的,所以只要坚持下来还是会有很好的结果的。加油!本篇博客讲的是前缀和的知识,只要基本概念清楚题目都比较简单,也没什么弯弯绕绕的,想说还是拼的是记忆力和毅力,加油!
概念
前缀和:
使用 O ( N ) O(N) O(N) 的时间复杂度去预处理,用 O ( 1 ) O(1) O(1) 的时间复杂度求出 [ l , r ] [l,r] [l,r] 的区间和,具体公式为 s [ r ] − s [ l − 1 ] s[r] - s[l-1] s[r]−s[l−1], 需要详解的可以参考我之前写过的博客前缀和与差分。
一、壁画
标签:前缀和
思路:
这道题首先我一看这个壁画首先要连续,所以说我想无非就是下图的这三种情况,然后我看这三种情况都满足题目说的条件:"必须与一段没有摧毁的墙壁相连",我想求的是最优情况,那么只要是从两边开始连续摧毁肯定是满足的,所以就是求最优的一段区间的和,那么就是前缀和了。然后第一时间想到的肯定是枚举所有的区间,然后根据要求可得,区间的长度为 l e n g t h = ( n + 2 − 1 ) / 2 length = (n + 2 - 1) / 2 length=(n+2−1)/2,也就是上取整,关于上取整可以参考我之前的博客算法竞赛常用的库函数中的 c m a t h cmath cmath 模块,然后就可以用 O ( N ) O(N) O(N) 的时间复杂度来遍历所有的可能,最后取最大值即可。
题目描述:
cpp
Thanh 想在一面被均分为 N 段的墙上画一幅精美的壁画。
每段墙面都有一个美观评分,这表示它的美观程度(如果它的上面有画的话)。
不幸的是,由于洪水泛滥,墙体开始崩溃,所以他需要加快他的作画进度!
每天 Thanh 可以绘制一段墙体。
在第一天,他可以自由的选择任意一段墙面进行绘制。
在接下来的每一天,他只能选择与绘制完成的墙面相邻的墙段进行作画,因为他不想分开壁画。
在每天结束时,一段未被涂颜料的墙将被摧毁(Thanh 使用的是防水涂料,因此涂漆的部分不能被破坏),且被毁掉的
墙段一定只与一段还未被毁掉的墙面相邻。
Thanh 的壁画的总体美观程度将等于他作画的所有墙段的美观评分的总和。
Thanh想要保证,无论墙壁是如何被摧毁的,他都可以达到至少 B 的美观总分。
请问他能够保证达到的美观总分 B 的最大值是多少。
输入格式
第一行包含整数 T,表示共有 T 组测试数据。
每组数据的第一行包含整数 N。
第二行包含一个长度为 N 的字符串,字符串由数字 0∼9 构成,第 i 个字符表示第 i 段墙面被上色后能达到的美观评分。
输出格式
每组数据输出一个结果,每个结果占一行。
结果表示为 Case #x: y,其中 x 为组别编号(从 1 开始),y 为 Thanh 可以保证达到的美观评分的最大值。
数据范围
1≤T≤100,存在一个测试点N=5∗106,其他测试点均满足2≤N≤100
输入样例:
4
4
1332
4
9583
3
616
10
1029384756
输出样例:
Case #1: 6
Case #2: 14
Case #3: 7
Case #4: 31
样例解释
在第一个样例中,无论墙壁如何被破坏,Thanh都可以获得 6 分的美观总分。在第一天,他可以随便选一个美观评分3的
墙段进行绘画。在一天结束时,第一部分或第四部分将被摧毁,但无论哪一部分都无关紧要。在第二天,他都可以在另一
段美观评分 3 的墙段上作画。
在第二个样例中,Thanh 在第一天选择最左边的美观评分为 9 的墙段上作画。在第一天结束时唯一可以被毁掉的墙体是最
右边的那段墙体,因为最左边的墙壁被涂上了颜料。在第二天,他可以选择在左数第二段评分为 5 的墙面上作画。然后
右数第二段墙体被摧毁。请注意,在第二天,Thanh不能选择绘制第三段墙面,因为它不与任何其他作画墙面相邻。这样
可以获得 14 分的美观总分。
示例代码:
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5e6+10;
string str;
int T, n;
int s[N];
int cnt = 1;
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> T;
while(T--)
{
cin >> n;
cin >> str;
for(int i = 1; i <= n; ++i) s[i] = s[i-1] + str[i-1] - '0';
int c = (n + 1) / 2;
int res = 0;
for(int i = 1; i + c - 1 <= n; ++i)
{
int j = i + c - 1;
res = max(res, s[j] - s[i-1]);
}
printf("Case #%d: %d\n", cnt++, res);
}
return 0;
}
二、前缀和
标签:前缀和、模板题
思路:
就是一个模板题,练练手。
题目描述:
cpp
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式
共 m 行,每行输出一个询问的结果。
数据范围
1≤l≤r≤n,1≤n,m≤100000,−1000≤数列中元素的值≤1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10
示例代码:
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+10;
int n, m;
int s[N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; ++i)
{
cin >> s[i];
s[i] += s[i-1];
}
while(m--)
{
int l, r;
cin >> l >> r;
cout << s[r] - s[l-1] << endl;
}
return 0;
}
三、子矩阵的和
标签:前缀和、模板题
思路:
模板题没什么说的,用来熟悉熟悉练练手。
题目描述:
输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角
坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数 n,m,q。
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。
输出格式
共 q 行,每行输出一个询问的结果。
数据范围
1≤n,m≤1000,1≤q≤200000,1≤x1≤x2≤n,1≤y1≤y2≤m,−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21
示例代码:
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int n, m, q;
int s[N][N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m >> q;
for(int i = 1; i <= n; ++i)
{
for(int j = 1; j <= m; ++j)
{
cin >> s[i][j];
s[i][j] += s[i][j-1] + s[i-1][j] - s[i-1][j-1];
}
}
while(q--)
{
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
cout << s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] << endl;
}
return 0;
}
四、K倍区间
标签:前缀和
思路1:
首先这道题第一反应就是直接暴力枚举每个子区间,然后计算符合条件的,时间复杂度为 O ( N 2 ) O(N^2) O(N2), N = 1 0 5 N = 10^5 N=105, 所以直接做肯定会超时,只能过一半的数据。
思路2:
我们可以从公式入手 s [ j ] − s [ i − 1 ] = k s[j] - s[i-1] = k s[j]−s[i−1]=k,即 s [ j ] ≡ s [ i − 1 ] ( m o d k ) s[j] \equiv s[i-1] \pmod k s[j]≡s[i−1](modk),这里的 j j j 为右边界, i i i 为左边界,并且 i ∈ [ 1 , j ] i \in [1,j] i∈[1,j],所以 i − 1 ∈ [ 0 , j − 1 ] i-1 \in [0,j-1] i−1∈[0,j−1],所以我们只用算在 j j j 之前的跟 s [ j ] m o d k s[j] \mod k s[j]modk的余数相同的 s [ i − 1 ] s[i-1] s[i−1] 的数量即可。那么我们可以从前往后求出 s [ i − 1 ] s[i-1] s[i−1] 的余数,用一个数组当作哈希表,下标即为余数,元素为个数,这样就可以了,注意因为 i − 1 ∈ [ 0 , j − 1 ] i-1 \in [0,j-1] i−1∈[0,j−1],所以预处理时 s [ 0 ] = 1 , c n t [ 0 ] = 1 s[0] = 1, cnt[0] = 1 s[0]=1,cnt[0]=1,因为定义是从 0 0 0 开始的,而 s [ 0 ] = 0 s[0] = 0 s[0]=0,所以当 j = 1 j = 1 j=1时, c n t [ 0 ] = 1 cnt[0] = 1 cnt[0]=1。
题目描述:
cpp
给定一个长度为 N 的数列,A1,A2,...AN,如果其中一段连续的子序列 Ai,Ai+1,...Aj 之和是 K 的倍数,我们就称这个区间
[i,j] 是 K 倍区间。
你能求出数列中总共有多少个 K 倍区间吗?
输入格式
第一行包含两个整数 N 和 K。
以下 N 行每行包含一个整数 Ai。
输出格式
输出一个整数,代表 K 倍区间的数目。
数据范围
1≤N,K≤100000,1≤Ai≤100000
输入样例:
5 2
1
2
3
4
5
输出样例:
6
示例代码1: 暴力 6/12
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+10;
int n, k;
LL s[N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; ++i)
{
cin >> s[i];
s[i] += s[i-1];
}
LL res = 0;
for(int i = 1; i <= n; ++i)
{
for(int j = i; j <= n; ++j)
{
if((s[j] - s[i-1]) % k == 0) res++;
}
}
cout << res << endl;
return 0;
}
示例代码2: 12/12
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+10;
int n, k;
LL s[N];
int cnt[N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; ++i)
{
cin >> s[i];
s[i] += s[i-1];
}
LL res = 0;
cnt[0]++;
for(int i = 1; i <= n; ++i)
{
res += cnt[s[i] % k];
cnt[s[i] % k]++;
}
cout << res << endl;
return 0;
}
五、统计子矩阵
标签:前缀和
思路1:
暴力枚举每个子矩阵,再判断条件,这里做了个优化,因为 A i j A_{ij} Aij 非负,所以一旦超出了 K K K 那就直接 b r e a k break break 即可。
思路2:
因为这道题是单调的,所以可以用双指针来做,可以将时间复杂度从 O ( N 4 ) O(N^4) O(N4) 降到 O ( N 3 ) O(N^3) O(N3), 因为 N , M ≤ 100 N,M \leq 100 N,M≤100,加上常数小并且真实的时间复杂度肯定是低于 O ( N 3 ) O(N^3) O(N3) 的,所以可以勉强通过。可以前缀和行,列用双指针枚举,即可,具体细节见代码。
题目描述:
cpp
给定一个 N×M 的矩阵 A,请你统计有多少个子矩阵 (最小 1×1,最大 N×M) 满足子矩阵中所有数的和不超过给定的整数
K?
输入格式
第一行包含三个整数 N,M 和 K。
之后 N 行每行包含 M 个整数,代表矩阵 A。
输出格式
一个整数代表答案。
数据范围对于 30% 的数据,N,M≤20,
对于 70% 的数据,N,M≤100,
对于 100% 的数据,1≤N,M≤500;0≤Aij≤1000;1≤K≤2.5×108。
输入样例:
3 4 10
1 2 3 4
5 6 7 8
9 10 11 12
输出样例:
19
样例解释
满足条件的子矩阵一共有 19,包含:
大小为 1×1 的有 10 个。大小为 1×2 的有 3 个。大小为 1×3 的有 2 个。大小为 1×4 的有 1 个。大小为 2×1 的有
3 个。
示例代码1: 暴力 7/10
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 510;
int n, m, k;
LL s[N][N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= n; ++i)
{
for(int j = 1; j <= m; ++j)
{
cin >> s[i][j];
s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
}
}
LL res = 0;
for(int x1 = 1; x1 <= n; ++x1)
{
for(int x2 = x1; x2 <= n; ++x2)
{
for(int y1 = 1; y1 <= m; ++y1)
{
for(int y2 = y1; y2 <= m; ++y2)
{
LL t = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
if(t <= k) res++;
else break; // 因为Aij非负
}
}
}
}
cout << res << endl;
return 0;
}
示例代码2: 双指针 10/10
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 510;
int n, m, k;
LL s[N][N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= n; ++i)
{
for(int j = 1; j <= m; ++j)
{
cin >> s[i][j];
s[i][j] += s[i-1][j]; // 至于处理行
}
}
LL res = 0;
for(int i = 1; i <= n; ++i)
{
for(int j = i; j <= n; ++j)
{
for(int l = 1, r = 1, sum = 0; r <= m; ++r)
{
sum += s[j][r] - s[i-1][r];
while(sum > k)
{
sum -= s[j][l] - s[i-1][l];
l++;
}
res += r - l + 1;
}
}
}
cout << res << endl;
return 0;
}
六、递增三元组
标签:二分、前缀和
思路1: 二分
要求 A i < B j < C k A_i < B_j < C_k Ai<Bj<Ck,可以分开来看,分别求出 A i < B j A_i < B_j Ai<Bj 和 B j < C k B_j < C_k Bj<Ck的个数,然后两个组合也就是相乘,最后把每个 B j B_j Bj 的结果相加即可。怎么求 A i < B j A_i < B_j Ai<Bj 中 A i A_i Ai 的个数呢,可以把 A i A_i Ai 排好序,然后用二分来求最后一个小于 B j B_j Bj 的下标,即可知道数量,同理 B j < C k B_j < C_k Bj<Ck 的数量也可通过二分来求出来,详情见代码。
思路2:前缀和:
整体思路就是定义一个 c n t [ i ] cnt[i] cnt[i] ,意为在 A [ i ] A[i] A[i] 中元素为 i i i 的个数,然后用前缀和 s [ i ] s[i] s[i] 一统计,意为在 A A A 中的元素小于等于 i i i 的元素个数,然后再定义一个数组 a s [ i ] as[i] as[i] ,遍历每一个 b j b_j bj ,求出在 A A A 中小于 b j b_j bj 的个数,同理可求出 c s [ i ] cs[i] cs[i] ,意为对于每个 b j b_j bj ,在 C C C 中大于 b j b_j bj 的元素个数,最后遍历每个 b j b_j bj , r e s = r e s + a s [ j ] ∗ c s [ j ] res = res + as[j] * cs[j] res=res+as[j]∗cs[j]。
题目描述:
cpp
给定三个整数数组
A=[A1,A2,...AN],
B=[B1,B2,...BN],
C=[C1,C2,...CN],
请你统计有多少个三元组 (i,j,k) 满足:
1≤i,j,k≤N
Ai<Bj<Ck
输入格式
第一行包含一个整数 N。
第二行包含 N 个整数 A1,A2,...AN。
第三行包含 N 个整数 B1,B2,...BN。
第四行包含 N 个整数 C1,C2,...CN。
输出格式
一个整数表示答案。
数据范围
1≤N≤105,0≤Ai,Bi,Ci≤105
输入样例:
3
1 1 1
2 2 2
3 3 3
输出样例:
27
示例代码1: 二分
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+10;
int n;
int a[N], b[N], c[N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i];
for(int i = 1; i <= n; ++i) cin >> b[i];
for(int i = 1; i <= n; ++i) cin >> c[i];
sort(a+1, a+1+n);
sort(c+1, c+1+n);
LL res = 0;
for(int i = 1; i <= n; ++i)
{
int key = b[i];
int r1 = 0, r2 = 0;
int l = 1, r = n;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(a[mid] < key) l = mid;
else r = mid - 1;
}
if(a[r] < key) r1 = r;
l = 1, r = n;
while(l < r)
{
int mid = l + r >> 1;
if(c[mid] > key) r = mid;
else l = mid + 1;
}
if(c[r] > key) r2 = n - r + 1;
res += (LL)r1 * r2;
}
cout << res << endl;
return 0;
}
示例代码2: 前缀和
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+10;
int n;
int a[N], b[N], c[N];
int as[N], cs[N];
int s[N], cnt[N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i], a[i]++;
for(int i = 1; i <= n; ++i) cin >> b[i], b[i]++;
for(int i = 1; i <= n; ++i) cin >> c[i], c[i]++;
for(int i = 1; i <= n; ++i) cnt[a[i]]++;
for(int i = 1; i < N; ++i) s[i] = s[i-1] + cnt[i];
for(int i = 1; i <= n; ++i) as[i] = s[b[i] - 1];
memset(s, 0, sizeof s);
memset(cnt, 0, sizeof cnt);
for(int i = 1; i <= n; ++i) cnt[c[i]]++;
for(int i = 1; i < N; ++i) s[i] = s[i-1] + cnt[i];
for(int i = 1; i <= n; ++i) cs[i] = s[N - 1] - s[b[i]];
LL res = 0;
for(int i = 1; i <= n; ++i) res += (LL)as[i] * cs[i];
cout << res << endl;
return 0;
}
七、激光炸弹
标签:前缀和
思路:
这道题其实数据范围允许,直接拿前缀和枚举每一个子矩阵即可,详情见代码。注:在写前缀和的时候尽量把前缀和矩阵 s s s 当成初始矩阵,不要再开一个初始矩阵 a a a 了,因为这道题这样出现了 M e m o r y L i m i t E x c e e d e d Memory\ Limit\ Exceeded Memory Limit Exceeded ,所以还是要注意点。
题目描述:
地图上有 N 个目标,用整数 Xi,Yi 表示目标在地图上的位置,每个目标都有一个价值 Wi。
注意:不同目标可能在同一位置。
现在有一种新型的激光炸弹,可以摧毁一个包含 R×R 个位置的正方形内的所有目标。
激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 x,y 轴平行。
求一颗炸弹最多能炸掉地图上总价值为多少的目标。
输入格式
第一行输入正整数 N 和 R,分别代表地图上的目标数目和正方形包含的横纵位置数量,数据用空格隔开。
接下来 N 行,每行输入一组数据,每组数据包括三个整数 Xi,Yi,Wi,分别代表目标的 x 坐标,y 坐标和价值,数据用空格隔开。
输出格式
输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。
数据范围
0≤R≤109,0<N≤10000,0≤Xi,Yi≤5000,0≤Wi≤1000
输入样例:
2 1
0 0 1
1 1 1
输出样例:
1
示例代码:
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5010;
int n, r;
int s[N][N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> r;
r = min(r, 5001);
while(n--)
{
int x, y, z;
cin >> x >> y >> z;
x++, y++;
s[x][y] += z;
}
for(int i = 1; i < N; ++i) // 预处理前缀和
{
for(int j = 1; j < N; ++j)
{
s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
}
}
LL res = 0;
for(int x1 = 1; x1 + r - 1 <= 5001; ++x1)
{
for(int y1 = 1; y1 + r - 1 <= 5001; ++y1)
{
int x2 = x1 + r - 1, y2 = y1 + r - 1;
res = max(res, (LL)s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]);
}
}
cout << res << endl;
return 0;
}