蓝桥杯AcWing学习笔记 2-2前缀和的学习(附相关蓝桥真题:K倍区间)(Java)

有参加蓝桥杯的同学可以给博主点个关注,博主也在准备蓝桥杯,可以跟着博主的博客一起刷题。

蓝桥杯

我的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[i1]+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[L1]

可以发现,当我们预处理完一个s之后,我们再去算某一段的和其实非常容易,直接算两个数的差就可以了,也就是我们在每一次算和的时候只用一次运算就可以了,每一次查询都优化了时间复杂度: O ( N ) → O ( 1 ) O(N) → O(1) O(N)O(1)

二维前缀和

我们二维前缀和有没有像一维前缀和那样的公式呢?

在一维中,我们前缀和数组每一个s[i]表示的是原数组前i个数的和;

在二维中,我们前缀和矩阵的这个数就代表着它在原矩阵中左上角所有元素的和:

image-20220211175756851

所有数全部替换为前缀和矩阵:

image-20220211180054249

第一个问题:前缀和矩阵怎么算出来?我们怎么通过原矩阵算出来?

容斥原理

什么是容斥原理呢?看图:

我们的目标是求这六个数的和:

image-20220211183329447

那么我们可以先求这左面四个数的和:

image-20220211192129092

然后再求上面三个数的和:

image-20220211192212811

现在的和是:黄色的格子被算了一次,蓝色的格子被算了两次,所以我们只需要减去所有蓝色的格子,就可以保证我们除了最后一个本身数的格子以外,所有数都只被加了一次,这样我们再加上此数字就可以得到前缀和矩阵的值。

image-20220211192514139

我们总结了一下,前缀和矩阵的值 = 此格子上面所有数的总和 + 此格子左面所有数的总和 - 重复的格子 + 原数

即为: 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[x1][y]+s[x][y1]s[x1][y1]+a[x][y]

我们可以在线性的时间复杂度之内,通过原矩阵算出前缀和矩阵的值,我们只用了常数次计算。

第二个问题:如何利用前缀和矩阵,计算某一个子矩阵的和?

同样:容斥原理

比方我们想算下图红色子矩阵的和:

image-20220211230559631

但我们现在只知道黄色矩阵的值:

image-20220211230632841

我们需要去掉“倒L”形状的值,这个就可以用容斥原理来去掉,首先去掉左面所有的值,然后再去掉上面的值;

现在,黄色格子我们已经全部去掉了,但是灰色格子我们去掉了两次,所以我们还要再加上它一次。

image-20220211231109806

最终公式:以(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,y11]s[x11,y2]+s[x11,y11]

我们利用前缀合矩阵以常数次计算快速的求出我们某一个子矩阵的和,时间复杂度优化为: O ( m n ) → O ( 1 ) O(mn) → O(1) O(mn)O(1)

二维前缀和主要是对一维前缀和的扩展。

容斥原理就是用空间换时间的概念。

前缀和的思想很重要,在很多的题目都会用到。

例题

AcWing 795. 前缀和

一维前缀和模板题

暴力的话每一次查询用时 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]); // 公式二
        }
    }
}

AcWing 796. 子矩阵的和

二维前缀和模板题

输入样例:

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行就是矩阵的元素:

image-20220211173656695

第5行的1 1 2 2求的是这个子矩阵的和:

image-20220211174118062

第6行的2 1 3 4求的是这个子矩阵的和:

image-20220211174426195

第7行的1 3 3 4求的是这个子矩阵的和:

image-20220211174623318

也就是说这个题是求一个矩阵的某一个子矩阵的和。

image-20220211174746393

那就直接按照我们前面讲的二维前缀和写代码就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]); // 公式二
        }
    }
}

第八届2017年蓝桥杯真题

AcWing 1230. K倍区间

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评测结果

image-20220212163523086

蓝桥杯评测结果

image-20220212163505479

用前缀和干掉了一层循环,但并没有达到我们想要的结果,那我们从哪方面还可以优化呢?

前缀和 + 数学 优化(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] % ks[L] % k的余数相同的个数,而我们的前缀和一般式不包含s[0]这个东西的,因为它的值是0,在前缀和中没有意义,但是这道题有意义,样例里面前缀和序列%k之后是1 1 0 0 1,两两比较,我们只能找到四个,如下图:

不加cnt[0] = 1res的值为4

image-20220212233612897

为什么少了两个?因为我们不一定需要两个序列,单个序列取余==0也构成k倍区间,此时我们就要假设s[0] = 0是有意义的;

我们cnt[0]中存的是s[]中等于0的数的个数,由于s[0] = 0,所以最初等于0的有1个数,所以cnt[0] = 1

加上cnt[0] = 1res的值为6

image-20220213001314923

蓝桥杯满分

image-20220213001532289

本题优化: O ( N 3 ) → O ( N 2 ) → O ( N ) O(N^3) → O(N^2) → O(N) O(N3)O(N2)O(N)

有对代码不理解的地方可以在下方评论

  • 53
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 47
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 47
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小成同学_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值