算法之前缀和

news2025/4/17 4:00:34

题目1: 【模板】一维前缀和(easy)

方法一: 暴力解法, 时间复杂度O(n*q), 当n=10^5, q = 10^5, 时间复杂度为O(10^10), 会超时.

方法二:

前缀和: 快速求出数组中某一段连续区间的和.

第一步: 预处理出来一个前缀和数组dp:

1. dp[i]表示区间[1,i]里所有元素的和

2. dp[i] = dp[i-1] + arr[i]

 为什么还有一个前缀和数组? 

使用暴力解法是先把arr存进去, 再遍历一次arr求和.

但是 求和 在存入原始数据的时候就能一起完成 O(n), 而区间[l,r]的和就是dp[r] - dp[l-1], 直接取出数据即可 O(q), 最终时间复杂度是O(n) + O(q), 实际上求前l项数据的和求前r项数据的和 本质是同一类问题,  可以把同一类问题抽象成一种状态表示, 进而用动态规划思想去解决.

 细节问题:

我们的数组都要像题目一样表示, 也就是下标从1开始计数, 为什么?

因为 dp[i] = dp[i-1] + arr[i] 这个公式, 当 i==0 的时候会出现dp[-1]越界访问, 而下标从1开始, 第一个数据是dp[1] = dp[0] + arr[1], 对于dp[0]保证其初始化的时候为0即可.

#include <iostream>
using namespace std;

int main() 
{
    int n,q;
    long long arr[100001]= {0};
    long long dp[100001] = {0};

    cin >> n >> q;

    //O(n)
    for(int i = 1; i <= n; i++)
    {
        cin >> arr[i];
        dp[i] = arr[i] + dp[i-1];
    }

    //O(q)
    for(int j = 0; j < q; j++)
    {
        long long l,r;
        cin >> l >> r;
        
        cout << dp[r] - dp[l-1] << endl;
    }

    return 0;
}
// 64 位输出请用 printf("%lld")

题目2: 【模板】二维前缀和(medium)

方法1: 暴力求解,时间复杂度O(m*n*q) 

方法2:前缀和 

2.1 利用一维前缀和: 

相当于x2x1+1个一维的前缀和相加, 只需要:
1. 确定好每次区间的起始坐标为y1+(x1-1)*m, 则终止坐标为起始坐标+(y2-y1), 
2. 起始坐标+=m, 重复循环计算 

#include <iostream>
using namespace std;

int main()
{
    int n, m, q;
    long long arr[1000001] = { 0 };
    long long dp[1000001] = { 0 };

    cin >> n >> m >> q;

    //O(n)
    for (int i = 1; i <= n * m; i++)
    {
        cin >> arr[i];
        dp[i] = arr[i] + dp[i - 1];
    }

    //O(q)
    for (int j = 0; j < q; j++)
    {
        long long x1, x2, y1, y2, sum = 0;
        cin >> x1 >> y1 >> x2 >> y2;
        long long start = y1 + ((x1 - 1) * m);
        long long gap = y2 - y1;

        for (int i = 0; i < x2 - x1 + 1; i++)
        {
            sum += dp[start+gap] - dp[start- 1];
            start += m;
        }
        cout << sum << endl;
    }

    return 0;
}
// 64 位输出请用 printf("%lld")

 2.2 二维的前缀和

a. 预处理一个前缀和矩阵

dp[i][j]表示: 从 [1][1] 位置到 [i][j] 位置这个矩阵的元素和.

可以把矩阵 i*j 沿 i 和 j 分为四块, 分别标记为A,B,C,D, 因为这样划分A+B 和 A+C 的面积可以分别用dp[i-1][j]和dp[i][j-1]表示, 而D的面积就是arr[i][j], 所以dp[i][j] = A+B  + A+C + D - A = dp[i-1][j] + dp[i][j-1] + dp[i-1][j] + arr[i][j] - dp[i-1][j-1]

b.使用这个前缀和矩阵 

将矩阵沿x1 y1划分成四份, D的面积推导同上:

#include <iostream>
using namespace std;

int main()
{
    int n, m, q;
    long long arr[1001][1001] = { 0 };
    long long dp[1001][1001] = { 0 };

    cin >> n >> m >> q;

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

    //O(q)
    for (int j = 0; j < q; j++)
    {
        long long x1, x2, y1, y2, sum = 0;
        cin >> x1 >> y1 >> x2 >> y2;
        cout<< dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1] << endl;
    }

    return 0;
}
// 64 位输出请用 printf("%lld")

