【算法】—— 前缀和

news2024/12/16 9:48:46

一、区间求和问题

给定一个长度为n的序列a,有m次查询,每次查询输出一个连续区间的和。

使用暴力做法求解是将每次查询都遍历该区间求和

//暴力做法

import java.util.Scanner;


public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int m = scan.nextInt();
        int[] a = new int[100010];
        for(int i = 1;i<=n;i++){
            a[i] = scan.nextInt();
        }
        for(int i = 0;i<=m;i++){
            int l = scan.nextInt(),r = scan.nextInt(),sum=0;
            for(int j = l;j<=r;j++){
                sum+=a[j];
            }
            System.out.println(sum);
        }
    }
}

可以看到,最坏情况下,时间复杂度为 O(nm) ,这种区间求和问题就可以使用前缀和来优化

前缀和实现原理:

核心思想:

在一个新的数组上的每个元素都存储 原数组开始位置 新数组元素位置 ,最后通过 减法 来实现快速计算

原理解释:

创建一个新的数组 s ,其中每个元素 s[i] 表示原数组从开始位置到位置 i 元素之和,即

当我们需要查询 [2,3] 时,就是计算 a[2] + a[3] , 对于数组 s 来说只需要将 s[3] - s[1]

这一步的时间复杂度为 O(1)

通过上述计算可以得到公式

在创建时新数组时还可以通过迭代计算每个 s[i]

可以得到公式:

这里注意 s[1] == a[1] 但是在循环里要写成 s[1] = s[0] + a[1] ,才不会下标越界,所以数组下标都由1开始。

代码演示:

import java.util.Scanner;

public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int[] a = new int[100010];
        int[] s = new int[100010];
        int n = scan.nextInt();
        int m = scan.nextInt();
        s[0] = 0;
        for(int i = 1;i<=n;i++){
            a[i] = scan.nextInt();
            s[i] = s[i-1]+a[i];
        }
        for(int i = 1;i<=m;i++){
            int l = scan.nextInt();
            int r = scan.nextInt();
            System.out.println(s[r] - s[l-1]);
        }
    }
}

这样时间复杂度就来到了 O(n+m) 。

理解了一维前缀和,我们将问题上升一个维度,来到

二维前缀和:

给定一个 n*m 大小的矩阵 A,给定 q 组查询,每次查询给定两个坐标,需要输出坐标1到坐标2的所有值

暴力做法:

import java.util.Scanner;

public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int[][] a = new int[100][100];
        int n = scan.nextInt();
        int m = scan.nextInt();
        int q = scan.nextInt();
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                a[i][j] = scan.nextInt();
            }
        }
        for(int i=1;i<=q;i++){
            int sum = 0;
            int x1= scan.nextInt(),y1= scan.nextInt(),x2= scan.nextInt(),y2= scan.nextInt();
            for(int j=x1;j<=x2;j++){
                for(int k=y1;k<=y2;k++){
                    sum+=a[j][k];
                }
            }
            System.out.println(sum);
        }
    }
}

中间的部分有一个三重循环,时间复杂度来到了 O(n * m * q)

依旧使用前缀和的思想来完成,在二维数组上的每个元素都要存储从开始位置的和

在二维前缀和数组中,求(x,y)就是求从(1,1)开始到(x,y)的所有和

对(x,y)做进一步拆分,会发现由四个部分组成

 先是当前点本身 a(x,y):

s[x][y-1]:

s[x-1][y]:

s[x-1][y-1]:

因为s[x-1][y-1]被加了两次,所以需要减去一次


 

综上可以得到前缀和计算公式:

s[x][y] = a[x][y] + s[x-1][y] + s[x][y-1] - s[x-1][y-1] + ;

现在起点不从(1,1)开始,计算两点表示的子矩阵的和

就可以让(x2,y2)减去两边的长方形(x2,y1-1)和 (x1-1,y2) ,因为 (x-1,y-1) 被减去两次,所以需要加上一次,得到公式:

\sum _{i=x1}^{x2}\sum _{j=y1}^{y2}A{_{i,j}}^{}

= a[x2][y2] - a[x2][y1-1] - a[x1-1][y2] + a[x1-1][y1-1] 

代码演示:

import java.util.Scanner;

public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int N = 1010;
        int[][] a = new int[N][N];
        int[][] s = new int[N][N];
        int n = scan.nextInt();
        int m = scan.nextInt();
        int q = scan.nextInt();
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                a[i][j] = scan.nextInt();
                //构建前缀和数组
                s[i][j] = a[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1];
            }
        }
        for(int i=1;i<=q;i++){
            int x1 = scan.nextInt(),y1 = scan.nextInt(),x2 = scan.nextInt(),y2 = scan.nextInt();
            int sum = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
            System.out.println(sum);
        }
    }
}

时间复杂度变为O(n*m+q)

二、区间修改问题

给定一个长度为n的序列a,有m组操作,每次操作将某一个连续区间 [ l ~ r ] 的元素都加上,最后输出操作结束后的数组a。

暴力做法为写一个循环,每次修改 [ l ~ r ] 的值,重复m次

import java.util.Scanner;

public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int[] a = new int[100010];
        int n = scan.nextInt();
        int m = scan.nextInt();
        for(int i = 1;i<=n;i++){
            a[i] = scan.nextInt();
        }
        for(int i = 1;i<=m;i++){
            int l = scan.nextInt();
            int r = scan.nextInt();
            int d = scan.nextInt();
            for(int j = l;j<=r;j++){
                a[j] += d;
            }
        }
        for(int i=1;i<=n;i++){
            System.out.print(a[i] + " ");
        }
    }
}

时间复杂度为 O(nm),这种区间求和问题就可以使用差分来优化

差分实现原理:

核心思想:通过计算原数组中每个元素之间的差让元素和元素之间产生联系,再利用差不断复原出原数组。期间对差进行加减就会影响这个差之后的所有的元素

定义一个差分数组 b ,求出每一个元素之间的差

对 数组b 求前缀和 得到 新数组c

这里就可以得到一个结论,差分数组求前缀和就可以得到原数组

当我们对 b[1] + 1 后

b[1] 之后的元素都会 +1

当我们对 b[3]  - 1 后

b[3] -1 之后的元素也都 -1 和如果原来的+1相呼应就可以得到修改前的元素,最后可以得到结论:

对差分数组 b[l]+d , b[r+1]-d ,求解前缀和后,就可以得到修改后的数组c

代码演示

import java.util.Scanner;

public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int[] a = new int[100010];
        int[] b = new int[100010];
        int[] c = new int[100010];
        int n = scan.nextInt();
        int m = scan.nextInt();
        for(int i = 1;i<=n;i++){
            a[i] = scan.nextInt();
            b[i] = a[i] - a[i-1];
        }
        for(int i = 1;i<=m;i++){
            int l = scan.nextInt(),r = scan.nextInt(),d = scan.nextInt();
               b[l] += d;
               b[r+1] -= d;
        }
        for(int i = 1;i<=n;i++){
            c[i] = c[i-1] + b[i];
        }
        for(int i = 1;i<=n;i++){
            System.out.print(c[i]+" ");
        }
    }
}

时间复杂度 O(n+m)

差分数组的常见性质:

  • 如果差分数组 b 除了 b[1] 以外所有值均为 0 ,则说明数组 a 的值全部都一样

差分数组还可以解决

归1问题:

一个数组 a 中共包含 n 个数,问最少多少次操作可以让 a 数组所有数都变成 1 。

操作的内容可以任选一个区间,使得区间内所有值 -1 ,数据保证一定有解

假设有一个数组【1,3,5,2,7,1】

3 变成 1 需要进行 3-1 次操作

5 变成 1 需要  4 次操作,因为 5 > 3 所以可以跟着 3 一起进行 2 次,所以只要 5-3 次

