算法-前缀和

模板--一维前缀和

题目:前缀和

题目:前缀和

1、分析:这道题目题意不难理解,看实例1:数组有3个元素:1,2,4。有俩次查询,第一次查询连续区间[1,2]相当于1+2等于3;第二次查询连续区间[2,3]相当于2+4等于6。按照暴力算法:m次查询,每次查询都需要遍历区间[l , r]然后将元素累加并输出,如果每次查询都是[1,n],那时间复杂度就变成了O(m*n),又因为1≦n,m≦10的5次方,最坏情况就是10的10次方,这是会超时的。

2、算法原理:我们试着用前缀和的思想去解决:结合下图:前缀和数组dp每一个元素dp[i] 代表arr数组区间[1,i] 的元素总和。我们也可以得出规律:dp[i] =  arr[i] + dp[i-1]

题目需要求出区间[l,r]区间的元素总和,我们现在有了前缀数组dp,不需要遍历区间一个个累加,假设l = 4,r = 6 ,区间[4,6]的和是不是相当于dp[6] - dp[3],其实就是dp[r] - dp[l - 1]
可能有人疑惑为什么题目要求输入数组参与运算时下标从1开始而不是0,假设 l 为0,那此时就变成dp[r] - dp[-1],这会导致数组越界;从一开始,假设 l 为1,那此时就变成dp[r] - dp[0],此时把dp[0]和arr[0]都初始化为0即可

3、代码实现:

#include <iostream>
using namespace std;
#include <vector>
int main() {
    //输入处理
    int n,m;
    cin >> n >>m;
    vector<int> arr(n+1);
    for(int i = 1; i<=n; i++)   cin >> arr[i];
    //预先处理前缀和数组
    vector<long long> dp(n+1);//元素数值求和过程可能会溢出
    for(int i = 1; i<=n; i++)
    {
        dp[i] = arr[i] + dp[i-1];
    }
    //使用前缀和数组
    int l,r;
    for(int i = 1; i <= m;i++)
    {
        cin >> l>> r;
        cout << dp[r]-dp[l-1] << endl;
    }
    return 0;
}

模板二-二维前缀和

题目:二维前缀和

1、分析:这道题和上一道题的区别就在于这个输入是矩阵其实就是二维数组,要是用暴力算法,q次查找,每一次查找都是从输入数组arr[x1][y1]遍历到arr[x2][y2],时间复杂度为O(N*M*q),会超时。

2、算法原理:如果用二维前缀和数组dp,dp[i][j]就代表从arr[1][1]到arr[ i ][ j ]这段区间里所有元素的和,这里如果dp每一个元素都是将arr[1][1]到arr[ i ][ j ]遍历一次的话,那么时间复杂度也会很高,所以我们要找出规律,观察右图:

现在要求 dp[ i ][ j ] ,把数组arr分成四部分:A,B,C,D,dp[ i ][ j ] = A + B + C + D,A就是dp[i-1][j-1],D就是arr[ i ][ j ];C和B单看着不好求,那A+B是不是等于dp[i-1][j],A+C是不是等于dp[i][j-1],那么dp[ i ][ j ]就可以写成:(A + B) + (A + C) + D - A = dp[i-1][j] + dp[i][j-1] + arr[ i ][ j ] + dp[i-1][j-1]。知道dp[ i ][ j ]怎么求后,是不是得使用这个数组来求(x1,y1)到(x2,y2)这个子矩阵的和啊,这个有什么规律可以查询吗?

(x1,y1)到(x2,y2)这个子矩阵的和是不是可以等于dp[ i ][ j ] -  A - B - C,跟前面差不多,A是不是就是 dp[ x1-1][ y1-1],B和C直接求比较麻烦,但是A + C是不是就是dp[x1-1][y2],A+B是不是就是
dp[x2][y1-1],那么dp[ i ][ j ] -  A - B - C = dp[ i ][ j ] - (A + B)-(A + C)+ A = dp[ i ][ j ] - dp[x2][y1-1] - dp[x1-1][y2] + dp[ x1-1][ y1-1]。至于为什么数组从下标1开始而不是0,理由和上道题一样,就是会越界。

