算法——前缀和

模板一维前缀和

【模板】前缀和_牛客题霸_牛客网
该算法是先预处理一个数组,用空间换时间,将原本时间复杂度为O(n2)降为O(n)

题目解析

题中下标(用i表示)从1开始计数,长度为n的数组,想访问到an 位置,创建数组时要创建大小为n+1的数组算法——前缀和_第1张图片

算法原理

  1. 解法一:暴力解法——模拟:题中q次询问,每次询问按要求从头遍历即可,时间复杂度为O(n*q),根据题中的数据范围,大概为O(1010)算法——前缀和_第2张图片

  2. 解法二:前缀和——快速求出数组某一连续区间的和(快速时间复杂度O(1),相比解法一遍历O(n)快许多),整体时间复杂度O(q)+O(n)。这个O(n)是我们在预处理前缀和数组时需要遍历一遍原数组。

    1. 先预处理出一个前缀和数组,先创建一个和原始数组同规模的数组dp(本质上是一个小的动态规划)。dp数组中某一个位置的元素dp[i]表示[1,i]区间所有元素的和。算法——前缀和_第3张图片

    2. 使用前缀和数组,例如求[l,r]区间的和时,我们只需要求出紫色线区间的和,减去绿色直线的和即可,即dp[r]-dp[l-1];因为我们上一步求出了dp的数组,所以这一步的时间复杂度为O(1),这也是相比暴力解法快速的原因。算法——前缀和_第4张图片

因为[1,r]这段区间的和与[1,l]这段区间的和本质上是同一类问题,当我们研究同一类问题时,我们可以把这些同一类问题抽象成状态表示,进而用动态规划的思想解决。

  1. 细节问题——为什么下标要从1开始计数?

如果我们要访问[0,2]区间的数组时,根据上面总结的我们需要访问dp[2]和dp[-1]这两段区间的数组和,但是-1这个位置访问不到,此时需要处理边界情况。但我们从下标1开始计数不会有问题(dp[0]置为0即可,不影响前缀和)
算法——前缀和_第5张图片

代码实现

#include 
#include
using namespace std;

