当前位置: 首页 > news >正文

前缀和算法

目录

1. 前缀和算法的基本原理

1.1 一维前缀和

1.2 二维前缀和

2. 一维前缀和的实现

2.1 代码示例

2.2 查询子数组和

3. 二维前缀和的实现

4. 前缀和列题:

和为K的子数组

和可被K整除的子数组

矩阵区域和

5. 前缀和算法的应用场景

5.1 快速求和

5.2 滑动窗口问题

5.3 矩阵操作

5. 注意事项

6. 总结


前缀和算法是一种非常实用的算法思想,广泛应用于数组和矩阵的求和问题中。它通过预先计算并存储部分结果,从而在后续查询中快速得到答案,大大提高了效率。以下是对前缀和算法的详细讲解,包括其基本原理、实现方法和应用场景。

1. 前缀和算法的基本原理

1.1 一维前缀和

对于一个数组 arr,其前缀和数组 prefix 定义为:

  • prefix[i] 表示数组 arr 从第 0 个元素到第 i 个元素的累加和,即:

通过前缀和数组,可以快速计算任意子数组的和。例如,计算从索引 i 到索引 j 的子数组和,可以直接通过以下公式得到:

(注意:当 i = 0 时,prefix[-1] 可以认为是 0)。

1.2 二维前缀和

对于一个二维数组 matrix,其前缀和数组 prefix 定义为:

  • prefix[i][j] 表示从 (0, 0)(i, j) 的矩形区域内的元素和,即:

 prefxi[i][j]=prefxi[i][j-1]+prefxi[i-1][j]+arr[i][j]-prefxi[i-1][j-1]

通过前缀和数组,可以快速计算任意子矩阵的和。例如,计算从 (x1, y1)(x2, y2) 的子矩阵和,可以直接通过以下公式得到:

sum(x1,y1,x2,y2)=prefix[x2][y2]−prefix[x1−1][y2]−prefix[x2][y1−1]+prefix[x1−1][y1−1]

(注意:当 x1 = 0y1 = 0 时,对应的前缀和值可以认为是 0)。


2. 一维前缀和的实现

2.1 代码示例

以下是一个计算一维前缀和的 C++ 代码示例:

cpp复制

#include <iostream>
#include <vector>
using namespace std;

vector<int> calculatePrefixSum(const vector<int>& arr) {
    int n = arr.size();
    vector<int> prefix(n, 0);
    prefix[0] = arr[0];
    for (int i = 1; i < n; ++i) {
        prefix[i] = prefix[i - 1] + arr[i];
    }
    return prefix;
}

int main() {
    vector<int> arr = {1, 2, 3, 4, 5};
    vector<int> prefix = calculatePrefixSum(arr);
    for (int i : prefix) {
        cout << i << " ";
    }
    return 0;
}

题目:前缀和(模板)

讲解:

假设你有一个数组,比如 [3, 1, 4, 1, 5, 9],现在需要频繁地计算某个区间内的数字之和。比如,计算从第 2 个数字到第 4 个数字的和(1 + 4 + 1 = 6)。如果每次都从头计算,会很麻烦,尤其是当数组很大、查询很多的时候。这时候,前缀和算法就能派上用场。

算法思路:

1. 准备工作:读入数据

  • 首先,我们知道数组的长度(n)和要查询的次数(q)。

  • 然后,把数组的每个数字读进来。注意,代码里数组是从索引 1 开始存的,这样方便处理边界情况。

2. 预处理:计算前缀和

  • 创建一个新的数组 dp,用来存前缀和。dp[i] 表示从数组的第一个数字加到第 i 个数字的总和。

  • 举个例子,对于数组 [3, 1, 4, 1, 5, 9],前缀和数组 dp 就是 [0, 3, 4, 8, 9, 14, 23]。这里 dp[0] 是 0,方便计算。

  • 计算方法很简单:dp[i] = dp[i - 1] + arr[i]。也就是说,第 i 个位置的前缀和等于第 i - 1 个位置的前缀和加上第 i 个数字。

3. 查询:快速计算区间和

  • 每次查询的时候,输入区间的左右边界 lr

  • 利用前缀和数组,可以直接算出区间 [l, r] 的和,公式是:dp[r] - dp[l - 1]

  • 这个公式的意思是:dp[r] 是从第一个数字加到第 r 个数字的总和,dp[l - 1] 是从第一个数字加到第 l - 1 个数字的总和。相减后,就得到了从第 l 个数字到第 r 个数字的和