3、代码实现

#include <iostream>
using namespace std;
#include <vector>
int main() {
    int n, m, q;
    cin >> n >> m >> q;
    //处理输入
    vector<vector<int>> arr((n + 1) ,vector<int>(m + 1));
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> arr[i][j];
        }
    }
    //构建二维前缀和数组
    vector<vector<long long>> dp((n + 1) ,vector<long long>(m + 1));//long long防溢出
    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]-dp[i-1][j-1]+arr[i][j];
        }
    }
    //使用二维前缀和数组
    int x1,y1,x2,y2;
    for(int i = 1;i<=q;i++)
    {
        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")

例题三:寻找数组的中心下标

1、分析:根据题意和示例1,这道题目要求的是一个数组下标,它的前面区间和和后面区间和要相等,如果有多个则返回最左边的。如果数组最左端的右边区间的元素和为0,那么这个最左端的左侧数之和就视为0,它就是中心下标;最右端同理。用暴力算法,遍历每一个元素时,分别算出它左区间和右区间的元素和,判断是否相等,算法复杂度为O(N2),会超时。

2、算法原理:我们尝试设计这样的代码:下标i走到哪里,就可以直接判断 i 前面的元素(区间[0,i - 1])和后面的元素(区间[i + 1,n - 1])是否相等,而不是每一次都要遍历[0,i - 1]和[i + 1,n - 1]。那是不是可以设计两个数组:分别先求出,每一个下标 i 的前缀和([0,i - 1]和)和后缀和([i + 1,n - 1]),后面就直接使用这两个数组,直接判断下标 i 的前缀和和后缀和是否相等。

3、核心思路:这道题我们只要关心前缀和([0,i - 1]和)和后缀和([i + 1,n - 1]),不需要和前面的两道题一样关心nums[i]的值,所以前面的模板要理解不能死记硬背。将这个nums数组抽象出来:

前缀和数组元素求法:f [ i ]代表nums数组区间[0,i-1]的元素和,f [ i ] = f [i - 1] + nums[i - 1]
后缀和数组元素求法:g [ i ]代表nums数组区间[i+1,n-1]的元素和,g [ i ] = g [i + 1] + nums[i + 1].

不过这里要注意两个细节:当i == 0时,f[0] = f[-1] + nums[-1]会越界,所以f[0]要单独初始化为0,因为nums[0]左端没有元素,求解f [ i ]时,应该从下标1开始到n-1结束;当i == n-1时,g[n-1] = g[n] + nums[n]也会越界,所以g[n-1]要单独初始化为0,因为nums[n-1]是数组最后一个元素,后面没有元素,所以后缀和相当于0。---这里的时间复杂度是O(N+N)

两个数组使用方法:这个就很简单了,下标走到哪里就直接比较f [ i ]和g [ i ]的大小是否相等,因为如果有多个符合题意的中心下标就返回最左边的下标,那你从0开始遍历到n-1,是不是一旦判断相等就可以直接返回下标咯。----这里的时间复杂度是O(N),总的来说加上上面的最终是不是O(N).

class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n), g(n);
         //前缀和数组元素求解
        for(int i = 1;i<n;i++)
        {
            f[i]=f[i-1]+nums[i-1];
        }
         ////后缀和数组元素求解
        for(int j= n-2;j>=0;j--)
        {
            g[j]=g[j+1]+nums[j+1];
        }
        int i =0;
        //使用两个数组元素进行判断
        while(i<n)
        {
            if(f[i]==g[i])
               break;
            i++;
        }
        if(i==n)
            return -1;
        return i;
    }
};

例题四:除自身以外数组的乘积

1、题目分析:这道题要我们返回一个数组answer,answer[i] 代表的是 nums 数组中 nums[i] 之外从区间[ 0,i-1]的积再乘区间[i+1,n-1]的积,再看题目两个例子就可以彻底理解这道题目了。用暴力算法的话,每一个answer[i] 都需要遍历数组nums,时间复杂度为O(N2)。