int main() 
{
    //1.读取数据
    int n,q;
    cin >> n >> q;
    vector<int> arr(n+1);
    for(int i = 1;i <= n;i++) cin >> arr[i];
    
    //2.预处理前缀和数组
    vector<long long> dp(n+1);   //防止溢出
    for(int i=1;i<=n;i++) dp[i] = dp[i-1]+arr[i];

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

模板——二维前缀和

【模板】二维前缀和_牛客题霸_牛客网

题目解析

算法——前缀和_第6张图片

算法原理

解法一:暴力解法——模拟
时间复杂度O(mnq)

解法二:前缀和(时间复杂度O(m*n)+O(q))

  1. 预处理出前缀和矩阵(这里遍历矩阵时间复杂度O(m*n))算法——前缀和_第7张图片

    1. 这里我们求dp[i][j]如果还像上个题一样从头开始遍历,那创建dp表所用的时间复杂度一定非常高。因此我们先找个规律,快速求出dp[i][j]的值此时我们把这个图抽象出来。我们把arr的值划分为四个部分:A部分的面积可以表示为[1][1]到[i-1][j-1](该元素为右下角)这块区间的和,但我们发现这样直接一块块的算BCD非常麻烦,所以我们可以转化成A+B([1][1]到[i-1][j]) A+C([1][1]到[i][j1]) D(arr[i][j]) -A([1][1]到[i-1][j-1])。这样我们求dp[i][j]时直接套用公式,用O(1)的时间复杂度求出,当求整个dp矩阵时只需遍历一遍矩阵就可以全部求出dp矩阵。算法——前缀和_第8张图片
  2. 使用前缀和矩阵(每一次求消耗时间复杂度O(1),一共q次即为O(q))

题中要求求出[x1][y1]到[x2][y2]区间和时,此时我们还可以将他划分为四个部分,即区间D为题中要求。我们可以先把图中整个和求出来(A+B+C+D)然后减去其他部分面积。求得公式后,我们接下来求区间就可以用O(1)的时间复杂度求
算法——前缀和_第9张图片

  1. 细节问题:和一维那里一样。我们要在矩阵的最上⾯和最左边添加上⼀⾏和⼀列 0,这样我们就可以省去⾮常多的边界条件的处理(可以⾃⾏尝试直接搞出来前缀和矩阵,边界条件的处理会让你崩溃的)。处理后的矩阵就像这样:

这样,我们填写前缀和矩阵数组的时候,下标直接从 1 开始,能⼤胆使⽤ i - 1 , j - 1 位置的值。

注意: dp 表与原数组内的元素的映射关系:
i. 从 dp 表到原矩阵,横纵坐标减⼀;
ii. 从原矩阵到 dp 表,横纵坐标加⼀

算法——前缀和_第10张图片

代码实现

#include 
using namespace std;
const int N = 1010;
int arr[N][N];
long long dp[N][N];
int n, m, q;
int main() 
{
    cin >> n >> m >> q;
    // 读⼊数据
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> arr[i][j];
    // 处理前缀和矩阵
    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];
    // 使⽤前缀和矩阵
    int x1, y1, x2, y2;
    while (q--) 
    {
        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;
}

寻找数组的中心下标

寻找数组的中心下标

题目解析

  • 数组** 中心下标 **是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
  • 如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
  • 如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。算法——前缀和_第11张图片

算法原理

  1. 解法一:暴力解法:先算出[0,i-1]这个区间的和,在算出[i+1,n-1]这段区间的和,比较两者是否相等。每次枚举一个中心下标,就让左边加一遍,右边加一遍。时间复杂度O(n2)

算法——前缀和_第12张图片

  1. 解法二:前缀和:刚好前缀和是用来记录某一段连续区间的和。这里我们用f记录前缀和数组,后g表示后缀和数组。
    1. f[i]表示:[0,i-1]区间的和(不要死记模板,要根据具体情况具体分析);g[i]表示:[i+1,n-1]区间的和
    2. 预处理前缀和数组和后缀和数组:f[i] = f[i-1]+nums[i-1] g[i]=g[i+1]+nums[i+1] 后缀数组倒着填(f[i-1]表示0~i-2区间的和 g[i+1]表示从i+2~n-1区间的和
    3. 从0~N-1枚举所有的中心下标i,然后判断f[i]是否等于g[i].(因为如果有多个中心下标,要选出左边的)
  2. 细节问题:
    1. f[0]和g[n-1]要特殊处理,因为两种情况是最左边和最右边。各有一边的和为0,所以将两端数字都置为0,不影响求和
    2. 填表顺序f要从左向右。g要从右向左

算法——前缀和_第13张图片

代码实现

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

        //1.预处理前缀数组和后缀数组
        for(int i=1;i<n;i++)
          f[i] = f[i-1] + nums[i-1];

        for(int i=n-2;i>=0;i--) //倒着填,最后一个位置是n-1,但是g[n-1]位置上的值是0就可以,n-1会越界
           g[i] = g[i+1]+nums[i+1]; 
        
        //2.使用
        for(int i=0;i<n;i++)
         if(f[i] == g[i])
           return i;

        return -1;   
    }
};

出自身以外数组的乘积

除自身以外数组的乘积

题目解析

  • 除自身以外其他数的乘积
  • 不能使用除法
  • 时间复杂度要求O(n)

算法——前缀和_第14张图片

算法原理

  1. 暴力解法:边枚举位置,边计算乘积。时间复杂度O(n2)
  2. 前缀和:利用“前缀和”数组和“后缀和”数组计算除自身位置两边的数的乘机
    1. 预处理数组:f表示前缀积,f[i]:我们只需要考虑0i-1区间数字的乘积。当我们求0i-1这段区间的乘积时,我们已经知道0~i-2这段区间的乘积,即f[i-1],则f[i] = f[i-1]*nums[i-1]

g表示后缀积,g[i]表示i+1n-1这段区间的积,同理想求这段区间的乘积时,我们已经直到了i+2n-1这段区间的乘积(即g[i+1]已知)则g[i] = g[i+1]*nums[i+1]

  1. 使用:先创造一个和原始数组同规模的数组ret(也就是dp)
  2. 细节问题:f(0)与g(n-1)设置为1,与上道题同理,只不过这题是乘积,不能变为0