题目3: 寻找数组的中心下标

直接利用前缀和:

同之前一样, 利用递推公式求出dp[i], 来表示前i个数的和:

 而后缀和可以用前缀和来表示: dp[n]-dp[i+1]

class Solution {
public:
    int pivotIndex(vector<int>& nums) 
    {
        int n = nums.size();

        vector<int> dp(n+1);
        dp[0] = 0;
        for(int i = 1; i <= n;i++)
        {
            dp[i] = dp[i-1] + nums[i-1];
        }

        for(int i = 0; i < n;i++)
        {
            if(dp[i] == dp[n]-dp[i+1])
                return i;
        }
        return -1;
    }
};

也可以用f[i]表示题意中的前缀和, g[i]表示题中的后缀和, 注意f[i]要从前向后初始化, 而g[i]要从后向前初始化, 最后用f[i] == g[i]进行比较即可:

class Solution {
public:
    int pivotIndex(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> f(n);
        vector<int> g(n);

        //初始化dp数组
        f[0] = 0;
        g[n-1] = 0;
        for(int i = 1; i < n;i++)
        {
            f[i] = f[i-1] + nums[i-1];
            g[n-1-i] = g[n-i] + nums[n-i];
        }

        //判断
        for(int i = 0; i < n;i++)
        {
            if(f[i] == g[i])
                return i;
        }
        return -1;
    }
};

题目4: 除自身以外数组的乘积

"前缀和"的思路和上一题几乎一模一样, 只是前缀和变成了前缀积, 注意把 f[0] 和 g[n-1] 设置为1:

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> f(n), g(n), answer(n);

        //初始化dp数组
        f[0] = 1;
        g[n-1] = 1;
        for(int i = 1; i < n;i++)
        {
            f[i] = f[i-1] * nums[i-1];
            g[n-1-i] = g[n-i]* nums[n-i];
        }
        for(int i = 0; i < n; i++)
            answer[i] = f[i]*g[i];

        return answer;
    }
};

 前缀和思路实际上是一种空间换时间的做法, 此题时间复杂度空间复杂度都是O(N)

优化一下上面的前缀积, 改成空间复杂度为O(1):

由于输出数组不算在空间复杂度内, 那么我们可以将用answer数组计算出前缀积, 然后再动态构造 后缀积得到结果, 最后的answer[i]从后向前计算, 即可很巧妙的利用到上次存放的answer[i]前缀积:

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> answer(n);

        //初始化dp数组
        answer[0] = 1;
        for(int i = 1; i < n;i++)
            answer[i] = answer[i-1] * nums[i-1];//此时answer[i]还是前缀积

        int R = 1;//动态更新后缀积
        for(int i = n-1; i >= 0; i--)
        {
            answer[i] = answer[i] * R;//最终结果,answer[i]为前缀积*后缀积
            R *= nums[i];//后缀积动态更新
        }

        return answer;
    }
};

题目5: 和为 k 的子数组(medium)

此时是连续的子数组, 看上去可以用滑动窗口解决, 但是不能, 因为 数据范围中有0和负数, 滑动窗口滑到和为k时不能停下来, 还要继续往后滑, 因为不确定后面有没有负数和0, 不具备单调性, 所以不能用滑动窗口

 前缀和+哈希表:

当下标为i时, 要找一段和为K的子数组, 只需要在[0, i-1]区间内找, 找有多少个前缀和等于sum[i] - k的子数组, 怎么去找呢?

每次求出sum[i]之后然后遍历一遍0~i-1, 这样时间复杂度还是O(n^2+n), 而暴力解法时间复杂度是O(n^2), 所以不能这样去找.

用一个哈希表去存放 前缀和等于sum[i]-k 出现的次数, 这样每次只需要 +=sum[i-k] 对应的值即可, 因为默认值是0.

注意: 

1.前缀和加入哈希表的时机? 

    在计算sum[i]之前, 只保存 [0,i-1] 位置的前缀和.

2. 如果 sum[i] 等于k, 那么它本身就是一个要找的子数组, 在哈希表中所对应的应该是sum[i]-k=0, 但是这个0相当于在[0,-1]区间内, 并不会被加入进哈希表, 所以在哈希表初始化时要令hash[0]=1, 避免这种情况被遗漏