2 < 5 可以跟着 5 进行 1 次后就不用再进行,可以忽略不计

7 > 2 需要进行 7-2 次操作

综上可以得出看出,每一个数需要的操作次数可以通过减去前一个数来求出,当减去前一个数<0时,就可以忽略不计,求出每一个元素之间的差,刚好是差分数组所实现的内容,因为是归1,而第一项(a[0])是 0 ,a[1] 的操作次数会多一次,所以最后结果还需要 -1 。

代码演示:

public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int[] a = new int[100010];
        int[] b = new int[100010];
        int n = scan.nextInt();
        int sum = 0;
        for(int i = 1;i<=n;i++){
            a[i] = scan.nextInt();
            b[i] = a[i] - a[i-1];
            if(b[i]>=0){
                sum += b[i];
            }
        }
        System.out.println(sum-1);
    }
}

二维差分:

给定一个 n*m 大小的矩阵 A,给定 q 组修改,每次查询给定两个坐标,需要修改坐标1到坐标2的所有值,最后打印修改完成的数组

暴力做法:

import java.util.Scanner;

public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int N = 1010;
        int[][] a = new int[N][N];
        int[][] s = new int[N][N];
        int n = scan.nextInt();
        int m = scan.nextInt();
        int q = scan.nextInt();
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                a[i][j] = scan.nextInt();
            }
        }
        for(int i=1;i<=q;i++){
            int x1 = scan.nextInt(),y1 = scan.nextInt(),x2 = scan.nextInt(),y2 = scan.nextInt(),d= scan.nextInt();
            for(int j=x1;j<=x2;j++){
                for(int k=y1;k<=y2;k++){
                    a[j][k] += d;
                }
            }
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                System.out.print(a[i][j]+" ");
            }
            System.out.println();
        }
    }
}

时间复杂度为 O(n*m*q)

接下来用差分思想进行优化

首先是用差让各个元素之间产生联系,这里减去上方和左方的格子

如果只是减去左方格子就变回了一维差分

(x,y-1)和(x-1,y)也是通过减去上方和左方的格子得来

这里会发现(x-1,y-1)被多减了一次,所以计算(x,y)时还需要再加上(x-1,y-1)

通过上图描述可得公式:

差分数组 b[x][y] = a[x][y] - a[x][y-1] - a[x-1][y] + a[x-1][y-1];

有了差分数组,就可以对元素进行修改

当对差分数组修改后,所有后续有关联的元素都会发生改变

当希望只在 (x1,x2) 和 (x2,y2) 之间进行修改,就需要对其他点进行修改

对于边上的两点(x2+1,y1)、(x1,y2+1)减少 d ,对于重复减的点(x2+1,y2+1)加上 d

对点修改完后,再通过前缀和对数组复原

代码演示:

import java.util.Scanner;

public class Test {
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int N = 1010;
        int[][] a = new int[N][N];
        int[][] b = new int[N][N];
        int n = scan.nextInt();
        int m = scan.nextInt();
        int q = scan.nextInt();
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                a[i][j] = scan.nextInt();
                b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1] ;
            }
        }
        for(int i=1;i<=q;i++){
            int x1=scan.nextInt(),y1=scan.nextInt(),x2=scan.nextInt(),y2=scan.nextInt(),d=scan.nextInt();
            b[x1][y1]+=d;
            b[x2+1][y1]-=d;
            b[x1][y2+1]-=d;
            b[x2+1][y2+1]+=d;
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                b[i][j]=b[i][j]+b[i-1][j]+b[i][j-1]-b[i-1][j-1];
                System.out.print(b[i][j]+" ");
            }
            System.out.println();
        }
    }
}

时间复杂度为 O(n*m+q)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2260425.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

股市投资策略升级:掌握马尔可夫决策过程与强化学习,提升交易技能