算法——前缀和_第15张图片

代码实现

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

        //1.预处理前缀数组和后缀数组
        f[0] = g[n-1] = 1; //细节问题
        for(int i=1;i<n;i++)
          f[i] = f[i-1] * nums[i-1];

        for(int i=n-2;i>=0;i--) //倒着填,最后一个位置是n-1,但是g[n-1]位置上的值是1就可以,n-1会越界
           g[i] = g[i+1]*nums[i+1]; 
        
        //2.使用
        vector<int>ret(n);
        for(int i=0;i<n;i++)
         ret[i] = f[i]*g[i];
         
        return ret;   

    }
};

和为k的子数组

和为k的子数组

题目解析

  • 给你一个整数数组 nums 和一个整数 k ,请你统计并返回 _该数组中和为 k** **的子数组的个数 _。
  • 子数组是数组中元素的连续非空序列。
  • 数组中数字有正有负

算法原理

  1. 暴力解法:固定一个位置,开始枚举,一直加,直到和等于k,但这时不能停止,因为数字有正有负,后面可能抵消。再次统计,直到加到最后为止。时间复杂度O(n2)
  2. 前缀和+hash:这里我们引入一个以i位置为结尾的所有子数组,这样我们求和为k的子数组问题就转换为了在[0,i-1]区间内有多少个前缀和为sum[i]-k的子数组。但如果这样就开始遍历,那时间复杂度为O(n2)+O(n)甚至还不如暴力解法。我们要思考如何快速找到前缀和的数组有多少个等于他,借助数据结构哈希表,将前缀和塞进哈希表里,统计出现的次数,这样就不用遍历前缀和数组,只需要在哈希表中找到它出现的次数即可
  3. 细节问题:
    1. 前缀和加入hash表的时机
      1. 把前缀和全算出来,然后放入hash表中(不可以),我们是要找i位置之前的,如果全放进哈希表中,我们可能会统计i位置之后的值,他们的前缀和也刚好等于k,此时会重复计数。所以在计算i位置之前,hash表里只保存[0,i-1]位置的前缀和。
    2. 我们不用真的创建一个前缀和数组
      1. dp[i]=dp[i-1]+nums[i],计算dp[i]前缀和时,我们只需要直到dp[i-1]区间和就行,不需要记录dp[i-2],dp[i-3]的值,所以我们可以用一个sum来i位置之前的和,当每次计算完之后,sum更新dp[i]即可。
    3. 如果整个前缀和等于k呢?
      1. 如果有一种情况,是当枚举到i位置时发现整个数组的和等于k,那我们就需要去[0,-1]这段区间找和为0,但这个区间不存在呀,那我们就需要先放一个hash[0] = 1,即默认有一个前缀和等于0

算法——前缀和_第16张图片

注意:此题不能用滑动窗口的方法解决,因为当定义left和right进行移动时,由于数组中有正有负的情况,可能存在前面有段区间的负数和与后面区间正数和相抵消的情况,但以为此时left和right都向右移动,就会漏掉这种情况,不符合单调性这一性质。
算法——前缀和_第17张图片

代码实现

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 x : nums)  
        {
          sum += x; // 计算当前位置的前缀和
          if(hash.count(sum - k)) ret += hash[sum - k]; // 统计个数
          hash[sum]++;
        }
        return ret;
    }
};

和可被K整除的⼦数组

和可被K整除的⼦数组

题目解析

返回其中元素之和可被 k 整除的(连续、非空) 子数组 的数目。

算法原理

  1. 暴力枚举:枚举出所有子数组,求和判断是否能被k整除。

  2. **前缀和+hash:**找出两个前缀和一个以i为结尾的前缀和sum;另一个标记为x。根据题目要求,我们有(sum-x)%k=0,根据同余定理可得,sum%k等于x%k。所以此时问题转换为只需要在[0,i-1]区间找有多少个前缀和余数等于sum%k. 同时根据C++负数%正数,我们需要将其修正为(sum%k+k)%k。并且该题并不需要真的创建一个前缀和数组,因为我们只需要记录前缀和的余数即可。此时我们创建一个hash,第一个存前缀和余数,第二个存出现的个数。(这里需要注意的细节问题和上道题一样)