#include <iostream>
using namespace std;
#include <vector>
int main() 
{
    //读入数据
    int n,q;
    cin>>n>>q;
    vector<int> arr(n+1);
    
    for(int i = 1;i<=n;i++)
    {
        cin>>arr[i];
    }
    //预处理数组
    vector<long long> dp(n+1);
    //dp[0] = 0;
    for(int i = 1;i<=n;i++)
    {
        dp[i] = dp[i-1]+arr[i];
    }

    //使用前缀数组和
    int l = 0,r= 0;
    while(q--)
    {
        cin>>l>>r;
        cout<<dp[r]-dp[l-1]<<endl;
    }
    return 0;
}
// 64 位输出请用 printf("%lld")

2.2 查询子数组和

通过前缀和数组,可以快速查询任意子数组的和:

cpp复制

int querySum(const vector<int>& prefix, int i, int j) {
    if (i == 0) {
        return prefix[j];
    }
    return prefix[j] - prefix[i - 1];
}

3. 二维前缀和的实现

二维前缀和

以下是一个计算二维前缀和的 C++ 代码示例:

#include <iostream>
#include <vector>
using namespace std;

int main() 
{
    //1.矩阵构建
    int n,m,q;
    cin>>n>>m>>q;
   vector<vector<long>> arr(n+1,vector<long>(m+1));
   for(int i = 1;i<n+1;i++)
   {
        for(int j = 1;j<=m;j++)
        {
            cin>>arr[i][j];
        }
   }
    //2.构造前缀和数组
     vector<vector<long>> dp(n+1,vector<long>(m+1));
     for(int i = 1;i<n+1;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];
        }
   }
   //3.使用前缀和矩阵进行查询
   
   int sum = 0;
   int x1,y1,x2,y2;
   int c  = q;
   while(q--)
   {
    cin>>x1>>y1>>x2>>y2;
    cout<<dp[x2][y2]-dp[x2][y1-1]-dp[x1-1][y2]+dp[x1-1][y1-1]<<endl;
   }
    

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

4. 前缀和列题:

和为K的子数组

该题的暴力解法比较简单,之间枚举即可,不过可能会超时。

这里要求是连续的非空数组,这里不能用滑动窗口,因为滑动窗口需要符合单调性,如果有负数进入窗口,窗口内的和会减少。

这里我们用前缀法+哈希来解决:

  1. 前缀和
    前缀和是指从数组的开头到某个位置的累加和。例如,对于数组 [1, 2, 3, 4],前缀和为:

    • prefix[0] = 1

    • prefix[1] = 1 + 2 = 3

    • prefix[2] = 1 + 2 + 3 = 6

    • prefix[3] = 1 + 2 + 3 + 4 = 10

    如果我们要求子数组 [i, j] 的和,可以用 prefix[j] - prefix[i-1] 来快速计算。

  2. 子数组和为 k 的条件
    假设我们已经计算了当前位置的前缀和 sum,那么如果存在一个子数组的和为 k,那么一定满足:

    sum - prefix[i-1] = k

    变形后得到:

    prefix[i-1] = sum - k

    这意味着,只要我们在之前的前缀和中找到过 sum - k,那么就说明存在一个子数组的和为 k

        3.哈希表的作用
        为了快速查找 sum - k 是否出现过,我们用哈希表记录每个前缀和出现的次数。这样,每              次计算新的前缀和时,只需要检查 sum - k 是否在哈希表中。

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> hash; // 哈希表,记录前缀和的出现次数
        hash[0] = 1; // 初始化:前缀和为0的情况出现1次
        int sum = 0, ret = 0; // sum为当前前缀和,ret为结果计数

        for (auto& x : nums) { // 遍历数组
            sum += x; // 计算当前前缀和

            // 检查sum - k是否在哈希表中
            if (hash.count(sum - k)) { 
                ret += hash[sum - k]; // 如果存在,说明存在和为k的子数组,更新结果
            }

            // 更新哈希表:当前前缀和sum的出现次数加1
            hash[sum]++;
        }

        return ret; // 返回结果
    }
};

