【Java算法专场】前缀和(下)

news2024/10/3 10:43:33

目录

和为 K 的子数组

算法分析

算法步骤

算法代码

算法示例

和可被 K 整除的子数组

算法分析

同余定理

负数取余

算法步骤

算法代码

算法示例

连续数组

算法分析

算法步骤

算法代码

算法示例

矩阵区域和

算法分析

算法步骤

算法代码

算法示例



算法分析

本道题是要在数组中查找和为k的子数组,如果我们使用暴力解法,时间复杂度为O(N^2)。对于这种求子数组和的问题,我们可以使用前缀和来解决。

算法步骤

  1. 初始化:定义一个sum,初始化为0,用来计算数组中元素和;定义一个HashMap用来存储数组中的前缀和,还有值的出现次数(此处需要添加(0,1),若sum-k刚好等于0的情况)。定义一个ans用来计算数组中有多少个和为k的子数组。

  2. 遍历数组:将数组中的元素累加到sum中,同时在HashMap中判断此时的sum是否存在,若存在,则将其键值添加到ans中。当添加完之后,将此时的sum添加到Hashmap中。

  3. 返回ans:当遍历完数组,此时返回ans即可。

算法代码

/**
     * 计算一个数组中连续子数组和等于给定值k的子数组个数
     * 该方法使用前缀和的概念来加速计算过程,通过HashMap记录每个前缀和出现的次数
     *
     * @param nums 整数数组,其中nums[i]表示子数组的元素
     * @param k 目标和,表示连续子数组的和
     * @return 返回连续子数组和等于k的子数组个数
     */
    public int subarraySum(int[] nums, int k) {
        // 初始化当前前缀和为0
        int sum=0;
        // 初始化结果变量为0,用于记录找到的子数组个数
        int ans=0;
        // 使用HashMap来存储每个前缀和出现的次数
        HashMap<Integer,Integer> hash=new HashMap<>();
        // 将前缀和0的出现次数设置为1,表示前缀和为0的出现了一次
        hash.put(0,1);
        // 遍历数组中的每个元素
        for(int x:nums){
            // 累加当前元素到前缀和
            sum+=x;
            // 如果当前前缀和减去目标和的值已经出现过,则说明找到了一个或多个和为k的子数组
            // 将其出现次数加到结果变量中
            ans+=hash.getOrDefault(sum-k,0);
            // 更新HashMap,记录当前前缀和的出现次数
            // 如果当前前缀和不存在,则默认为0,然后加1
            hash.put(sum,hash.getOrDefault(sum,0)+1);
        }
        // 返回找到的子数组个数
        return ans;
    }

时间复杂度为O(n),n为数组长度,只遍历一遍数组,hash的查找速度为O(1).

空间复杂度为O(n),最坏情况下hash存储的个数刚好是n个,n是数组长度。

算法示例

以nums = [1,2,3], k = 3为例

第一步:初始化

sum=0,ans=0,hash={(0,1)}

第二步:遍历数组

  1. sum+=1,ans+=0,hash={(0,1),(1,1)}
  2. sum=3, sum-k=0,此时hash中key为0的键值为1,ans+1,ans=1,hash={(0,1),(1,1),(3,1)}
  3. sum=6,sum-k=3,此时hash中key为3的键值为1,ans+1,ans=2,hash={(0,1),(1,1),(3,1),(6,1)}.

第三步:返回ans

此时ans=2,返回即可。

和可被 K 整除的子数组

算法分析

本道题与上一道题类似,不过这道题求和能被K整除的子数组。若使用暴力解法,时间复杂度为O(n^2)。但我们可以使用更好的方法来优化。那就是前缀和。

在讲这道题之前,我们先来了解一下什么是同余定理

同余定理

给定一个正整数 m,如果两个整数 a 和 b 满足 m|(a-b),即 a-b 能够被 m 整除,或者 (a-b)/m 得到一个整数,那么就称整数 a 与 b 对模 m 同余,记作 a≡b(mod m)。对模 m 同余是整数的一个等价关系。这里我们不做过多介绍(想要了解的更清楚可以去查一下)。

(A-B)%m=0 <==> A%m=B%m

示例:(5-3)%2=0  <=> 5%2=1  3%2=1

负数取余

负数%整数=负数

但我们这里需要的正数,所以我们可以在A%m之后再加上m之后就可以得到一个正数。同时,可能A本身是一个正数,那么取余之后再加上m,就有点多余了,所有我们这里在+m之后还需对其再模一次m,即(A%m+m)%m.

