蓝桥杯JAVA-28.前缀和与差分详解

2023-11-05

个人博客
www.tothefor.com
蓝桥杯复习知识点汇总

目录

开始之前,推荐先看一下总结。再看内容。也许会帮你更好的理解。

前缀和是指某序列的前n项和,可以把它理解为数学上的数列的前n项和,而差分可以看成前缀和的逆运算。合理的使用前缀和与差分,可以将某些复杂的问题简单化。

1.前缀和

首先,看一个问题:

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

这个问题,最容易想到的就是暴力了。但当数大了之后,时间复杂度就比较高了。

1.1 一维前缀和

首先明白两个数组,一个是原数组a,一个是前缀和数组sum。

  • 前缀和数组的定义:sum[i]=a[1]+a[2]+a[3]+…+a[i],即为前i个数的和:
    假设的是a[1]=1,a[2]=2…a[i]=i。
    sum[1]=a[1]; //1
    sum[2]=a[1]+a[2]; //3
    sum[3]=a[1]+a[2]+a[3]; //6
    sum[4]=a[1]+a[2]+a[3]+a[4]; //10
    sum[5]=a[1]+a[2]+a[3]+a[4]+a[5]; //15

    sum[i]=a[1]+a[2]+a[3]+…+a[i]。

假如我们要求[2,5]区间的和,怎么求?看上面列出来的。

是不是直接用sum[5]-sum[1]即可。因为:a[1]+a[2]+a[3]+a[4]+a[5]-a[1]=a[2]+a[3]+a[4]+a[5]。

并且sum[5]=sum[4]+a[5]的。即sum[i]=sum[i-1]+a[i]的。

所以,如果求[l,r]的区间和,则直接用:sum[r]-sum[l-1]即可。

原理

sum[r] =a[1]+a[2]+a[3]+a[l-1]+a[l]+a[l+1]…a[r];
sum[l-1]=a[1]+a[2]+a[3]+a[l-1];
sum[r]-sum[l-1]=a[l]+a[l+1]+…+a[r];

右边会把相同的消掉。如图:

这样,对于每个询问,只需要执行 sum[r]-sum[l-1]。输出原序列中从第l个数到第r个数的和的时间复杂度变成了O(1)。

代码实现:

final int m = (int) 1e5 + 5;
int[] sum = new int[m];
int[] a = new int[m];
for (int i = 1; i <= n; i++) {
  sum[i] = sum[i - 1] + a[i];
}
int l, r; //待求区间[l,r]
l = Scin.nextInt();
r = Scin.nextInt();
System.out.println(sum[r]-sum[l-1]); //查询

这就是一维前缀和。

1.2 二维前缀和

二维前缀和和一维差不多,二维中的sum存的是一块区域的面积。这里为了方便sum数组就写成s数组了。

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

二维前缀和的sum数组求法

定义一个二维数组s[][], s[i][j]表示二维数组中,左上角(1,1)到右下角( i,j )所包围的矩阵元素的和。需要注意的是,这里的(1,1)根据自行情况而定。可能有的人是(0,0),或其他的。这里都统一以(1,1)开始。

如图:

其中,紫色面积是指(1,1)到(i,j-1)的矩形面积, 绿色面积是指(1,1)到(i-1, j )的矩形面积。每一个颜色的矩形面积都代表了它所包围元素的和。

从图中我们很容易看出,整个外围蓝色矩形面积s[i][j] = 绿色面积s[i-1][j] + 紫色面积s[i][j-1] - 重复加的红色的面积s[i-1][j-1]+小方块的面积a[i][j];

所以,得出二维前缀和sum数组的预处理公式:s[i] [j] = s[i-1][j] + s[i][j-1] + a[i] [j] - s[i-1][ j-1]

二维前缀和的区间求法

现在,sum数组已经处理好了。我们来求一下二维的区间和问题。求以(x1,y1)为左上角和以(x2,y2)为右下角的矩阵的元素的和为例。

先看一张区域分割图:

再看具体的计算:

紫色面积是指 ( 1,1 )到(x1-1,y2)的矩形面积 , 棕色面积是指(1,1)到(x2,y1-1)的矩形面积;不难得出:绿色矩形的面积 = 整个外围面积s[x2, y2] - 棕色面积s[x2, y1 - 1] - 紫色面积s[x1 - 1, y2] + 重复减去的红色面积 s[x1 - 1, y1 - 1]。

所以,以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]

这就是二维前缀和。

示例:

import java.io.*;
import java.util.*;

/**
 * @Author DragonOne
 * @Date 2021/9/4 19:40
 */
