有参加蓝桥杯的同学可以给博主点个关注,博主也在准备蓝桥杯,可以跟着博主的博客一起刷题。
我的AcWing
题目及图片来自蓝桥杯C++ AB组辅导课
当我们想快速的求出来某一个静态数组(中间不会修改的)某一个区间内所有数的和可以用前缀和来处理。
假设一个数组a的区间为[L, R],全部遍历一遍的话时间复杂度是 O ( N ) O(N) O(N)
for (int i = L; i <= R; i++) {
res += a[i];
}
如果我们需要求很多次的话就可能会超时,那么前缀和就可以快速的让我们算出来某一个区间内所有数的和。
思想:
我们重新预处理一个新的数组:s[i]= a[1] + a[2] + … + a[i],s[i]是原数组前i个数的和,特殊规定s[0] = 0;处理s是很快的,因为我们可以发现 s [ i ] = s [ i − 1 ] + a [ i ] ( 公 式 一 ) s[i] = s[i - 1] + a[i](公式一) s[i]=s[i−1]+a[i](公式一)s[i]可以通过循环一遍就得到:
for (int i = 1; i < n; i++) {
s[i] = s[i - 1] + a[i];
}
当我们有了s之后,我们想算L到R的和,就可以用s来算了: a [ L ] + a [ L + 1 ] + a [ L + 2 ] + . . . + a [ R ] = s [ R ] − s [ L − 1 ] ( 公 式 二 ) a[L] + a[L + 1] + a[L + 2] + ... + a[R] = s[R] - s[L - 1](公式二) a[L]+a[L+1]+a[L+2]+...+a[R]=s[R]−s[L−1](公式二)
可以发现,当我们预处理完一个s之后,我们再去算某一段的和其实非常容易,直接算两个数的差就可以了,也就是我们在每一次算和的时候只用一次运算就可以了,每一次查询都优化了时间复杂度: O ( N ) → O ( 1 ) O(N) → O(1) O(N)→O(1)
我们二维前缀和有没有像一维前缀和那样的公式呢?
在一维中,我们前缀和数组每一个s[i]表示的是原数组前i个数的和;
在二维中,我们前缀和矩阵的这个数就代表着它在原矩阵中左上角所有元素的和:
所有数全部替换为前缀和矩阵:
第一个问题:前缀和矩阵怎么算出来?我们怎么通过原矩阵算出来?
容斥原理
什么是容斥原理呢?看图:
我们的目标是求这六个数的和:
那么我们可以先求这左面四个数的和:
然后再求上面三个数的和:
现在的和是:黄色的格子被算了一次,蓝色的格子被算了两次,所以我们只需要减去所有蓝色的格子,就可以保证我们除了最后一个本身数的格子以外,所有数都只被加了一次,这样我们再加上此数字就可以得到前缀和矩阵的值。
我们总结了一下,前缀和矩阵的值 = 此格子上面所有数的总和 + 此格子左面所有数的总和 - 重复的格子 + 原数
即为: 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](公式一) s[x][y]=s[x−1][y]+s[x][y−1]−s[x−1][y−1]+a[x][y](公式一)
我们可以在线性的时间复杂度之内,通过原矩阵算出前缀和矩阵的值,我们只用了常数次计算。
第二个问题:如何利用前缀和矩阵,计算某一个子矩阵的和?
同样:容斥原理
比方我们想算下图红色子矩阵的和:
但我们现在只知道黄色矩阵的值:
我们需要去掉“倒L”形状的值,这个就可以用容斥原理来去掉,首先去掉左面所有的值,然后再去掉上面的值;
现在,黄色格子我们已经全部去掉了,但是灰色格子我们去掉了两次,所以我们还要再加上它一次。
最终公式:以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为: 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[x2, y2] - s[x2, y1 - 1] - s[x1 - 1, y2] + s[x1 - 1, y1 - 1] (公式二) s[x2,y2]−s[x2,y1−1]−s[x1−1,y2]+s[x1−1,y1−1](公式二)
我们利用前缀合矩阵以常数次计算快速的求出我们某一个子矩阵的和,时间复杂度优化为: O ( m n ) → O ( 1 ) O(mn) → O(1) O(mn)→O(1)
二维前缀和主要是对一维前缀和的扩展。
容斥原理就是用空间换时间的概念。
前缀和的思想很重要,在很多的题目都会用到。
一维前缀和模板题
暴力的话每一次查询用时 O ( n ) O(n) O(n),一共m次,总共时间复杂度 O ( m n ) O(mn) O(mn) n,m <= 100000 mn = 1e10,超时。
前缀和优化,每一次查询用时 O ( 1 ) O(1) O(1),一共m次,总共 O ( n ) O(n) O(n)。
直接套模板即可。
import java.util.Scanner;
public class Main {
static final int N = 100010;
static int n, m;
static int[] a = new int[N]; // 原数组
static int[] s = new int[N]; // 前缀和数组 定义为全局变量 数组值全部初始化为0 不需要单独的s[0] = 0
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
s[i] = s[i - 1] + a[i]; // 公式一
}
while (m-- != 0) {
int l = sc.nextInt();
int r = sc.nextInt();
System.out.println(s[r] - s[l - 1]); // 公式二
}
}
}
二维前缀和模板题
输入样例:
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
第1行的3 4 3,前面两个3 4是矩阵的行列,最后一个3是要查询几次
234行就是矩阵的元素:
第5行的1 1 2 2求的是这个子矩阵的和:
第6行的2 1 3 4求的是这个子矩阵的和:
第7行的1 3 3 4求的是这个子矩阵的和:
也就是说这个题是求一个矩阵的某一个子矩阵的和。
那就直接按照我们前面讲的二维前缀和写代码就ok了,上面已经讲的非常细了。
import java.util.Scanner;
public class Main {
static final int N = 1010;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int q = sc.nextInt();
int[][] a = new int[N][N];
int[][] s = new int[N][N];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
a[i][j] = sc.nextInt();
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j]; // 公式一
}
}
while (q-- != 0) {
int x1 = sc.nextInt();
int y1 = sc.nextInt();
int x2 = sc.nextInt();
int y2 = sc.nextInt();
System.out.println(s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]); // 公式二
}
}
}
JavaB组第10题
一维前缀和
某一段连续的区间是k的倍数
我们看第一个样例:
输入样例:
5 2
1
2
3
4
5
输出样例:
6
求5个数某一段连续的区间是2的倍数有多少
长度是1:2,4 2个
长度是2: 0个
长度是3:[1, 3] [3, 5] 2个
长度是4:[1, 4] [2, 5] 2个
长度是5: 0个
总和是6,输出6。
暴力枚举(超时)
最朴素做法
时间复杂度
O ( N 3 ) O(N^3) O(N3)
超时,AcWing只过了6/11个数据,蓝桥杯过了2/7个数据,满分100分我只拿到了28分
import java.util.Scanner;
public class Main {
static final int N = 100010;
static int[] a = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int k = sc.nextInt();
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
}
int res = 0;
for (int r = 1; r <= n; r++) { // 右端点
for (int l = 1; l <= r; l++) { // 左端点
int sum = 0;
for (int i = l; i <= r; i++) {
sum += a[i];
}
if (sum % k == 0) res++;
}
}
System.out.print(res);
}
}
前缀和优化(超时)
时间复杂度
O ( N 2 ) O(N^2) O(N2)
但并没有什么用,AcWing还是超时,蓝桥杯的测评结果跟暴力枚举拿的分一样,我以为能骗到更多的分,这就很懵了
import java.util.Scanner;
public class Main {
static final int N = 100010;
static int[] a = new int[N]; // 原数组
static int[] s = new int[N]; // 前缀和数组
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int k = sc.nextInt();
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
s[i] = s[i - 1] + a[i]; // 公式一
}
int res = 0;
for (int r = 1; r <= n; r++) { // 右端点
for (int l = 1; l <= r; l++) { // 左端点
int sum = s[r] - s[l - 1]; // 公式二
if (sum % k == 0) res++;
}
}
System.out.print(res);
}
}
AcWing评测结果
蓝桥杯评测结果
用前缀和干掉了一层循环,但并没有达到我们想要的结果,那我们从哪方面还可以优化呢?
前缀和 + 数学 优化(AC)
时间复杂度
O ( N ) O(N) O(N)
我们可以再干掉一层循环。
当R
固定时,在1 ~ R
之间找到有多少个L
满足(s[R] - s[L - 1]) % k == 0
这其实就是我们第二层循环的含义,我们将循环条件简单的变一下:
当R
固定时,在0 ~ R - 1
之间找到有多少个L
满足(s[R] - s[L]) % k == 0
,可转换为:s[R] % k == s[L] % k
即有多少个S[L]
与S[R]
的余数相同
我们可以开一个新的数组,cnt[i]
,表示余数是i
的数有多少个
for(int R = 1; R <= n; R++) {
res += cnt[s[R] % k];
cnt[s[R] % k]++;
}
完整代码 AcWing AC 蓝桥杯满分
时间复杂度
O ( N ) O(N) O(N)
import java.util.Scanner;
public class Main {
static final int N = 100010;
static long[] s = new long[N]; // 前缀和数组 要开成long 防止爆int
static int[] cnt = new int[N]; // 存每个余数的个数数组
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int k = sc.nextInt();
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + sc.nextInt(); // 公式一 直接一步到位 不需要原数组存值
}
long res = 0;
cnt[0] = 1; // cnt[0]中存的是s[]中等于0的数的个数 由于s[0] = 0 所以最初等于0的有1个数 所以cnt[0] = 1 详见下图
for (int r = 1; r <= n; r++) { // 数学思想优化公式二
/*
推荐输出以下语句:能看到以r结尾的k倍区间个数
1 0 --> 1%2=1 前面有0个1 res+0
2 1 --> 3%2=1 前面有1个1 res+1
3 1 --> 6%2=0 前面有1个0 res+1
4 2 --> 10%2=0 前面有2个0 res+2
5 2 --> 15%2=1 前面有2个1 res+2
*/
//System.out.println(r + " " + cnt[(int)(s[r] % k)]);
res += cnt[(int)(s[r] % k)];
cnt[(int)(s[r] % k)]++; // 记录序列%k不同余数的个数
}
System.out.print(res);
}
}
cnt[0]
为什么要赋值为1?
因为我们的思路是找到两个序列和s[R] % k
和s[L] % k
的余数相同的个数,而我们的前缀和一般式不包含s[0]
这个东西的,因为它的值是0,在前缀和中没有意义,但是这道题有意义,样例里面前缀和序列%k之后是1 1 0 0 1,两两比较,我们只能找到四个,如下图:
不加cnt[0] = 1
,res
的值为4
为什么少了两个?因为我们不一定需要两个序列,单个序列取余==0也构成k倍区间,此时我们就要假设s[0] = 0
是有意义的;
我们cnt[0]
中存的是s[]
中等于0的数的个数,由于s[0] = 0
,所以最初等于0的有1个数,所以cnt[0] = 1
。
加上cnt[0] = 1
,res
的值为6
蓝桥杯满分
本题优化: O ( N 3 ) → O ( N 2 ) → O ( N ) O(N^3) → O(N^2) → O(N) O(N3)→O(N2)→O(N)
有对代码不理解的地方可以在下方评论