作者&#xff1a;老余捞鱼 原创不易&#xff0c;转载请标明出处及原作者。 写在前面的话&#xff1a;本文将深入探讨马尔可夫决策过程&#xff08;MDP&#xff09;和强化学习在股票交易中的运用。通过阐述MDP的基本原理和其在交易策略中的实际应用&#xff0c;试图向您揭示这些…

jvm结构介绍

1. 垃圾回收&#xff08;Garbage Collection, GC&#xff09;&#xff1a;JVM自动管理内存的机制&#xff0c;负责回收不再使用的对象占用的内存空间。常见的垃圾回收算法包括标记-清除&#xff08;Mark-Sweep&#xff09;、复制&#xff08;Copying&#xff09;、标记-整理&am…

基于智能电能表的智能家居能源管理系统设计

目录 引言系统设计 硬件设计软件设计系统功能模块 电能测量模块数据传输模块能源管理模块控制算法 数据采集与处理算法能源优化算法代码实现 电能测量模块实现数据传输模块实现系统调试与优化结论与展望 1. 引言 随着智能家居的发展&#xff0c;电能管理成为智能家居系统中的…

【计算机组成原理】实验二:通用寄存器单元实验

实验二&#xff1a;通用寄存器单元实验 一、实验目的 了解通用寄存器的组成和硬件电路&#xff0c;利用通用寄存器实现数据的置数、左移、右移等功能。 二、实验内容 数据输入通用寄存器 寄存器内容无进位位左移实验 寄存器内容无进位位右移实验 三、实验步骤和结果 实…

codeforces一些题目思路复盘

codeforces round 919 dv2 C Partitioning the Array 大致题意&#xff0c;对于n约数i&#xff0c;我们把原数组分成份&#xff0c;并且每份中有i个元素&#xff0c;对于每个分组情况&#xff0c;如果存在一个数m使得数组中元素modm后使得每个部分的数组完全相同&#xff0c;如…

《拉依达的嵌入式\驱动面试宝典》—C/CPP基础篇(四)

《拉依达的嵌入式\驱动面试宝典》—C/CPP基础篇(四) 你好,我是拉依达。 感谢所有阅读关注我的同学支持,目前博客累计阅读 27w,关注1.5w人。其中博客《最全Linux驱动开发全流程详细解析(持续更新)-CSDN博客》已经是 Linux驱动 相关内容搜索的推荐首位,感谢大家支持。 《拉…

又细又长的马尾:tail

英语里边有一个单词 tail&#xff0c;意为“尾巴”&#xff0c;这应当是众所周知的事情了。 不过&#xff0c;tail 这条尾巴&#xff0c;并不简单&#xff0c;因为它还是一个词根&#xff0c;也就是说 tail 其实是自由词素。 事实上&#xff0c;tail 最初来自 马尾 这样一个概…

Lumos学习王佩丰Excel第二十一讲:经典Excel动态图表实现原理

一、动态图表实现原理 1、理解图表中的数据系列 在Excel图表中&#xff0c;系列指的是图表中的数据集合&#xff0c;它通常代表着一个数据源。每个系列都可以包含多个数据点&#xff0c;这些数据点在图表中以特定的形式展现&#xff0c;如柱状图中的柱子&#xff0c;折线图中…

使用Qt Creator设计可视化窗体(一)

一、创建项目 打开 Qt Creator &#xff0c;在菜单栏中选中&#xff1a; “文件” --------> “新建文件或项目” &#xff1b;或者使用快捷键&#xff1a;Ctrl n&#xff1b;或者直接点击&#xff1a;“new” Qt 中的构建工具有三种可供选择&#xff0c;分别是&#…

Rust之抽空学习系列(四)—— 编程通用概念(下)

Rust之抽空学习系列&#xff08;四&#xff09;—— 编程通用概念&#xff08;下&#xff09; 1、函数 函数用来对功能逻辑进行封装&#xff0c;能够增强复用、提高代码的可读 以下是函数的主要组成部分&#xff1a; 名称参数返回类型函数体 1.1、函数名称 在Rust中&…