public class Main {
    public static void main(String[] args) throws Exception {
        Scanner Scin = new Scanner(System.in);
        StreamTokenizer STcin = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        BufferedReader BRcin = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));

        final int m = (int) 1e2 + 5;
        int[][] s = new int[m][m];
        int[][] a = new int[m][m];
        for (int i = 1; i <= 5; i++) {
            for (int j = 1; j <= 5; j++) {
                a[i][j] = i + j;
                s[i][j] = s[i - 1][j] + s[i][j - 1] + a[i][j] - s[i - 1][j - 1];
            }
        }
        System.out.println("a数组为:");
        for (int i = 1; i <= 5; i++) {
            for (int j = 1; j <= 5; j++) {
                System.out.print(a[i][j] + " ");
            }
            System.out.println();
        }
        System.out.println("s数组为:");
        for (int i = 1; i <= 5; i++) {
            for (int j = 1; j <= 5; j++) {
                System.out.print(s[i][j] + " ");
            }
            System.out.println();
        }
        System.out.println("(2,2)到(4,3)的矩阵和为:");
        int x1, x2, y1, y2;
        x1 = 2;
        y1 = 2;
        x2 = 4;
        y2 = 3;
        System.out.println("s[x2][y2]: " + s[x2][y2]);
        System.out.println("s[x1 - 1][y2]: " + s[x1 - 1][y2]);
        System.out.println("s[x2][y1 - 1]: " + s[x2][y1 - 1]);
        System.out.println("s[x1 - 1][y1 - 1]: " + s[x1 - 1][y1 - 1]);
        System.out.println(s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);

    }
}


输出:

a数组为:
2 3 4 5 6 
3 4 5 6 7 
4 5 6 7 8 
5 6 7 8 9 
6 7 8 9 10 
s数组为:
2 5 9 14 20 
5 12 21 32 45 
9 21 36 54 75 
14 32 54 80 110 
20 45 75 110 150 
(2,2)(4,3)的矩阵和为:
s[x2][y2]: 54
s[x1 - 1][y2]: 9
s[x2][y1 - 1]: 14
s[x1 - 1][y1 - 1]: 2
33

2.差分

差分可以看成前缀和的逆运算。类似于积分和微分。

2.1 一维差分

差分数组

已知一个原数组a:a[1], a[2], a[3], a[n];

然后构造一个数组b : b[1] ,b[2] , b[3], b[i];

使得 a[i] = b[1] + b[2 ]+ b[3] +, + b[i]

也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。换句话说,每一个a[i]都是b数组中从头开始的一段区间和。我们只要有b数组,通过前缀和运算,就可以在O(n) 的时间内得到a数组 。

至于考虑如何构造差分b数组。这个我们现在不需要关心,其实也没有必要关心如何构造b数组。往后面看就明白为什么了。

差分原理

首先,需要知道两个数组,一个原数组a,一个差分数组b。

首先明白的是,a数组是b数组的前缀和,b数组是a数组的差分数组。即b数组相当于是原数组,a数组是b数组的前缀和。

明白后,想一个问题。如果b[1]加上1,那么a数组怎么变?是不是a[1]及后面的所有数都会加上1。因为a数组是b数组的前缀和,a[i]存的是b的前i个数的和。

即:差分b数组中的 b[i] + c ,通过前缀和运算后,a数组变成 a[i] + c ,a[i+1] + c, a[n] + c;
最好自行在纸上画画。

好。明白上面后。单个修改没问题了。那我们再来算一个区间的。把a数组中的[ l, r]区间中的每一个数都加上c。

最容易想到的就是对b[l]+c,那么a[l]及其后面的数都加上了c。然后对b[r]+c吗?这不是就不对了嘛。
对a[r+1]-c即可,那么a[r+1]及其后面的都减去了c,就把前面加的抵消了。见图。

b[l] + c,效果使得a数组中 a[l]及以后的数都加上了c(红色部分),但我们只要求l到r区间加上c, 因此还需要执行 b[r+1] - c,让a数组中a[r+1]及往后的区间再减去c(绿色部分),这样对于a[r] 以后区间的数相当于没有发生改变。

一维差分结论:给a数组中的[ l, r]区间中的每一个数都加上c,只需对差分数组b做 b[l] + = c, b[r+1] - = c即可。

这就是一维差分数组。

差分数组的构造

前面说了,这个不需要我们关心的。只需要明白了上面的原理即可。因为,我们只需要把原数组a中的数也当成是要操作的数即可。

即,我们一开始可以把a数组和b数组都当成0,即a、b数组全是0。然后,对于每一个a数组中的数,我们都可以看成是一次修改,进而在b数组中进行操作。例如,a[1]=3,a[2]=10。即,在b数组的区间[1,1]中加上3,b数组的区间[2,2]中加上10。