算法步骤

  1. 初始化:定义一个ans并初始化为0;定义一个sum并初始化为0,用来累加数组和;定义一个HashMap,Key用来记录余数,values用来记录余数出现的次数。
  2. 遍历数组:让sum累加数组中的元素。同时,当sum累加一个元素之后,此时需要在hashmap查看是否有以【(sum%k+k)%k】为键的键值对。如果当前前缀和的余数已经出现过,则说明找到了一个或多个能被k整除的子数组,则将此键值对下的键值加到ans中。当加完之后,将sum此时的余数映射到hashmap中,并将其键值+1.
  3. 处理细节:此处有可能sum的余数为0,所以我们需要在遍历之前先添加一个(0,1).
  4. 返回结果:当遍历完数组之后,此时返回ans即可。

算法代码

    /**
     * 计算数组中前缀和能被k整除的子数组数量
     * 
     * @param nums 输入的整数数组
     * @param k 整数k,用于判断子数组的和是否能被k整除
     * @return 返回满足条件的子数组数量
     */
    public int subarraysDivByK(int[] nums, int k) {
        // 用于计算前缀和
        int sum=0;
        // 用于记录满足条件的子数组数量
        int ans=0;
        // 使用HashMap来存储每个前缀和的出现次数
        HashMap<Integer,Integer> hash=new HashMap<>();
        for(int x:nums){
            // 累加当前元素到前缀和
            sum+=x;
            // 如果当前前缀和减去目标和的值已经出现过,则说明找到了一个或多个和为k的子数组
            ans+=hash.getOrDefault((sum%k+k)%k,0);
            // 更新HashMap,记录当前前缀和的出现次数
            hash.put((sum%k+k)%k,hash.getOrDefault((sum%k+k)%k,0)+1);
        }
    }

时空复杂度为O(n),n为数组长度,整个过程,只遍历了一遍数组。在最坏情况下,可能hash的键值对有n个。 

算法示例

以nums = [4,5,0,-2,-3,1], k = 5为例

第一步:初始化

sum=0,ans=0.hash={(0,1)};

第二步:遍历数组

(记余数为mod)

  1. sum+=nums[0]=4,mod=(4%5+5)%5=4,此时hash中不存在以4为键的键值对,添加到hash中,hash={(0,1),(4,1)}
  2. sum+=nums[1]=4+5=9,mod=(9%5+5)%5=4.此时hash存在以4为键的键值对,键值为1,此时ans+1=1,更新键值,hash={(0,1),(4,2)}
  3. sum+=nums[2]=9+0=9,mod=(9%5+5)%5=4,此时hash存在以4为键的键值对,键值为2,此时ans+2=3,更新键值,hash={(0,1),(4,3)}
  4. sum+nums[3]=9-2=6,mod=(7%5+5)%5=2,此时hash不存在以2为键的键值对,添加到hash中,hash={(0,1),(4,3),(2,1)}
  5. sum+nums[4]=7-3=4,mod=(4%5+5)%5=4,此时hash存在以4为键的键值对,键值为3,此时ans+3=6,更新键值,hash={(0,1),(4,4),(2,1)}
  6. sun+nums[5]=4+1=5,mod=(5%5+5)%5=0,此时hash存在以0为键的键值对,键值为1,此时ans+1=7,更新键值,hash={(0,2),(4,4),(2,1)}

第三步:返回结果

此时已经遍历完数组,ans=7,返回ans即可。

连续数组

算法分析

本道题与第一道和为k的子数组类似,但本道题要求的在一个二进制数组中找具有相同数量0和1的最长子数组。若用暴力解法,时间复杂度为O(n^2),但我们这里可以使用前缀和。让时间复杂度达到O(n).

算法步骤

  1. 初始化:定义ans并初始化为0,定义sum,用来计算每次遍历到的位置之前的差值;定义hash,用来存放sum以及其在数组中的索引。同时需要将以0为键的键值设置为-1,这是为了防止在0处就出现了相同数量的0和1.
  2. 遍历数组:在将nums[i]添加到sum时,如果nums[i]是0,那么就视为-1,反之,若是1,则直接累加到数组中。通过-1和1,我们就能算出0和1此时的差值。
  3.  查看0和1的数量:每次更新完sum的值,我们都需要判断sum是否已经在hash中;若已经在hash中,我们则直接更新ans的值,ans=Math.max(ans,i-hash.get(sum))。若此时sum不存在hash中,就将sum及其索引i添加到hash中,即hash.put(sum,i)。
  4. 返回结果:当我们遍历完数组,此时就可以将ans返回。

