算法深度剖析:前缀和

news2024/11/26 20:45:09

文章目录

  • 前言
  • 一、一维前缀和模板
  • 二、二维前缀和模板
  • 三、寻找数组的中心下标
  • 四、除自身以外数组的乘积
  • 五、和为 K 的子数组
  • 六、和可被 K 整除的子数组
  • 七、连续数组
  • 八、矩阵区域和


前言

本章将深度剖析前缀和,以及总结前缀和模板。

前缀和是一种在算法和数据处理中的重要技巧,特别适合解决连续子数组求和的问题。通过构建一个前缀和数组,我们可以快速查询任意连续区间的和,从而在一定程度上优化时间复杂度。

基本原理
前缀和的核心思想是预先计算数组的前缀和,使得区间求和可以在常数时间内完成。假设有一个数组 ( arr ),其前缀和数组定义如下:

  • 设 ( prefix[i] ) 表示从数组起点到位置 ( i ) 的元素之和。
  • 因此,前缀和数组 ( prefix ) 可以定义为:
    [
    prefix[i] = arr[0] + arr[1] + \dots + arr[i]
    ]

计算任意区间和 ( arr[l] + arr[l+1] + \dots + arr[r] ) 可以通过前缀和快速得到:
[
arr[l] + arr[l+1] + \dots + arr[r] = prefix[r] - prefix[l-1]
]
其中, ( prefix[r] ) 是从 ( arr[0] ) 到 ( arr[r] ) 的和,减去从 ( arr[0] ) 到 ( arr[l-1] ) 的和就得到了区间 ( [l, r] ) 的和。

例子
假设有数组 ( arr = [1, 2, 3, 4, 5] ),构建前缀和数组 ( prefix ) 如下:

  • ( prefix[0] = 1 )
  • ( prefix[1] = 1 + 2 = 3 )
  • ( prefix[2] = 1 + 2 + 3 = 6 )
  • ( prefix[3] = 1 + 2 + 3 + 4 = 10 )
  • ( prefix[4] = 1 + 2 + 3 + 4 + 5 = 15 )

那么,求区间和 ( arr[1] + arr[2] + arr[3] ) 就可以通过前缀和数组计算:
[
arr[1] + arr[2] + arr[3] = prefix[3] - prefix[0] = 10 - 1 = 9
]

时间复杂度

  • 构建前缀和数组的时间复杂度为 ( O(n) ),其中 ( n ) 是数组的长度。
  • 一旦构建好前缀和数组,查询任意区间的和的时间复杂度为 ( O(1) )。

前缀和技术通常用于快速解决子数组求和、二维区域求和等问题。

在这里插入图片描述


一、一维前缀和模板

【模板】前缀和
在这里插入图片描述
在这里插入图片描述

#include <iostream>
using namespace std;
#include<vector>
typedef long long LL;

int n,q;

int main()
{
    cin >> n >> q;
    vector<LL> arr(n + 1);
    for (int i = 1; i <= n; i++)
    {
        cin >> arr[i];
    }

    //定义前缀和数组
    vector<LL> dp(n + 1);
    for (int i = 1; i <= n; i++)
    {
        dp[i] = dp[i - 1] + arr[i];
    }

    //使用前缀和数组
    while (q--)
    {
        LL l, r;
        cin >> l >> r;
        cout << dp[r] - dp[l - 1] << endl;
    }

    return 0;
}

二、二维前缀和模板

【模板】二维前缀和

在这里插入图片描述

在这里插入图片描述

#include <iostream>
using namespace std;

#include<vector>
typedef long long LL;

int main()
{
    int n, m, q;
    cin >> n >> m >> q;

    //初始化原始数据
    vector<vector<LL>> arr(n + 1, vector<LL> (m + 1));

    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            cin >> arr[i][j];
        }
    }

    //定义前缀和数组
    vector<vector<LL>> dp(n + 1, vector<LL> (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] + arr[i][j] - dp[i - 1][j - 1];
        }
    }

    //使用前缀和数组
    while (q--)
    {
        LL x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1 - 1][y1 - 1] << endl;
    }

    return 0;
}

三、寻找数组的中心下标

寻找数组的中心下标