关键点解释

  1. hash[0] = 1 的作用
    这是一个重要的初始化操作。它表示“前缀和为0的情况出现1次”。这样做的目的是为了处理从数组开头到某个位置的子数组和为 k 的情况。如果没有这个初始化,代码会漏掉这种情况。

    例如:数组 [1, 2, 3]k = 3
    如果没有 hash[0] = 1,当 sum = 3 时,sum - k = 0,但哈希表中没有 0,会漏掉子数组 [1, 2]

  2. sum += x 的作用
    每次循环,计算当前的前缀和 sum,即从数组开头到当前位置的累加和。

  3. if (hash.count(sum - k)) 的作用
    检查 sum - k 是否在哈希表中。如果存在,说明之前有某个前缀和等于 sum - k,那么从那个位置到当前位置的子数组和就是 k
    例如:当前 sum = 5k = 2,如果哈希表中有 3,那么从 sum = 3sum = 5 的子数组和就是 2

  4. hash[sum]++ 的作用
    每次计算完新的前缀和后,将其加入哈希表,并更新出现次数。这样,后续计算时可以快速查找

和可被K整除的子数组

 本篇以前缀和解法为主,所以这里我们依然用哈希+前缀和来解决。

本题和上题解法基本类似,但这里找的是可以被K整除的子数组,能被整除,说明模为0,比如(5+0)%5=0。

那么我们这里需要补充两个知识点:

1. 同余定理

2.负数%正数的结果修正

算法思路:

由图中可知,假设sum-x这个子数组区间的和必然能被K整除,那么我们可以得到(sum-x)%k = 0,由同余定理可以得到,sum%K = X% K。
此时我们只需要找在[0,i-1]区间内有多少个前缀和的余数x%k等于sum%k。
但是sum%k可能算出来是负的,比如数组nums[]={-7,8},k=4,这个时候前缀和数组为[-7,1],这个时候如果用sum%K来算,-7%4=-3,1%4 = 1,这里我们就忽略了一个单独数字的子数组{8},所以我们这个时候需要把负数转为正数,在Java和C++中使用(sum%K+K)%K,这个时候代入可以得到(-7%4+4)%4=3,(1%4+4)%4 = 3,通过哈希表可以统计前面已经出现过一次3,所以从当前下标开始[1]开始到sum[i]的i下标是一个新的子数组区间。
 

class Solution
{
public:
    int subarraysDivByK(vector<int>& nums, int k) {
        int sum = 0;
        unordered_map<int, int> hash;
        int ret = 0;
        hash[0] = 1; // 初始化,处理前缀和为0的情况
        for (auto& x : nums) {
            sum += x;
            if (hash.count((sum % k + k) % k)) // 检查是否存在相同余数
                ret += hash[(sum % k + k) % k];
            hash[(sum % k + k) % k]++; // 更新哈希表,存储当前余数的频率
        }
        return ret;
    }
};

连续数组

 本题依然使用前缀和+哈希解决

这里我们把0看成-1,这样就相当于找和为0的子数组,和前面那一道和为k的子数组解法基本类似。只是这里是有一些细节不同。

算法解析

1. 前缀和的转换

数组中的元素只有 0 和 1,为了方便处理,我们将 0 转换为 -1,1 保持不变。这样,问题就变成了找到一个最长的子数组,使得该子数组的和为 0。具体来说:

  • 如果 nums[i] == 0,则将其视为 -1

  • 如果 nums[i] == 1,则保持为 1

例如,对于数组 [0, 1, 0, 1],转换后为 [-1, 1, -1, 1]

2. 哈希表的作用

我们用一个哈希表 hash 来记录每个前缀和第一次出现的位置。哈希表的键是前缀和,值是该前缀和第一次出现的索引。

  • 初始化时,hash[0] = -1。这是因为如果从索引 0 开始的子数组满足条件(即前缀和为 0),那么它的长度应该是 i - (-1) = i + 1

3. 遍历数组

我们遍历数组,计算当前的前缀和 sum

  • 如果当前前缀和 sum 已经在哈希表中出现过,说明从第一次出现该前缀和的位置到当前位置的子数组的和为 0(即 0 和 1 的数量相等)。

  • 更新结果 ret 为当前子数组的长度 i - hash[sum]

  • 如果当前前缀和 sum 没有在哈希表中出现过,则将其加入哈希表,记录其第一次出现的位置。

4. 返回结果

最终,ret 中存储的就是最长满足条件的子数组的长度。

class Solution {
public:
    int findMaxLength(vector<int>& nums) {
        unordered_map<int, int> hash; // 哈希表:存储前缀和及其第一次出现的位置
        hash[0] = -1; // 初始化:前缀和为0时,位置为-1
        int sum = 0, ret = 0; // sum:当前前缀和,ret:最长子数组长度

        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i] == 0 ? -1 : 1; // 将0转换为-1,1保持不变,计算前缀和