算法代码

/**
     * 寻找给定数组中最长的连续子数组,使得子数组中0和1的个数相等
     *
     * @param nums 一个只包含0和1的整数数组
     * @return 返回满足条件的最长子数组的长度
     */
    public int findMaxLength(int[] nums) {
        // sum用于记录数组遍历过程中的0和1的累积差值
        int sum=0;
        // ans用于记录满足条件的最长子数组的长度
        int ans=0;
        // 使用HashMap来存储每个累积差值首次出现的索引位置
        HashMap<Integer,Integer> hash=new HashMap<>();
        // 初始化sum为0,并将0作为HashMap的初始索引位置
        hash.put(0,-1);
        // 遍历数组,计算累积差值,并更新最长子数组的长度
        for(int i=0;i<nums.length;i++){
            // 如果当前元素为0,则将sum减1,否则将sum加1
            sum+=(nums[i]==0?-1:1);
            // 如果当前累积差值已经在HashMap中存在,则找到了一个满足条件的子数组
            if(hash.containsKey(sum)) {
                // 更新最长子数组的长度
                ans=Math.max(ans,i-hash.get(sum));
            } else {
                // 如果当前累积差值首次出现,则将其索引位置存入HashMap
                hash.put(sum,i);
            }
        }
        // 返回最长子数组的长度
        return ans;
    }

时空复杂度为O(n),n为数组长度,整个过程只遍历了一遍数组,在最坏情况下,hash的键值对可能是O(n). 

算法示例

以nums = [0,1,0]为例

第一步:初始化

ans=0,sum=0,hash={(0,-1)};

第二步:遍历数组

  1. sum+=nums[0]=-1,此时hash中不存在以-1为键的键值对,将其添加到hash中,hash={(0,-1),(-1,0)}
  2. sum+=nums[1]=-1+1=0,此时hash中存在以0为键的键值对,键值为-1。此时索i=1,ans=1-(-1)=2。
  3. sum+=nums[2]=0-1=-1,此时hash中存在以-1为键的键值对,键值为1,此时索引i=2,ans=2-0=2。

第三步:返回结果

此时已经遍历完数组,ans=2,返回即可。

矩阵区域和

算法分析

本道题看起来难理解,其实就是那就通过下图来进行理解。

不难看出,其实题目要计算的,其实就是要计算向外扩大k个方格的矩阵的和并将计算出以每个点为中心的矩阵和以一个新的矩阵返回。 

算法步骤

  1. 初始化:定义n并初始化为mat数组的行数,定义m并初始化为mat[0].length,即列的长度。创建一个(n+1)*(m+1)的二维前缀和数组dp。
  2. 预处理dp数组:在上一篇我们已经讲过如何实现一个dp数组,这里我直接上式子:dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+mat[i-1][j-1],为什么是mat[i-1][j-1],在前面我们的二维数组是从1下标开始的,但这里是从0开始,所以dp[i][j]中对应mat中的mat[i-1][j-1].
  3. 定义ret结果矩阵:这里我们定义个大小为(n*m)的ret二维数组,用来存放矩阵和。
  4. 定义坐标:这里我们需要定义矩阵的左上角坐标(x1,y1),以及右下角坐标(x2,y2).在计算坐标时,由于dp是从1开始,而ret是从0开始的,所以我们在dp中取值时,坐标都要+1,同时需要考虑当前索引下的i-k和i-j是否越界。依据上述:x1=Math.max(0,i-k)+1  y1=Math.max(0,j-k)+1;  x2=Math.min(n-1,i+k)+1  y2=Math.min(m-1,j+k)+1.
  5. 计算结果:当完成坐标的计算,那么我们就可以根据ret[i][j]=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1]计算矩阵和
  6. 返回ret矩阵:当完成计算之后,我们返回ret矩阵即可。

算法代码

/**
     * 计算矩阵中每个元素的块和
     *
     * @param mat 输入的矩阵
     * @param k 块的大小参数
     * @return 每个元素的块和构成的新矩阵
     */
    public int[][] matrixBlockSum(int[][] mat, int k) {
        // 获取矩阵的行数
        int n = mat.length;
        // 获取矩阵的列数
        int m = mat[0].length;
        // 初始化二维前缀和数组,大小为矩阵大小加1,用于方便计算
        int[][] dp = new int[n + 1][m + 1];
        // 遍历矩阵,计算二维前缀和
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                // 当前元素的二维前缀和等于其上方、左方和左上方元素的前缀和加上当前元素值
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        // 初始化结果矩阵,用于存放每个元素的块和
        int[][] ret = new int[n][m];
        // 遍历矩阵,计算每个元素的块和
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // 计算块的左上角坐标
                int x1 = Math.max(0, i - k)+1;
                int y1 = Math.max(0, j - k)+1;
                // 计算块的右下角坐标
                int x2 = Math.min(n - 1, i + k)+1;
                int y2 = Math.min(m - 1, j + k)+1;
                // 计算当前元素的块和,减去不需要的部分
                ret[i][j] = dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1 - 1][y1 - 1];
            }
        }
        // 返回结果矩阵
        return ret;
    }