在这里插入图片描述
在这里插入图片描述

算法思路:

根据中心下标的定义,除中心下标元素外,该元素左边的「前缀和」应等于右边的「后缀和」。

  • 因此,可以先预处理两个数组,一个表示前缀和,另一个表示后缀和
  • 然后,用一个 for 循环枚举可能的中心下标,判断每个位置的前缀和和后缀和是否相等,如果相等,则返回该下标。
class Solution {
public:
    int pivotIndex(vector<int>& nums) 
    {
        int n = nums.size();
        //构建前缀和
        vector<int> dp_first(n);
        dp_first[0] = 0;
        for (int i = 1; i < n; i++)
        {
            dp_first[i] = dp_first[i - 1] + nums[i - 1];
        }
        
        //构建后缀和
        vector<int> dp_end(n);
        dp_end[n - 1] = 0;
        for (int i = n - 2; i >= 0; i--)
        {
            dp_end[i] = dp_end[i + 1] + nums[i + 1];
        }

        //使用前缀和
        for (int i = 0; i < n; i++)
        {
            if (dp_first[i] == dp_end[i])
                return i;
        }
        return -1;
    }
};

四、除自身以外数组的乘积

除自身以外数组的乘积

在这里插入图片描述

算法思路:

题目要求不能使用除法,并要求在 O ( N ) O(N) O(N) 时间复杂度内完成,排除了暴力解法和计算数组乘积后除以单个元素的方法。

分析可知,每个位置的最终结果 ret[i] 可以分为两部分:

  1. 前缀积部分nums[0] * nums[1] * ... * nums[i - 1]
  2. 后缀积部分nums[i + 1] * nums[i + 2] * ... * nums[n - 1]

可以利用前缀和的思想,定义两个数组 postsuf,分别存储两部分信息:

  1. post:表示 i 位置之前所有元素的前缀乘积,即 [0, i - 1] 区间的乘积。
  2. suf:表示 i 位置之后所有元素的后缀乘积,即 [i + 1, n - 1] 区间的乘积。

最后,根据 postsuf 计算出每个位置的最终结果。
在这里插入图片描述

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) 
    {
        int n = nums.size();
        //构建前缀积
        vector<int> dp_first(n);
        dp_first[0] = 1;
        for (int i = 1; i < n; i++)
        {
            dp_first[i] = dp_first[i - 1] * nums[i - 1];
        }
        
        //构建后缀积
        vector<int> dp_end(n);
        dp_end[n - 1] = 1;
        for (int i = n - 2; i >= 0; i--)
        {
            dp_end[i] = dp_end[i + 1] * nums[i + 1];
        }

        //使用前后缀积
        vector<int> answer(n);
        for (int i = 0; i < n; i++)
        {
            answer[i] = dp_first[i] * dp_end[i];
        }
        return answer;
    }
};

五、和为 K 的子数组

和为 K 的子数组
在这里插入图片描述
(将前缀和存入哈希表):

算法思路:

i 为数组中的任意位置,sum[i] 表示 [0, i] 区间内所有元素的和。

  • 我们需要找到“以 i 为结尾且和为 k 的子数组”,这等价于找出所有可能的起始位置 x1, x2, x3...,使得 [x, i] 区间的和为 k
  • 此时,[0, x] 区间的和应为 sum[i] - k

因此,问题转化为:

  • 找出 [0, i - 1] 区间内有多少前缀和等于 sum[i] - k

无需真正初始化前缀和数组,因为我们只关注 i 位置之前,前缀和为 sum[i] - k 的次数。我们可以使用一个哈希表,在计算当前位置的前缀和时,同时记录每个前缀和出现的次数。

在这里插入图片描述

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) 
    {
        // 哈希表模拟前缀和数组
        unordered_map<int, int> hash;
        hash[0] = 1;

        //使用前缀和数组
        int sum = 0, ret = 0;

        for (auto e : nums)
        {
            sum += e;
            if (hash.count(sum - k)) ret += hash[sum - k];
            hash[sum]++;
        }
        return ret;
    }
}; 

六、和可被 K 整除的子数组