springboot423玩具租赁系统boot(论文+源码)_kaic

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装玩具租赁系统软件来发挥其高效地信息处理的作用&#xff0c…

.NET6 WebAPI从基础到进阶--朝夕教育

1、环境准备 1. Visual Studio 2022 2. .NET6 平台支持 3. Internet Information Services 服务器&#xff08; IIS &#xff09; 4. Linux 服务器 【 CentOS 系统】 ( 跨平台部署使用 ) 5. Linux 服务器下的 Docker 容器&#xff08; Docker 部署使用&#xff09; …

Attentive Fusion论文精读

OPV2V: An Open Benchmark Dataset and Fusion Pipeline for Perception with Vehicle-to-Vehicle Communication 文章目录 背景创新点1.提出新的数据2.提出了一种注意力中间融合管道 2.相关工作车对车感知早期融合晚期融合中间融合 车对车数据集 3. OPV2V 数据集A.数据收集模…

Datawhale AI冬令营(第一期)task2--微调玩法攻略

目录 1.微调玩法攻略 1.1.微调思路 1.2.什么是大模型人格化&#xff1f; 1.3. 大模型人格化的应用场景 1.4 构建对应格式的数据集 1.4.1 选择数据格式 1.4.2 Alpaca 格式要求 1.4.3 构建数据集 1.4.4 没有剧本怎么办 1.4.5 整理成 json 格式 1.微调玩法攻略 1.1.微…

VQ-VAE和VAE 的区别是什么?

第一行所展示的就是普通的VAE,它的核心是通过encoder和decoder&#xff0c;将像素空间的图像压缩到一个提取了核心特征的隐变量向量。VQ-VAE的思想是&#xff0c;即使VAE中压缩的这个隐变量中的向量提取了图片中的核心特征信息&#xff0c;但是这些信息仍然可能存在冗余&#x…

Redis--高并发分布式结构

目录 一、引言 二、redis 1.什么是redis&#xff1f; 三、基础概念 1.什么是分布式&#xff1f; 2.应用服务和数据库服务分离 3.负载均衡 4.分库分表 5.微服务架构 四、总结 一、引言 本篇文章就简单介绍一下什么是redis&#xff0c;以及一些关于高并发和分布式结构的…

188-下翻便携式6U CPCI工控机箱

一、板卡概述 下翻式CPCI便携工控机,系统采用6u cpci背板结构,1个系统槽,7个扩展槽, 满足对携带的需求,可装标准6U8槽CPCI主板,8个扩展槽, 满足客户对空间扩展的需求.可宽温服务的工作产品,15高亮度液晶显示屏,超薄88键笔记本键盘,触摸式鼠标,加固型机箱结构,使它能够适应各种复…

Linux 磁盘满了怎么办?快速排查和清理方法

当 Linux 磁盘满了&#xff0c;会导致系统无法正常运行&#xff0c;比如无法写入文件、服务停止、甚至系统崩溃。因此&#xff0c;快速排查并清理磁盘空间是非常重要的。以下是详细的排查和解决步骤&#xff1a; 一、快速定位磁盘占用原因 1. 检查磁盘使用情况 使用 df 命令查…

OpenGL ES详解——多个纹理实现混叠显示

目录 一、获取图片纹理数据 二、着色器编写 1. 顶点着色器 2. 片元着色器 三、绑定和绘制纹理 1. 绑定纹理 2. 绘制纹理 四、源码下载 一、获取图片纹理数据 获取图片纹理数据代码如下&#xff1a; //获取图片1纹理数据 mTextureId loadTexture(mContext, R.mipmap.…

对话小系统(智能图书助手)

对话小系统&#xff08;智能图书助手&#xff09; 文章说明核心代码效果展示源码下载 文章说明 现在GPT的功能十分强大&#xff0c;是否可以利用开源的接口来实现自己的智能小助手呢&#xff0c;我想到可以提供一些能力接口&#xff0c;然后对问询内容进行意图识别&#xff0c;…