所以,对于差分数组如何构造,我们无需知道。

示例:

import java.io.*;
import java.util.*;

/**
 * @Author DragonOne
 * @Date 2021/9/4 19:40
 */
public class Main {
    public static final int m = (int) 1e2 + 5;
    public static int[] b = new int[m];
    public static int[] a = new int[m];

    public static void main(String[] args) throws Exception {
        Scanner Scin = new Scanner(System.in);
        StreamTokenizer STcin = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        BufferedReader BRcin = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));

        for (int i = 1; i <= 10; i++) {
            a[i] = i;
        }
        System.out.println("a数组为:");
        for (int i = 0; i < 10; i++) {
            System.out.print(a[i + 1] + " ");
        }
        System.out.println();

        for (int i = 1; i <= 10; i++) {
            insert(i, i, a[i]);
        }
        System.out.println("b数组为:");
        for (int i = 1; i <= 10; i++) {
            System.out.print(b[i] + " ");
        }
        System.out.println();
    }

    public static void insert(int l, int r, int c) {
        b[l] += c;
        b[r + 1] -= c;
    }
}

输出:

a数组为:
1 2 3 4 5 6 7 8 9 10 
b数组为:
1 1 1 1 1 1 1 1 1 1 

当然了,如果非要自己去构造的话也是可以的,这里也提供一种构造方法。

如下:
a[0 ]= 0;
b[1] = a[1] - a[0];
b[2] = a[2] - a[1];
b[3] =a [3] - a[2];

b[n] = a[n] - a[n-1];

如图:

2.2 二维差分

同理在二维中,a[][]数组是b[][]数组的前缀和数组,那么b[][]是a[][]的差分数组。至于差分数组的构造在一维中已经说的很明白了。

问题:已知原数组a中被选中的子矩阵为 以(x1,y1)为左上角,以(x2,y2)为右上角所围成的矩形区域。

始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i][j]的修改,会影响到a数组中从a[i][j]及往后的每一个数。

原理

先看一张区域分割图:

再来看具体的分割:

  • b[x1][ y1 ] +=c ; 对应编号图1 ,让整个a数组中蓝色矩形面积的元素都加上了c。

  • b[x1][y2+1]-=c ; 对应编号图2 ,让整个a数组中绿色矩形面积的元素再减去c,使其内元素不发生改变。

  • b[x2+1][y1]- =c ; 对应编号图3 ,让整个a数组中紫色矩形面积的元素再减去c,使其内元素不发生改变。

  • b[x2+1][y2+1]+=c; 对应编号图4,让整个a数组中红色矩形面积的元素再加上c,红色内的相当于被减了两次,再加上一次c,才能使其恢复。

这里,再说一下为什么不用构造差分数组。因为我们可以先假想a数组为空,那么b数组一开始也为空,但是实际上a数组并不为空,因此我们每次让以(i,j)为左上角到以(i,j)为右上角面积内元素(其实就是一个小方格的面积)去插入 c=a[i][j],等价于原数组a中(i,j) 到(i,j)范围内 加上了 a[i][j] ,因此执行n * m次插入操作,就成功构建了差分b数组。

实现:

void insert(int x1,int y1,int x2,int y2,int c)
{     //对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
    b[x1][y1]+=c;
    b[x2+1][y1]-=c;
    b[x1][y2+1]-=c;
    b[x2+1][y2+1]+=c;
}

当然了,二维差分操作也有直接的构造方法,公式如下:
b[i][j]=a[i][j]−a[i−1][j]−a[i][j−1]+a[i−1][j−1]

构造同一维差分那里的思维相同。这里不再说了。

这就是二维差分。

3.总结

一句话:前缀和受影响的向前看,差分受影响的向后看。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