和可被 K 整除的子数组
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
本题需要的前置知识:

  • 同余定理
    (a - b) % n == 0,则 a % n == b % n。也就是说,如果两个数之差能被 n 整除,那么这两个数对 n 取模的结果相同。
    例如,(26 - 2) % 12 == 0,所以 26 % 12 == 2 % 12 == 2

  • C++ 中负数取模结果的处理

    • 在 C++ 中,负数取模的结果会保留负号,例如 -1 % 3 = -1
    • 为防止负数结果影响,常用 (a % n + n) % n 确保结果为正,例如:-1 % 3 = (-1 % 3 + 3) % 3 = 2

算法思路:

此题与 LeetCode 560 题“和为 K 的子数组”思路类似。

i 为数组中的任意位置,sum[i] 表示 [0, i] 区间内的和。

  • 要找出“以 i 为结尾、和可被 k 整除的子数组”,需要找到所有起点 x1, x2, x3... 使得 [x, i] 的和能被 k 整除。
  • 假设 [0, x - 1] 的和为 a[0, i] 的和为 b,则有 (b - a) % k == 0
  • 根据同余定理,[0, x - 1] 区间和 [0, i] 区间的前缀和同余。因此问题变成:
    • 找到 [0, i - 1] 中前缀和的余数等于 sum[i] % k 的个数。

无需初始化前缀和数组,只需用一个哈希表记录每种前缀和的出现次数,同时计算当前位置的前缀和。

class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) 
    {
        // 哈希表模拟前缀和数组
        unordered_map<int,int> hash;
        hash[0] = 1;
        //使用前缀和数组
        int sum = 0, ret = 0;
        for (auto e : nums)
        {
            sum += e;
            int r = (sum % k + k) % k;
            if (hash.count(r)) ret += hash[r];
            hash[r]++;
        }
        return ret;
    }
};

七、连续数组

连续数组

在这里插入图片描述

算法思路:

稍作转换,这道题可以化为经典问题:

  • 本题需要找一个连续区间,使得 0 和 1 出现的次数相同。
  • 将 0 视为 -1,1 视为 1,问题就转化为找一个区间,使其和等于 0。

这样问题与 LeetCode 560 题“和为 K 的子数组”思路相似。

i 为数组中任意位置,用 sum[i] 表示 [0, i] 区间内所有元素的和。我们希望找到最大长度的“以 i 为结尾、和为 0 的子数组”,这需要找到从左至右第一个位置 x1 使得 [x1, i] 的和为 0。此时 [0, x1 - 1] 区间的和等于 sum[i]。因此问题变成:

  • 找到 [0, i - 1] 区间内首次出现 sum[i] 的位置即可。

我们无需真正初始化一个前缀和数组,因为只关心 i 位置之前,首次出现等于 sum[i] 的前缀和位置。只需一个哈希表,在计算当前位置前缀和的同时,记录该前缀和的首次出现位置。
在这里插入图片描述

class Solution {
public:
    int findMaxLength(vector<int>& nums) 
    {
        // 哈希表模拟前缀和数组
        unordered_map<int, int> hash;
        hash[0] = -1; 

        // 使用前缀和数组
        int sum = 0, ret = 0;
        for (int i = 0; i < nums.size(); i++)
        {
            sum += nums[i] == 0 ? -1 : 1;
            if (hash.count(sum)) ret = max(ret, i - hash[sum]);
            else hash[sum] = i;
        }    
        return ret;
    }
};

八、矩阵区域和

矩阵区域和
在这里插入图片描述

算法思路:

这道题主要是二维前缀和的基本应用,关键在于填写结果矩阵时,找到原矩阵对应区域的「左上角」和「右下角」坐标(建议画图理解)。

  • 左上角坐标x1 = i - k,y1 = j - k,为了不超出矩阵范围,需要对 0 取 max,修正后的坐标为:x1 = max(0, i - k),y1 = max(0, j - k)
  • 右下角坐标x2 = i + k,y2 = j + k,同理,为避免超出矩阵范围,需要对 m - 1n - 1min,修正后的坐标为:x2 = min(m - 1, i + k),y2 = min(n - 1, j + k)

最后将修正后的坐标代入二维前缀和的计算公式即可(注意下标的映射关系)。
在这里插入图片描述