2、算法原理:我们尝试能不能在求每一个answer[i] 时,直接得到 nums 数组中 nums[i] 之外从区间[ 0,i-1]的积和区间[i+1,n-1]的积,而不是遍历从头到尾乘一遍,看了上一道题目以及题目提醒,这里可以用前缀和思想。不过不一样的是,我们前面是对区间元素求和,这道题要求积。

还是用这个图:我们预先处理两个数组,前缀积数组 f 和后缀积数组 g,
f[ i ]代表nums数组[ 0,i-1]的积:f [ i ] = f [i - 1] * nums[i - 1]
g[ i ]代表区间[i+1,n-1]的积:g [ i ] = g [i + 1] * nums[i + 1]。
细节处理:这里的f[0] 和g[n-1] 可不是和上一道题初始化为0,如果初始化为0那么两个数组所有元素都是0哦,应该初始化为1。
使用这两个数组:从0到n-1遍历,
answer[ i ] = f[ i ] * g[ i ]

3、代码实现:

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n), g(n);
        f[0]=g[n-1]=1;
        //前缀积数组
        for(int i = 1;i<n;i++)
        {
            f[i]=f[i-1]*nums[i-1];
        }
        //后缀积数组
        for(int j= n-2;j>=0;j--)
        {
            g[j]=g[j+1]*nums[j+1];
        }
         vector<int> ret;
        int i =0;
        while(i<n)
        {
           ret.push_back(f[i]*g[i]);
            i++;
        }
        return ret;
    }
};

到这里我们其实可以看出前缀和后缀和数组其实就是先预处理一些数据来降低时间复杂度,用空间来换时间。

例题五:和为K的子数组

1、分析:这道题用暴力算法就是从0开始往后遍历到n-1,对路上的元素累加看是否为k,因为数组可能有0和负数,所以即使找到满足条件的子数组也要继续向后走,两层循环就是O(N2)了;用滑动窗口也不行,滑动窗口的使用是数组具有单调性,left和right指针可以一直往前走,但是这道题可能在区间[left,right]里面还有子数组满足k,那right又不回退,结果就会出错;如果直接使用前面的前缀和数组dp呢,dp[i]代表的是[0 , i]的元素和,肯定不能直接使用,直接使用反而比暴力算法还要麻烦。

2、算法原理:我们试着对nums数组从后向前看,要找符合k的子数组,可不可以转化为找符合前缀和为dp[i] - k的子数组,那我找的时候固定i,找dp[i] - k需要从0遍历到i,每一个前缀和 dp[j] 都拎出来和dp[i] - k相比较吗?这样时间复杂度还是O(N2),我们这里用哈希表,把每一个前缀和加入哈希表hash<int,int>,第二个int表示前缀和出现的次数。那么每一个 i 我都可以直接从哈希表直接找出[0,i - 1]符合条件hash[dp[i] - k]的前缀和的次数并加入ret。

处理细节:
(1)这里使用哈希表加入hash[dp[i] ]的时间节点是要在我预处理dp数组后使用之前,一次性把所有元素映射吗,肯定不行,有可能你在 i 位置时,满足条件dp[i] - k这个值的前缀和不止在区间[0,i-1]里面某一个子数组,后面的[i,n-1]区间的前缀和可能也满足,但是我们又不用在这一步加入ret。所以应该是在计算dp[i]前hsah表只保存[0,i - 1]这个区间的前缀和
(2)那我需不需要真的创建一个前缀和数组dp呢?其实也不用,我只要用一个变量sum来记录上一个位置的前缀和。
(3)如果dp[i]==k呢,我是不是需要在区间[-1,0]找前缀和为0的子数组,所以hash[0]是不是应该先初始化为1,防止这种情况出现。
3、代码实现

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        int n = nums.size()-1;
        unordered_map<int,int> hash;
        hash[0]=1;
        int sum=0,ret=0;
        for(auto e:nums)
        {
            sum += e;
            if(hash[sum-k]) ret+=hash[sum-k];
            hash[sum]++;
        }
        return ret;
    }
};

可能有人会疑惑:我可不可以在计算更新ret之前先执行hash[sum]++,好像看着没什么问题,但如果是这个例子就会报错:

例题六:和可被k整除的子数组

1、分析:这道题和例题五类似,就是多了被k整除这个条件,暴力枚举时间复杂度是O(N)。我们尝试用上一道题前缀和的思想:
上图中sum指[0,i]区间的前缀和,x指[0,i-1]区间某个下标前缀和,那么求被k整除的子数组,是不是就是(sum - x) % k =0,就相当于sum % k = x % k. 所以我们放入hash表的就不是前缀和了,应该是前缀和 % k。至于前缀和 % k什么时候放入hash表?如果sum % k ==0呢?用不用真的创建前缀和数组?这些细节和上一道题都是一样的。

2、代码实现:余数的求法sum % k 要注意结果为负数,我们需要手动改正为正的。

class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) {
        int sum=0;
        unordered_map <int,int> hash;
        hash[0%k]=1;//前缀和为0 ,去%k
        int ret=0;
        for(auto x:nums)
        {
            sum+=x;//当前位置前缀和
            int p = (sum%k+k)%k;//修正后的余数
            if(hash[p]) ret+=hash[p];
            hash[p]++;//更新hash表:加入当前位置前缀和%k的余数
        }
        return ret;
    }
};

例题七:连续数组

1、分析:这道题如果用暴力枚举时间复杂度是O(N),而且一直盯着0和1是很难想出能降低时间复杂度的办法的,单纯用前缀和也不行,因为不知道k到底是多少,我们可以把0看成-1,那是不是就变成要找含相同数量的-1和1的连续子数组了,不就是找和为0的连续子数组吗,k不就是0吗?不就变成了在[0,i-1]区间找前缀和为sum-k=sum的子数组吗?

2、但是如果照搬前面的代码是万万不行的,因为这道题要最大长度,不是连续子数组的个数,所以有挺多细节需要注意的。
(1)hash表还是存前缀和和出现次数吗?不是。要求长度那就要存下标
(2)什么时候存这个下标呢?是不是每一个符号前缀和为sum-k的下标都要存呢?
存下标这里和前面一样,都是使用了前缀和sum后,再考虑更不更新hash[sum],这里因为要求最大长度:看下面这个图,如果求到i这里更新sum后,发现前面 j 的前缀和也为j,已经有hash[sum]= j 了,我还要不要让hash[sum] = i呢?肯定不更新是吧,更新了只会让长度变小。
所以要注意hash只记录sum第一次的下标,后面有相同的sum值不用理。

(3)hash[0]的初始化是不是1呢?因为有一种情况就是sum-k=0,那我找子数组是不是在[-1,0]找,长度也应该是 i+1 对不对,而且这里hash[0]要下标,如果是0,那这种情况下 i-0 会把nums[0]少算了,长度不对,所以应该是-1。
(4)长度计算:可能有人疑惑,你上面那个 i-0 给它每次求长度加1不就好了吗,跟之前滑动窗口right-left+1不一样,这里长度计算是找到了前缀和满足sum-k的下标后,这个下标本身不在后面的k数组中的,长度计算变成了 i - hash[sum-k]才对。
3、代码实现:

class Solution {
public:
    int findMaxLength(vector<int>& nums) {
       
        unordered_map<int,int> hash;
        hash[0] = -1;//有可能sum==k,要在[-1,0]找,
                       //只能返回-1,让i-(-1)才对;i-0的话会把下标0这个位置长度忽略
        int sum = 0;
        int len = 0;
       for(int i =0;i<nums.size();i++)
       {
            if(nums[i] == 0) sum+=-1;//把0当做-1,找满足前缀和为sum-k(0)的子数组长度
            else sum+=nums[i];
            if(hash.count(sum))//只记录前缀和sum第一次出现的下标
            {
                len = max(len,i-hash[sum]);//因为下标越小长度越大
            }
            else hash[sum]=i;//说明hash没有记录过前缀和为sum的,这里就是第一次记录
       }
       return len;
    }
};

例题八:矩阵区域和

1、分析:这道题单看题目是比较难以理解的,通过例子和下面的图比较容易理解:

相当于在mat[ i ][ j ]处往上下扩展k个单位,往左右扩展k 个单位,然后answer[ i ][ j ]就是mat数组从(i-k,j-k)这个点到(i+k,j+k)这个点围成的区域和。那这个题目是不是和例题二就很像了,用二维前缀和来求解

2、核心思路:
(1)预处理dp数组:例题二的输入数组下标从1开始,而mat数组是从0开始,那我们用这个dp[ i ][ j ]= mat[ i ][ j ]+dp[ i-1 ][ j ]+dp[ i ][ j-1 ]- dp[ i-1 ][ j-1 ] 公式是不是还要注意边界情况,这里如果单独去处理边界情况很麻烦,我们依然可以让二维前缀和数组 dp 从1开始,只不过要注意:例如dp[1][1]对应的是mat[0][0],所以式子应该改为dp[ i ][ j ]= mat[ i-1 ][ j-1 ]+dp[ i-1 ][ j ]+dp[ i ][ j-1 ]- dp[ i-1 ][ j-1 ]
(2)修正(i-k,j-k)和(i+k,j+k),这里 i-k 和 j-k 都有可能小于0,这个时候我们要手动修正成0;i+k可能大于m-1,要改成m-1,j+k可能大于n-1,要改成n-1。
(3)使用dp数组:mat数组从(i-k,j-k)这个点到(i+k,j+k)这个点围成的区域和解法和例题二一样,但是式子不需要死记硬背,画个图就可以得出:

这里我用(r1,c1)和(r2,c2)代替,answer[ i ][ j ] = dp[r2][c2] - dp[r1-1][c2] - dp[r2][c1-1] + dp[r1-1][c1-1]。因为前面我们dp是从下标1开始,但是题目要求的返回数组要和mat数组一样从下标0开始,所以使用dp数组要把横纵坐标都要加1,那就在求r1,r2,c1,c2后都再加1咯。

3、代码实现:给出两份:第一个是让dp数组从下标1开始,第二份是dp数组从下标0开始,但是要处理边界情况的

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));
        for(int i = 1;i<=m;i++)
        {
            for(int j= 1;j<=n;j++)
            {
               dp[i][j]= mat[i-1][j-1]+dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1];
            }
        }
        vector<vector<int>> answer(m, vector<int>(n));
        for(int i = 0;i<m;i++)
        {
            for(int j= 0;j<n;j++)
            {
                int r1=max(i-k,0)+1, 
                r2=min(m-1,i+k)+1,
                c1=max(j-k,0)+1,
                c2=min(n-1,j+k)+1;
                answer[i][j] = dp[r2][c2]-dp[r1-1][c2]-dp[r2][c1-1]+dp[r1-1][c1-1];
            }
        }
        return answer;

    }
};
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, vector<int>(n));
        for(int i = 0;i<m;i++)
        {
            for(int j= 0;j<n;j++)
            {
                if(i==0 && j==0) dp[i][j]= mat[i][j];
                else if(i==0) dp[i][j]= mat[i][j]+dp[i][j-1];
                else if(j==0) dp[i][j]= mat[i][j]+dp[i-1][j];
                else dp[i][j]= mat[i][j]+dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1];
            }
        }
        vector<vector<int>> answer(m, vector<int>(n));
        for(int i = 0;i<m;i++)
        {
            for(int j= 0;j<n;j++)
            {
                int r1=(i-k < 0)? 0: (i-k), 
                r2=(i+k) > (m-1) ? m-1:i+k,
                c1=(j-k < 0)? 0: (j-k),
                c2=(j+k) > (n-1) ? n-1:j+k;
                if(r1==0 && c1==0)
                {
                    answer[i][j] = dp[r2][c2];
                }
                else if(r1==0 )
                {
                    answer[i][j] = dp[r2][c2]-dp[r2][c1-1];
                }
                else if(c1==0 )
                {
                    answer[i][j] = dp[r2][c2]-dp[r1-1][c2];
                }
                else 
                {
                    answer[i][j] = dp[r2][c2]-dp[r1-1][c2]-dp[r2][c1-1]+dp[r1-1][c1-1];
                }
            }
        }
        return answer;

    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值