            if (hash.count(sum)) { // 如果当前前缀和已经出现过
                ret = max(ret, i - hash[sum]); // 更新最长子数组长度
            } else { // 如果当前前缀和没有出现过
                hash[sum] = i; // 记录当前前缀和第一次出现的位置
            }
        }

        return ret; // 返回最长子数组长度
    }
};

矩阵区域和

class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
        int m = mat.size();
        int n = mat[0].size();
        vector<vector<int>> dp(m+1,vector<int>(n+1));
        //1.预处理一个前缀和二维数组
        for(int i = 1;i<m+1;i++)
        {
            for(int j =1;j<n+1;j++)
            {
                dp[i][j] = dp[i-1][j]+dp[i][j-1]+mat[i-1][j-1]-dp[i-1][j-1];
            }
        }
        //2.插入前缀和的计算
        vector<vector<int>> answer(m,vector<int>(n));
        for(int i =0;i<m;i++)
        {
            for(int j = 0;j<n;j++)
            {
                // 计算左上角坐标 (row1, col1) 和右下角坐标 (row2, col2)
                //防止越界
               int row1 = max(0,i-k),col1=max(0,j-k);
               int row2 = min(i+k,m-1),col2 =min(j+k,n-1);
               answer[i][j] = dp[row2+1][col2+1]-dp[row2+1][col1]-dp[row1][col2+1]+dp[row1][col1];
            }
        }

        return answer;
    }
};

5. 前缀和算法的应用场景

5.1 快速求和

前缀和算法可以快速计算任意子数组或子矩阵的和,时间复杂度为 O(1)。这在处理大量查询时非常高效。

5.2 滑动窗口问题

在滑动窗口问题中,前缀和可以快速计算窗口内的和,从而避免重复计算。

5.3 矩阵操作

在二维数组中,前缀和可以用于快速计算任意子矩阵的和,适用于图像处理、矩阵分析等场景。


6. 注意事项

  • 边界条件:在计算前缀和时,需要注意边界条件,例如数组或矩阵的大小为 0 的情况。

  • 空间复杂度:前缀和算法需要额外的空间来存储前缀和数组,空间复杂度为 O(n) 或 O(n * m)。

  • 适用范围:前缀和算法适用于求和问题,但对于其他类型的操作(如最大值、最小值等),可能需要结合其他数据结构(如线段树)。


7. 总结

前缀和算法是一种简单而高效的算法思想,通过预先计算并存储部分结果,可以在后续查询中快速得到答案。它在处理数组和矩阵的求和问题中非常实用,特别是在需要大量查询的情况下。掌握前缀和算法的基本原理和实现方法,可以帮助你在解决相关问题时更加高效。

相关文章:

  • 【车规芯片】如何引导时钟树生长方向
  • 【STM32】玩转IIC之驱动MPU6050及姿态解算
  • c语言笔记 指针篇(上)
  • 8.1.STM32_OLED
  • Java实现大数据量导出报表
  • Select 下拉菜单选项分组
  • 面试基础----Spring Cloud 微服务架构中的熔断降级:Hystrix 与 Resilience4j 解析
  • 以影像技术重构智能座舱体验,开启驾乘互动新纪元
  • RK3588V2--ES8388声卡适配记录
  • Leetcode---209长度最小子数组
  • 代码贴——堆(二叉树)数据结构
  • 智能对讲机:5G+AI赋能下的石油工业新“声”态
  • linux top htop 命令有什么不同
  • vue These dependencies were not found
  • 【mysql】mysql数据库数据导入、导出/备份还原操作
  • 16.1STM32_ADC
  • 微软AI900认证备考全攻略:开启AI职业进阶之路
  • android13打基础:控件datepicker
  • 【代码分享】基于IRM和RRT*的无人机路径规划方法详解与Matlab实现
  • 中科大 计算机网络组成原理 1.4 接入网和物理媒体 笔记
  • 企业网站建设与实施调查报告/广告联盟怎么赚钱
  • 网站建设修改/汕头seo按天付费
  • 网站建设售后培训/软文代写新闻稿
  • 建立一个购物网站平台费用/pr的选择应该优先选择的链接为
  • 棋牌网站开发搭建/河南平价的seo整站优化定制
  • 做外贸在什么网站好/优秀的软文