补充知识:

  1. **同余定理:**如果(a+b)➗p=k…0(即a+b能被p整除),则a%p 等于b%p
  2. **C++中【负数%正数】的结果以及修正:**负数%整数=负数,如果想将结果修正成正数,变成a%p+p,为了正负统一,则(a%p+p)%p算法——前缀和_第18张图片

:该题依然不能用滑动窗口思想来解题,因为有可能出现负数或者0的情况。

代码实现

class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) 
    {
        unordered_map<int, int> hash;
        hash[0 % k] = 1; // 0 这个数的余数
        int sum = 0, ret = 0;
        for(auto x : nums)
        {
            sum += x; // 算出当前位置的前缀和
            int r = (sum % k + k) % k; // 修正后的余数
            if(hash.count(r)) ret += hash[r]; // 统计结果
            hash[r]++;
        }
            return ret;
    }
};

连续数组

连续数组

题目解析

找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。

算法原理

如果我们直接去统计0和1出现的个数,这样难度会有点大,不妨我们转换一下思路,我们把0全部变成-1,那就转化为在数组中找出最长的子数组,找出和为0即可。我们之前做过一道和为k的子数组,这样就容易一点。

前缀和+hash:这里思路和和为k的子数组一样,这里主要考虑一些细节问题:

  1. **hash表里存什么:**因为题中要找出最长的子数组,所以hash,第一个存前缀和,第二个存下标,因为我们要统计长度
  2. **什么时候存入hash表:**当前位置的值,和当前位置所绑定的前缀和用完之后再存入
  3. **如果有重复的前缀和与下标,怎样存:**保留前面的这样能够保证子数组到i位置长度最长
  4. **默认前缀和为0:**当我们发现整个数组和为0的时候,我们需要在下标-1的位置(前几道题是置为0,因为要统计和或者乘积,这道题我们需要记录下标从而统计长度)所以hash[0]=-1
  5. **如何计算长度:**计算i到j的距离我们公式是i-j+1,在绿色标记的区间长度中实际上是不包含j这个点的,所以我们算多了一个,要减去1,即为i-j

算法——前缀和_第19张图片

代码实现

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

矩阵区域和

矩阵区域和

题目解析

answer矩阵中每一个位置返回的值是原矩阵中以该位置为中心,上下左右同时扩展k个格子,所组成的矩阵的和,填入answer,如果超出矩阵范围不计算,此时answer[0][0]位置为12
算法——前缀和_第20张图片算法——前缀和_第21张图片

算法原理

本质是快速求出矩阵某一范围的和,用二维前缀和。

  1. ret表示所求阴影面积,我们用总面积-(A+B)-(A+C)+A得出(这是动态规划算法里的状态转移方程)

算法——前缀和_第22张图片

  1. 接下来处理扩大k个格子坐标的越界问题算法——前缀和_第23张图片

  2. 处理下标映射关系问题:在我们的dp矩阵中,我们为了方便处理边界情况,我们是让下标从1开始,但是leetcode中的下标是从0开始。

注意: dp 表与原数组内的元素的映射关系:
i. 从 dp 表到原矩阵,横纵坐标减⼀;
ii. 从原矩阵到 dp 表,横纵坐标加⼀

算法——前缀和_第24张图片
在第一步求坐标的时候直接+1,然后直接拿值即可
算法——前缀和_第25张图片

代码实现

class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) 
    {
        int m = mat.size(), n = mat[0].size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        // 1. 预处理前缀和矩阵
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] +mat[i - 1][j - 1];
        // 2. 使⽤
        vector<vector<int>> ret(m, vector<int>(n));
        for(int i = 0; i < m; i++)
            for(int j = 0; j < n; j++)  
                {
                    int x1 = max(0, i - k) + 1, y1 = max(0, j - k) + 1;
                    int x2 = min(m - 1, i + k) + 1, y2 = min(n - 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;
    }
};

你可能感兴趣的:(算法,算法)