时空复杂度为O(n*m),n*m是数组的大小,整个过程需要开辟两个数组。 

算法示例

以mat = [[1,2,3],[4,5,6],[7,8,9]], k = 1为例

这里不做太多说明,上图!

 

以上就是前缀和算法的专题训练,就先到这里了~

若有不足,欢迎指正~

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

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

相关文章

1008 Elevator(Java)

题目 解释 输入数字N&#xff0c;数字N后面跟着N个数字代表着目的楼层&#xff0c;起始点是0层&#xff0c;每上升一层花费6秒&#xff0c;每下降一层花费4秒&#xff0c;每达到一个目的楼层&#xff0c;电梯会停5秒。问你一共花费多少秒&#xff1f; 解题思路 这道题不难&a…

【论文笔记】Matching Anything by Segmenting Anything

【引用格式】&#xff1a;Li S, Ke L, Danelljan M, et al. Matching Anything by Segmenting Anything[C]//Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2024: 18963-18973. 【网址】&#xff1a;https://openaccess.thecvf.com/co…

【Python】把list转换成json文件(list中为字典,元素按行写入)

0.前言 数据需要处理成与大模型输入相同类型的数据&#xff0c;从csv文件读出后&#xff0c;想要转换成json文件&#xff0c;看了好多资料都是把整个list写入了json&#xff0c;并不是我想要的格式&#xff0c;这里记录一下最后的按行写入的格式。 1.list转json import json …

Camtasia怎么自动加字幕 Camtasia怎么设置字幕

为了防止观众听不清楚视频中的解说声音&#xff0c;在进行视频编辑时&#xff0c;往往需要添加字幕。但是&#xff0c;你是否觉得一个一个添加字幕有些麻烦&#xff1f;使用Camtasia时&#xff0c;有没有什么能够快速、方便添加字幕的方法呢&#xff1f;添加字幕后&#xff0c;…

Docker 环境下使用 Traefik 3 的最佳实践:快速上手

Traefik 最近终于发布了大版本升级后的第一个修正版本&#xff0c;或许是时候正式迁移程序到新版本了。 写在前面 最近 Traefik 发布了 3.1 版本。作为从 Traefik 1.x 开始使用的用户&#xff0c;Traefik 每个大版本升级都会出现一些配置不兼容的情况&#xff0c;这次 3.x 的正…

DC-DC芯片:MT2492说明

文章目录 一. MT2492介绍1. 特点2. 应用3. 额定值4. 电气特性5. 引脚说明6. 典型应用电路 二. 功能说明内部调节器过电流保护和打嗝模式误差放大器启动和关闭内部软启动设置输出电压电感器的选择输入电容器的选择输入电容器的选择PCB布局建议 一. MT2492介绍 MT2492是一款完全…

LaneATT推理详解及部署实现(下)

目录 前言一、LaneATT推理(Python)1. LaneATT预测2. LaneATT预处理3. LaneATT后处理4. LaneATT推理 二、LaneATT推理(C)1. ONNX导出2. LaneATT预处理3. LaneATT后处理4. LaneATT推理 三、LaneATT部署1. 源码下载2. 环境配置2.1 配置CMakeLists.txt2.2 配置Makefile 3. ONNX导出…

平衡二叉树 - 力扣(LeetCode) C语言

110. 平衡二叉树 - 力扣&#xff08;LeetCode&#xff09;&#xff08;点击前面链接即可查看题目&#xff09; 一、题目 给定一个二叉树&#xff0c;判断它是否是 平衡二叉树 是指该树所有节点的左右子树的深度相差不超过 1。 示例 1&#xff1a; 输入&#xff1a;root […

计量经济学(十六)--一文读懂和学会医学统计学中的四种检验方法