class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) 
    {
        // 构建二维前缀和
        int n = mat.size(), m = mat[0].size();
        vector<vector<int>> dp(n + 1, vector<int> (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] + mat[i - 1][j - 1] - dp[i - 1][j - 1];
            }
        }   

        //使用二维前缀和
        vector<vector<int>> answer(n, vector<int> (m));
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                int a, b, c, d;
                a = i - k < 0 ? 1 : i - k + 1;
                b = j - k < 0 ? 1 : j - k + 1;
                c = i + k >= n ? n : i + k + 1;
                d = j + k >= m ? m : j + k + 1;
                answer[i][j] = dp[c][d] - dp[c][b - 1] - dp[a - 1][d] + dp[a - 1][b - 1];
            }
        }
        return answer;
    }
};

在这里插入图片描述

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

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

相关文章

关于我、重生到500年前凭借C语言改变世界科技vlog.14——常见C语言算法

文章目录 1.冒泡排序2.二分查找3.转移表希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力&#xff01; 根据当前所学C语言知识&#xff0c;对前面知识进行及时的总结巩固&#xff0c;出了这么一篇 vlog 介绍当前所学知识能遇到的常见算法&#xff0c;这些算法是…

特定领域的Embeddings模型微调全面指南

假设你正在为医学领域构建一个问答系统。你希望确保当用户提出问题时&#xff0c;系统能够准确地检索相关的医学文章。但是通用的嵌入模型可能在处理医学术语的高度专业化词汇和细微差别时会遇到困难。 这时候&#xff0c;微调就能派上用场了&#xff01;&#xff01;&#xf…

视频推荐的算法(字节青训)

题目&#xff1a; 西瓜视频 正在开发一个新功能&#xff0c;旨在将访问量达到80百分位数以上的视频展示在首页的推荐列表中。实现一个程序&#xff0c;计算给定数据中的80百分位数。 例如&#xff1a;假设有一个包含从1到100的整数数组&#xff0c;80百分位数的值为80&#…

安卓13默认连接wifi热点 android13默认连接wifi

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.编译6.彩蛋1.前言 有时候我们需要让固件里面内置好,相关的wifi的ssid和密码,让固件起来就可以连接wifi,不用在手动操作。 2.问题分析 这个功能,使用普通的安卓代码就可以实现了。 3.代…

详解RabbitMQ三种队列类型

RabbitMQ 是一个强大的消息队列系统&#xff0c;它提供了多种队列类型以满足不同的使用需求。本文将探讨三种主要队列类型&#xff1a;经典队列、仲裁队列和流式队列&#xff0c;并讨论它们的区别和选型建议。 经典队列&#xff08;Classic Queues&#xff09; 简介&#xff…

CytoSPACE·空转和单细胞数据的高分辨率比对

1. 准备输入文件&#xff0c;需要四个文件&#xff0c;所有文件都应以制表符分隔的表格输入格式 (.txt) 提供。 a. scRNA-seq 基因表达文件 矩阵必须是基因&#xff08;行&#xff09;乘以细胞&#xff08;列&#xff09;。 第一行必须包含单个细胞 ID&#xff0c;第一列必须…

react使用Fullcalendar

前言&#xff1a; 最近在做项目时&#xff0c;遇到了需要用日历的项目。一开始考虑使用antd的日历组件。后来 调研技术库&#xff0c;发现了fullcalendar 库。经过对比 fullcalendar 更强大&#xff0c;更灵活。 其实 antd的日历组件 也不错&#xff0c;简单的需求用他也行。…

LabVIEW过程控制实验平台

A3000实验平台通过LabVIEW开发&#xff0c;实现了过程控制的虚拟仿真与实时通信&#xff0c;显著提高了教学与实验的互动性和效率。该平台采用模块化设计&#xff0c;支持多种控制策略的实验教学&#xff0c;克服了传统实验设备的不足。项目背景 目前高校过程控制实验设备普遍…

腾讯会议pc端3.29.11开启悬浮窗口

之前是&#xff1a;pc端每次最小化&#xff0c;它就自动收回到任务栏里了 版本&#xff1a;3.29.11 解决办法&#xff1a; 打开腾讯会议&#xff0c;点击左上角的【头像】。 单击【设置】。 选择【显示当前说话者】来管理麦克风浮窗。 再进入会议&#xff0c;点击最小化一哈&…