3. 我们不用真的创建一个前缀和数组, 用一个sum变量去标记前一个前缀和即可.

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;
            ret += hash[sum-k];
            hash[sum]++;
        }
        return ret;
    }
};

题目6: 和可被 K 整除的子数组

前置知识: 

1. 同余定理:
如果 (a - b) % n == 0 , 那么我们可以得到一个结论:  a % n == b % n 。用文字叙述就是, 如果两个数相减的差能被 n 整除, 那么这两个数对 n 取模的结果相同.

2. c++ 中负数取模的结果, 以及如何修正「负数取模」的结果
        a. c++ 中关于负数取模运算, 因为c++整除是向零取整(余数符号和被除数一样), 而不是向下取整(余数符号和除数相等), 所以我们来推导一下上面的结论, 因为(a - b) % n == 0 , 所以a = b+kn,  两边同时取余即可得到上面的结论, 但是这里两边同时取余是有条件的, 假如以c++的取余规则, a是正数, b是负数, 这就导致a是向下取整(实际是向零取整, 但是方向都相同)得到的余数, 而b是向上取整(实际是向零取整, 方向是向上取整)得到的余数, 取余是不等价的, 所以需要把负数的取余操作进行修正, 以(a % n + n) % n 的形式输出保证为正.

了解了这些再来看这道题:假如一段区间和能被k整除, 也就是(sum-k)%k = 0, 根据同余定理, sum%k = x%k, 所以对于每一个sum, 只需找去找[0, i-1]区间里前缀和(x) = sum%k的次数即可, 又回归到了上一道题, 思路同上一道题, 注意当k==sum的时候, 需要找0, 所以hash[0]要初始化为1:

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;
            ret += hash[(sum%k+k)%k];
            hash[(sum%k+k)%k]++;
        }
        return ret;
    }
};

题目7: 连续数组(medium)

 此题如果直接去统计区间0和1的个数会发现很困难, 可以转化一下, 把所有的0看作-1, 含有相同数量的0和1的子数组的和就为0, 问题转化为求 和为0的子数组, 和之前的那道题类似.

前缀和 + 哈希:

1. 哈希表中存什么?

这题不是求和为k的子数组的数目, 而是求区间长度, 所以哈希表应该存 hash<前缀和, 下标>

2. 什么时候存入哈希表?

和之前一样, 判断完再存入 

3. 出现重复的<sum,i>, 该如何处理?

因为是从前向后遍历数组的, 所以下标i越小, 求出的区间就越长, 所以只存入第一个前缀和为sum的i即可. 

4. 区间长度怎么算?

如图, i-j 即为区间长度

5. hash[0]该如何初始化?

同之前一样, 可能当前的sum和就为0, 要去区间 [0,-1] 寻找0, 所以hash[0] = - 1

class Solution {
public:
    int findMaxLength(vector<int>& nums)
    {
        unordered_map<int, int> hash;
        hash[0] = -1; //默认有一个前缀和为0的情况

        int sum = 0, ret = 0, n = nums.size();
        for(int i = 0; i < n; i++)
        {
            sum += nums[i] == 0 ? -1 : 1;//计算当前前缀和
            if(hash.count(sum)) 
                ret = max(ret,i-hash[sum]);
            else //找不到sum才存下标
                hash[sum] = i;
        }
        return ret;
    }
};

题目8 : 矩阵区域和(medium)

题目的描述是让我们求出一个矩阵, 矩阵里每个元素answer[i][j] 是 mat[i][j]  周围k格内元素的和, 如果越界则以边界为界限.

解法: 利用二维前缀和

a. dp[i][j]表示: dp[i][j] = A+B  + A+C + D - A = dp[i-1][j] + dp[i][j-1] + dp[i-1][j] + arr[i][j] - dp[i-1][j-1]

b.使用这个前缀和矩阵 

所以只要我们给定两个点x1,y1 和 x2,y2, 对应矩阵的和可以求出来,  所以对于每一个answer[i][j], 我们只需要确定好x1,y1,x2,y2即可, 由题意, x1 = i-k, y1 = j-k, x2 = i+k, y2=j+k,  但是还要考虑越界情况, 所以x1 = max(i-k,0),  y1 = max(j-k,0), x2 = min(i+k, n-1), y2 = min(j+k, m-1)