蓝桥杯JAVA-28.前缀和与差分详解 的相关文章

  • Objective-C 相当于 Java 枚举或“静态最终”对象

    我试图找到一个与 Java 枚举类型或 public static final 对象等效的 Objective C 例如 public enum MyEnum private String str private int val FOO f
  • 使用 Hibernate 用瞬态对象更新持久对象

    每天 数据都是通过网络服务导入的 我创建一个新的 暂时的 实例我通过 JPA 注释在 hibernate 中映射的 pojo 的 我将数据从 Web 服务填充到瞬态实例中 我从数据库加载持久对象 我想用瞬态实例中的数据更新该持久对象 我以某
  • 序列化 ArrayList

    我正在尝试编写一个 Android 游戏 即使用户想要返回主菜单或者活动被系统终止 我也希望能够暂停游戏 onSaveInstanceState 似乎并没有给我很大的控制权来决定何时可以读回捆绑包 而且据我所知 捆绑包仅在短时间内有效 所以
  • 合并 2 个 .jks 信任库文件

    我正在使用启用了 SSL 的 Tomcat 并使用信任库进行客户端身份验证 我有两个 jks trustore 文件 第一个 我将其用于 PROD 环境 另一个用于 TEST 环境客户端证书 我在 Tomcat 上部署了 Web 应用程序
  • 使用 s:select 标签在下拉菜单中使用 i18n [重复]

    这个问题在这里已经有答案了 我的 JSP 页面中有一个下拉菜单 它是通过
  • 如何将参数传递给Workmanager DoWork方法

    我想安排任务在 24 小时后从数据库中删除 public class WorkManager extends Worker public WorkManager NonNull Context context NonNull WorkerP
  • bean 的 CDI @TransactionAttribute

    我正在尝试CDI在测试应用程序上 我有一个DAO它注入一个托管的容器JTA像这样的持久化上下文 public class TestDAO implements Serializable PersistenceContext private
  • Jboss EAP 7 - 如何从部署中排除隐式模块(javax.jms)?

    我没想到我会来到这里 但经过大量 Google 和 StackOverflow 搜索后 我来到了这里 这就是我的确切问题 https www linkedin com pulse tale two jars marco antonio al
  • Byte[] 和 java.lang.OutOfMemoryError 按位读/写文件

    我正在努力擦除 Android 中的一些可用空间 这是我的代码 private void creatingFileDelete int size int passMode File lastFile new File Environment
  • 使用 Copy.CopyIntoItems Web 服务将文件上传到 SharePoint 2010 时收到 400 错误请求

    SharePoint 新手 我尝试使用 Java 的 CopyIntoItems Web 服务方法将文档上传到 SharePoint 但不断收到 400 错误请求 我使用 Java 的 wsimport 从 wsdl 文件生成类文件 这是我
  • 如何在 SpringBoot v3.0.0 中使用嵌入式 MongoDB?

    我正在尝试连接嵌入式 mongodb 并使用 MongoDbSpringIntegrationTest 对其进行测试 问题是相同的代码在 2 7 7 中适用于 spring boot 但在 3 0 0 中不适用于 spring boot 问
  • 字符串 a == 字符串 b 的规则 [重复]

    这个问题在这里已经有答案了 我试图了解字符串池的工作原理以及一个字符串等于另一个字符串的规则是什么 例如这个片段 public static void main String hi String s1 lol String s2 lol S
  • 是否有适用于 Java 的 CalDAV 客户端库? [关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 我想使用 CalDAV 协议与我的日
  • java内存不足然后退出

    我有一个必须分析大文件的软件 限制输入或提供无限内存都不是一个选择 所以我必须忍受飞行的 OOME 因为 OOME 只杀死线程 所以我的软件运行在一些糟糕的状态 从外面看一切都很好 因为进程正在运行 但在内部却是脑死亡 我想拔掉它的插头 但
  • 错误包括 bouncycastle 提供商

    我需要使用bouncycastle provider我的项目中的库 我已将其包含在 gradle 项目中 apply plugin application sourceCompatibility 1 6 version 1 0 0 main
  • 将序列化数据发送到 servlet 时出现 java.io.EOFException

    我正在尝试从 Java 本地应用程序上传一个包含文件到服务器的对象 我的计划是 在 tomcat 上运行的 servlet 将使用以下方法获取对象ObjectInputStream in the doGet方法 但我得到一个EOFExcep
  • 如何让 Camel FTP 按需只获取一次

    我对骆驼还很陌生 我一直在尝试让 Camel 根据需要仅通过 FTP 获取单个文件一次 我无法让它发挥作用 这是我尝试过的 让我知道什么是最好的方法以及我的代码有什么问题 1 读取文件后发送一条空消息当收到空消息时 停止路由 from di
  • 原子整数的compareandexchange()与compareandset()

    在研究 AtomicInteger 时 我发现这个 API 提供了两种方法 比较和交换 如果当前值被引用 则自动将该值设置为 newValue to 作为见证值 预期值 记忆效应为 由指定VarHandle compareAndExchan
  • 使用替换但不使用根元素的 Jaxb 继承

    我正在浏览布莱斯的博客http blog bdoughan com 2010 11 jaxb and inheritance using substitution html http blog bdoughan com 2010 11 ja
  • Android NDK - 仅用 C/C++ 编写

    有没有一种可能的方法可以使用 C C 编写整个 NDK 应用程序 而无需像 hello jni 示例项目 HelloJni java 中那样的 Java 入门 类 以某种方式创建一个 HelloJni c 来执行相同的操作 从 Androi

随机推荐