聊一聊:ChatGPT搜索引擎会取代谷歌和百度吗?

当地时间 10 月 31 日&#xff0c;OpenAI 正式推出了 ChatGPT 搜索功能&#xff0c;能实时、快速获取附带相关网页来源链接的答案。这一重大升级标志着其正式向谷歌的搜索引擎霸主地位发起挑战。 本周五我们聊一聊&#xff1a; 欢迎在评论区畅所欲言&#xff0c;分享你的观点~ …

贪心算法习题其四【力扣】【算法学习day.21】

前言 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&am…

python实战项目52:Selenium爬取steam黑神话悟空评论

python实战项目52:Selenium爬取steam黑神话悟空评论 一、思路分析二、完整代码一、思路分析 Selenium爬取steam游戏评论的思路非常简单,初始化Chromedriver,然后打开评论页面,循环下拉滚动条,每下拉一次滚动条获取一次页面源代码,使用xpath解析数据并保存数据。本文的主…

Claude 3.5 新功能 支持对 100 页的PDF 图像、图表和图形进行可视化分析

Claude 3.5 Sonnet发布PDF图像预览新功能&#xff0c;允许用户分析长度不超过100页的PDF中的视觉内容。 此功能使用户能够轻松上传文档并提取信息&#xff0c;特别适用于包含图表、图形和其他视觉元素的研究论文和技术文档。 视觉PDF分析&#xff1a;用户现在可以从包含各种视觉…

【Qt c++】Qt内置图标

Qt内置图标 前言简例示例 前言 Qt内置图标封装在QStyle中&#xff0c;大概七十多个图标&#xff0c;可以直接拿来用。图标的大小&#xff1a;我认为 size 30 还是可以的. 简例 SP_TitleBarMenuButton, SP_TitleBarMinButton, SP_TitleBarMaxButton, SP_TitleBarCloseButton…

Redis 的使⽤和原理

第一章:初识 Redis 1.1盛赞 Redis Redis 是⼀种基于键值对&#xff08;key-value&#xff09;的 NoSQL 数据库&#xff0c;与很多键值对数据库不同的是&#xff0c;Redis 中的值可以是由 string&#xff08;字符串&#xff09;、hash&#xff08;哈希&#xff09;、list&…

【Clikhouse 探秘】ClickHouse 物化视图:加速大数据分析的新利器

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

atest v0.0.18 提供了强大、灵活的 HTTP API Mock 功能

atest 发布 v0.0.18 atest 是致力于帮助开发者持续保持高质量 API 的开源接口工具。 你可以在命令行终端或者容器中启动&#xff1a; docker run -p 8080:8080 ghcr.io/linuxsuren/api-testing:v0.0.18 亮点 在开源之夏 2024 中 atest 增加了基于 MySQL 的测试用例历史的支持HT…

深度了解flink(十) JobManager(4) ResourceManager HA

ResourceManager&#xff08;ZK模式&#xff09;的高可用启动流程 ResourceManager启动流程在DefaultDispatcherResourceManagerComponentFactory#create中 public DispatcherResourceManagerComponent create(Configuration configuration,ResourceID resourceId,Executor i…

Linux系统编程——信号的基本概念(信号产生于处理、可靠信号、可重入函数、SIGCHLD)

一、什么是信号 1、信号的定义 信号是UNIX和Linux系统响应某些条件而产生的一个事件&#xff0c;接收到该信号的进程会相应地采取一些行动。信号是软中断&#xff0c;通常信号是由一个错误产生的。但它们还可以作为进程间通信或修改行为的一种方式&#xff0c;明确地由一个进程…

节省50%人工录入时间!免费开源AI工具让法律文件数据提取更高效

法律行业痛点&#xff1a;处理大量的合同、诉讼材料和财务报告等文件是一项繁琐且耗时的工作。这些文件中的表格常包含关键信息&#xff0c;如费用清单、时效统计和条款列表等&#xff0c;手动录入和整理这些数据不仅效率低下&#xff0c;而且容易出错。表格识别技术&#xff0…