下标的对应关系:

此题大体思路是这样, 但是注意我们原题中给的矩阵mat是下标从0开始的, 也就是说对应的dp[i][j]如果在边界上初始化会出现越界访问, 所以我们应该给dp数组开辟一个n+1*m+1大小的空间, 其中第0行第0列全部初始化为0, 方便dp[i][j]的计算, 与之对应的, 初始化公式也要进行修改:

dp里用到mat就要减1: 

 ans里用到dp就要加1, 因为ans的下标是对应着mat的下标:

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));//n+1 * m+1

        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                dp[i][j] = dp[i][j-1] + dp[i-1][j] + mat[i-1][j-1] - dp[i-1][j-1];
            }
        }

        vector<vector<int>> ret(n, vector<int>(m));//n * m
        for(int i = 0; i < n; i++)
        {
            for(int j = 0; j < m; j++)
            {
                int x1 = max(0,i-k)+1, y1 = max(0,j-k)+1;
                int x2 = min(n-1,i+k)+1, y2 = min(m-1,j+k)+1; 
                ret[i][j] = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1];
            }
        }
        return ret;
    }
};

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

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

相关文章

“币安悬赏500万美元”,调查BOME内幕!创始人:谁控制了Meme,谁就控制了宇宙!

自去年11月开始&#xff0c;背靠美帝资本力量的Solana在FTX交易所暴雷后重新崛起。目前Sol价格已经从熊市的8美刀上涨至200刀附近。除了Sol本币上涨外&#xff0c;Solana生态也是快速发展。 这其中又以Meme币最为突出&#xff01;西方玩家正把所有的梗都搬进链上制成Meme币&am…

echarts geo地图加投影两种方法

方法1&#xff0c;geo中加多个地图图形&#xff0c;叠加。缩放时 可能会不一致&#xff0c;需要捕捉georoam事件&#xff0c;使下层的geo随着上层的geo一起缩放拖曳 geo: [{zlevel: 3,//geo显示级别&#xff0c;默认是0 【最顶层图形】map: BJ,//地图名roam: true,scaleLimit: …

[AIGC] MySQL与PostgreSQL:两种流行的数据库系统的对比

数据库是存储和查询数据的重要工具。在选择数据库时&#xff0c;两个经常被考虑的选项都是开源的&#xff1a;MySQL和PostgreSQL。这两个数据库都与许多应用程序一起使用&#xff0c;但它们在某些方面存在显著的不同。在本文中&#xff0c;我们将比较MySQL和PostgreSQL的一些关…

基金分析之与行业间的相关系数计算

这几年买了非常多的基金&#xff0c;一直很好奇基金在非业绩披露期都持有了什么东西&#xff1f;所以写了一个基金净值和各申万一级行业相关系数&#xff08;以及和市场主流指数&#xff09;的代码看看能否分析出点东西。 这里依然用了wind API&#xff0c;复现前记得安装。 …

超快速排序(蓝桥杯,归并排序,acwing)

题目描述&#xff1a; 在这个问题中&#xff0c;您必须分析特定的排序算法----超快速排序。 该算法通过交换两个相邻的序列元素来处理 n 个不同整数的序列&#xff0c;直到序列按升序排序。 对于输入序列 9 1 0 5 4&#xff0c;超快速排序生成输出 0 1 4 5 9。 您的任务是确…

JavaScript基础知识2