1. 统计学是什么? 统计学是应用数学的一个分支,主要通过利用概率论建立数学模型,收集所观察系统的数据,进行量化的分析、总结,并进而进行推断和预测,为相关决策提供依据和参考。它被广泛的应用在各门学科之上,从物理和社会科学到人文科学,甚至被用来工商业及政府的情报…

原神自定义倒计时

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><title>原神倒计时</title><style>* {margin: 0;padding: 0;box-sizing: border-box;user-select: none;body {background: #0b1b2c;}}header {…

C++ ---- vector的底层原理剖析及其实现

vector 一、定义二、常用接口及模拟实现三、vector迭代器失效问题四、使用memcpy拷贝会出现的问题五、二维数组vector<vector< T >> vv 一、定义 vector 是 C 标准模板库&#xff08;Standard Template Library, STL&#xff09;中的一个非常有用的容器。它是一个…

SD卡受损数据会消失吗 内存卡坏了数据还能恢复吗 SD卡受损里面的数据怎么办 sd卡受损最简单的修复方法

SD卡里的数据有时候也容易出现消失或者遭到破坏等情况&#xff0c;针对此类情况&#xff0c;给大家详细讲解&#xff0c;SD卡受损数据会消失吗&#xff0c;以及SD卡受损了里面的数据怎么办。 一、SD卡受损数据会消失吗 SD卡本身比较小巧&#xff0c;里面的小芯片比较密集&…

04 RabbitMQ:控制界面详解

04 RabbitMQ&#xff1a;控制界面详解 1. 控制台界面2. 控制界面详解2.1. Overview&#xff08;概览&#xff09;2.1.1. Totals&#xff08;总数&#xff09;2.1.1.1. Queued messages2.1.1.2. Message rates2.1.1.3. Global counts 2.1.2. Nodes&#xff08;节点消息&#xff…

SpringBoot多数据源事务处理

多数据源时,一般会配置多个事务管理器 Spring编程式 第二种方式 不可能去同一个方法上写两个事务注解 不允许 SpringBoot 2.6.0之后禁止自己注入自己 本来可以自己注入自己去调用 (为什么要自己注入自己调用,AOP代理,类不是自己写的类) 最简单方式 引入 <dependency&…

DPDK基础入门(一):认识和理解DPDK

Linux的网络瓶颈 以Linux为例&#xff0c;传统网络设备驱动包处理的动作可以概括如下&#xff1a; 数据包到达网卡设备。网卡设备依据配置进行DMA操作。网卡发送中断&#xff0c;唤醒处理器。驱动软件填充读写缓冲区数据结构。数据报文达到内核协议栈&#xff0c;进行高层处理…

Linux中信号的发送及信号的自定义捕捉方法

预备知识&#xff1a; 信号产生时进程早已知道该信号如何处理。 信号产生时进程可能并不能立即处理信号而是等到合适的时候处理。 信号其他相关常见概念 实际执行信号的处理动作称为信号递达(Delivery) 信号从产生到递达之间的状态,称为信号未决(Pending)。 进程可以选择阻…

C语言第14篇

1.以下定义语句中&#xff0c;错误的是________. A) int a[]{1,2}; B) char a{"test"}; C) char s[10]{"test"}; D) int a[]{a,b,c}; 2.以下定义语句中&#xff0c;错误的是________. A) int a[]{1,2}; B) char a[]{…

扬声器、麦克风的等效电路及相关技术参数(灵敏度等)

扬声器、麦克风都是日常我们所需的电子小器件&#xff0c;今天小编来具体讲解一下有关两者的等效电路及相关技术参数。 1、扬声器 等效电路 Re表示扬声器音圈的直流电阻 Le表示音圈的电感,对高频信号产生的阻抗 Mm表示动圈的等效质量,主要影响扬声器的低频响应 Rm 表示动圈…

算力共享:forward_to_next_shard,推断之间的链接

目录 forward_to_next_shard 参数 函数逻辑 _process_prompt StandardNode get_current_shard map_partitions_to_shards forward_to_next_shard 这段代码定义了一个名为 forward_to_next_shard 的异步函数,它是设计用于在分布式模型或数据处理系统中的节点(或称为“分…

【秋招笔试】2024-08-03-科大讯飞秋招笔试题(算法岗)-三语言题解(CPP/Python/Java)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 💻 ACM金牌团队🏅️ | 多次AK大厂笔试 | 编程一对一辅导 ✨ 本系列打算持续跟新 秋招笔试题 👏 感谢大家的订阅➕ 和 喜欢💗 ✨ 笔试合集传送们 -> 🧷春秋招笔试合集 🍖 本次题目难度中等偏上,最后一题又是…