求数组的最大值案例 let arr[2,6,1,7,400,55,88,100]let maxarr[0]let minarr[0]for(let i1;i<arr.length;i){max<arr[i]?maxarr[i]:maxmin>arr[i]?minarr[i]:min}console.log(最大值是&#xff1a;${max})console.log(最小值是&#xff1a;${min}) 操作数组 修改…

企业专业化管理金字塔:技能进阶与案例分析

在纷繁复杂的企业管理领域中&#xff0c;一套行之有效的管理技能体系对于企业的稳健发展至关重要。本文将深入探讨企业专业化管理金字塔的五个层次&#xff1a;基本的管理技能、业务操作管理技能、组织管理技能、组织开发技能以及管理转变技能&#xff0c;并结合实际案例&#…

Java后端八股------设计模式

Coffee可以设计成接口。 b

代码随想录算法训练营第27天|93.复原IP地址、78.子集、90.子集二

目录 一、力扣93.复原IP地址1.1 题目1.2 思路1.3 代码1.4 总结 二、力扣78.子集2.1 题目2.2 思路2.3 代码2.4 总结 三、力扣90.子集二3.1 题目3.2 思路3.3 代码3.4 总结 一、力扣93.复原IP地址 &#xff08;比较困难&#xff0c;做起来很吃力&#xff09; 1.1 题目 1.2 思路 …

30.网络游戏逆向分析与漏洞攻防-网络通信数据包分析工具-数据搜索功能

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果 内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;29.数据推测功能…

解决MySQL “Lock wait timeout exceeded; try restarting transaction“ 错误

在处理MySQL数据库时&#xff0c;我们偶尔会遇到一个棘手的错误消息&#xff1a;“Lock wait timeout exceeded; try restarting transaction”。这通常表明我们的一个事务在尝试获取资源时被阻塞了太长时间。在并发环境中&#xff0c;多个事务同时竞争相同的资源可能会导致这种…

前端 - 基础 表单标签 -- 表单元素( input - type属性) 文本框和密码框

表单元素 &#xff1a; 在表单域中可以定义各种表单元素&#xff0c;这些表单元素就是允许用户在表单中输入或选择 的内容控件。 表单元素的外观也各不一样&#xff0c;有小圆圈&#xff0c;有正方形&#xff0c;也有方框&#xff0c;乱七八糟的&#xff0c;各种各样&#xf…

关系数据库:关系数据结构基础与概念解析

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

2024/03/19总结

算法&#xff1a; 简单的一个dfs就能过 ac: #include "iostream" #include "algorithm" #include "cstring" #include "queue" using std::cin; using std::cout; using std::endl; #define N 110 char a[N][N]; int abook[N][N]; i…

算法|基础算法|二分和双指针

数学与数论|二分 1.基础二分 2.双指针 心有猛虎&#xff0c;细嗅蔷薇。你好朋友&#xff0c;这里是锅巴的C\C学习笔记&#xff0c;常言道&#xff0c;不积跬步无以至千里&#xff0c;希望有朝一日我们积累的滴水可以击穿顽石。 基础二分 模板 bool check(int x){/*...*/} …

spring suite搭建springboot操作

一、前言 有时候久了没开新项目了&#xff0c;重新开发一个新项目&#xff0c;搭建springboot的过程都有点淡忘了&#xff0c;所有温故知新。 二、搭建步骤 从0开始搭建springboot 1&#xff0e;创建work空间。步骤FileNewJava Working Set。 2.选择Java Working Set。 3.自…

Java八股文(RabbitMQ)

Java八股文のRabbitMQ RabbitMQ RabbitMQ RabbitMQ 是什么&#xff1f;它解决了哪些问题&#xff1f; RabbitMQ 是一个开源的消息代理中间件&#xff0c;用于在应用程序之间进行可靠的异步消息传递。 它解决了应用程序间解耦、消息传递、负载均衡、故障恢复等问题。 RabbitMQ …

SpringCloud Bus 消息总线

一、前言 接下来是开展一系列的 SpringCloud 的学习之旅&#xff0c;从传统的模块之间调用&#xff0c;一步步的升级为 SpringCloud 模块之间的调用&#xff0c;此篇文章为第八篇&#xff0c;即介绍 Bus 消息总线。 二、概述 2.1 遗留的问题 在上一篇文章的最后&#xff0c;我…

Gradle v8.5 笔记 - 从入门到进阶(基于 Kotlin DSL)

目录 一、前置说明 二、Gradle 启动&#xff01; 2.1、安装 2.2、初始化项目 2.3、gradle 项目目录介绍 2.4、Gradle 项目下载慢&#xff1f;&#xff08;万能解决办法&#xff09; 2.5、Gradle 常用命令 2.6、项目构建流程 2.7、设置文件&#xff08;settings.gradle.…

15|BabyAGI:根据气候变化自动制定鲜花存储策略

一种新型的代理——Autonomous Agents&#xff08;自治代 理或自主代理&#xff09;&#xff0c; 在 LangChain 的代理、工具和记忆这些组件的支持下&#xff0c;它们能够在无需外部干预的情况下自主 运行&#xff0c;这在真实世界的应用中具有巨大的价值。 AutoGPT 它的主要…