001前缀和与差分——算法备赛

前缀和的一个应用是差分,差分是前缀和的逆运算。

定义一个数组D[] D[i]=a[i]-a[i-1] a是原始数组 可以表示前缀和

一般定义D的大小为 size+1(size为a数组的大小),初始全为0

区间修改

//[L,R]中每个元素加d
D[L]+=d;
D[R+1]-=d;

利用查分数组,把O(n)区间修改变为O(1)的端点修改

k值区间

问题描述

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

原题链接

思路分析

用哈希表mp[i]记录前缀和为i的个数,mp[0]初始为1,每个区间[l,r]的元素和为s=sum[r]-sum[l-1],每次遍历得到一个sum[r],要知道

s=k的右边界为r的区间的个数,只需知道前面共有多少个前缀和sum[l-1]等于sum[r]-k即可

代码

int subarraySum(vector<int>& nums, int k) {
         unordered_map<int, int> mp;
        mp[0] = 1;
        int count = 0, pre = 0;
        for (auto& x:nums) {
            pre += x;
            if (mp.count(pre - k)) {
                count += mp[pre - k];
            }
            mp[pre]++;
        }
        return count;
    }

k倍区间

问题描述

给定长度为n的数列a1,a2,a3…,其中一段[i,j] (i<=j)区间和是k的倍数,我们称[i,j]为k倍区间。

求给定数列的k倍区间数量。

原题链接

思路

定义前缀和sum[i]为区间 [1,i]的元素和 (sum[0]=0)。任意区间和 a[i,j]=sum[j]-sum[i-1]

若遍历左右区间边界i,j求解时间复杂度为O(n^2).会超时。

可利用一个数学技巧降低时间复杂度。

两个数除k的余数相等时,他们相减的差一定是k的倍数

例 (a=7,b=4,k=3)

我们可以枚举一个右边界 因为 任意区间和 a[i,j]=sum[j]-sum[i-1]sum[j]是固定的 (余k得数固定,设为a),我们只需求出[1,j]中有多少个 子前缀和的余k得数为a 便能求得以j为右边界有多少区间为k倍区间

代码

#include<iostream>
#include<cstdio>
#include<vector>
#define ll long long
using namespace std;
int main(){
	int n,k;
	cin>>n>>k;
	vector<ll>sum(n+1);
    vector<int>cnt(k);
    cnt[0]=1;  //初始cnt[0]=1,免去特殊情况的处理。
    ll tar=0;
	for(int i=1;i<=n;i++){
		cin>>sum[i];
		sum[i]+=sum[i-1];
        tar+=cnt[sum[i]%k];
        cnt[sum[i]%k]++;  //余数为sum[i]%k的前缀和数加1.
	} 
	cout<<tar;
	return 0;
} 

长度k倍区间

给你一个整数数组 nums 和一个整数 k

返回 nums 中一个 非空子数组最大 和,要求该子数组的长度可以 k 整除

原题链接

思路分析

与上一题不同,这题求的是子数组长度为k的倍数的最大子数组和。

相似地,区间和a[i,j]=sum[j]-sum[i-1],要使a[i,j]最大,sum[i-1]越小越好,同时要满足长度是k的倍数。

可以定义一个数组minn,minn[i]记录了当前遍历到的所有下标余k等于i的元素中的最小值

若区间和a[i,j]的长度(j-i+1)为k的倍数,其满足ji-1余k的结果相同(等于j%k),而minn[j%k]中存储了满足条件的最小sum[i-1],此时sum[j]-minn[j%k]即是以j为右边界长度为k的倍数的最大子数组和。依次遍历右边界,更新历史最大值ans,最后即可求得答案。

具体实现时,minn[0]~minn[k-2]初始值为对应的nums值,minn[k-1]0ans初始值为LONG_MIN

代码

long long maxSubarraySum(vector<int>& nums, int k) {
         int n=nums.size();
         vector<long long>minn(k);
        long long ans=LONG_MIN,sum=0;
        for(int i=0;i<k-1;i++){
            sum+=nums[i];
            minn[i]=sum;
        }
        minn[k-1]=0;
        for(int i=k-1;i<n;i++){
            sum+=nums[i];
            ans=max(sum-minn[i%k],ans);
            minn[i%k]=min(minn[i%k],sum);
        }
        return ans;
    }

零数组变换|

问题描述

给定一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] = [li, ri]

对于每个查询 queries[i]

  • nums 的下标范围 [li, ri] 内选择一个下标子集。
  • 将选中的每个下标对应的元素值减 1。

零数组 是指所有元素都等于 0 的数组。

如果在按顺序处理所有查询后,可以将 nums 转换为 零数组 ,则返回 true,否则返回 false

数组的 子集 是对数组元素的选择(可能为空)。

原题链接

思路分析

定义一个数组sumsum[i]存储的是对nums[i]的最大减量。

遍历queries数组,对于每个区间[l,r],对sum中该区间的所有元素都加一,这可以用差分在O(1)时间复杂度内实现。

定义一个差分数组d,d[i]=sum[i]-sum[i-1]

最后遍历sum数组,如果sum[i]<nums[i],说明无法将该元素减为0,也就返回false;遍历到最后,说明所有元素都可减为0,返回true

时间复杂度O(n+m)

代码

 bool isZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
        int n=nums.size(),m=queries.size();
        vector<int>d(n+1);  //差分数组
        for(int i=0;i<m;i++){
            d[queries[i][0]]++; d[queries[i][1]+1]--;
        }
        int sum=0;
        for(int i=0;i<n;i++){
            sum+=d[i];  //sum为对当前元素nums[i]最大的减量
            if(sum<nums[i]) return false;
        }
        return true;
    }

零数组变换||

问题描述

给你一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] = [li, ri, vali]

每个 queries[i] 表示在 nums 上执行以下操作:

  • nums[li, ri] 范围内的每个下标对应元素的值 最多 减少 vali
  • 每个下标的减少的数值可以独立选择。

零数组 是指所有元素都等于 0 的数组。

返回 k 可以取到的 最小非负值,使得在 顺序 处理前 k 个查询后,nums 变成 零数组。如果不存在这样的 k,则返回 -1。

原题链接

思路分析

与**零数组变换|**类似,定义虚拟减量数组sum的差分数组d,d[i]=sum[i]-sum[i-1]

遍历nums数组,如果当前nums[i]对应的最大减量sum[i]<nums[i],则说明当前选择的区间不足以使nums数组变为零数组,需要内层向右遍历queries继续选择区间,直到sum>=nums[i]或ans>=m(ans为当前选择区间数)时结束。

如果内层遍历完,sum还是小于nums[i]说明所有区间选择完,该元素还是不能置0,直接返回-1即可

最后外层遍历结束,返回ans

时间复杂度O(n+m)

代码

int minZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
        int n=nums.size(),m=queries.size();
        vector<int>d(n+1);
        int ans=0,sum=0;
        for(int i=0;i<n;i++){
            sum+=d[i];
            while(sum<nums[i]&&ans<m){
                int x=queries[ans][0],y=queries[ans][1],z=queries[ans][2];
                d[x]+=z;
                d[y+1]-=z;
                if(i>=x&&i<=y) sum+=z;  //最大减量加z
                ans++;  //选择的区间数=1
            }
            if(sum<nums[i]) return -1;
        }
        return ans;
    }

零数组变换|||

问题描述

给你一个长度为 n 的整数数组 nums 和一个二维数组 queries ,其中 queries[i] = [li, ri]

每一个 queries[i] 表示对于 nums 的以下操作:

  • nums 中下标在范围 [li, ri] 之间的每一个元素 最多 减少 1 。
  • 坐标范围内每一个元素减少的值相互 独立

零数组 指的是一个数组里所有元素都等于 0 。

请你返回 最多 可以从 queries 中删除多少个元素,使得 queries 中剩下的元素仍然能将 nums 变为一个 零数组 。如果无法将 nums 变为一个 零数组 ,返回 -1 。

原题链接

思路分析

贪心+大顶堆+差分

题目问最多可以删除多少个元素,也就是说最少可保留多少个元素

从左往右遍历nums数组,对于nums[i]=k,我们需要在queries中选择k个左端点小于等于i的区间,因为所选区间要最少,所以所选区间的右边界越大越好

这启发我们用大顶堆维护左端点 ≤i 的未选区间的右端点。

代码实现时,还需要把 queries 按照左端点排序,这样我们便于遍历 左端点 ≤i 的区间。

代码

int maxRemoval(vector<int>& nums, vector<vector<int>>& queries) {
        ranges::sort(queries, {}, [](auto& a) { return a[0]; });  //按左端点升序排序
        int n = nums.size(), j = 0, sum_d = 0;
        vector<int> diff(n + 1, 0);  //差分数组
        priority_queue<int> pq;  //存的是能删除的区间右边界
        for (int i = 0; i < n; i++) {
            sum_d += diff[i];  //当前元素的最大减量
            while (j < queries.size() && queries[j][0] <= i) {
                pq.push(queries[j][1]);  //先将左边界小于等于i的区间的右边界值全部入队
                j++;
            }
            while (sum_d < nums[i] && !pq.empty() && pq.top() >= i) {  //队列中的区间左边界都是小于等于i的
                diff[pq.top() + 1]--;
                pq.pop();  //出队表示区间保留
                sum_d++;  //sum_d直接加1
            }
            if (sum_d < nums[i]) {  //还是不能满足要求
                return -1;
            }
        }
        return pq.size();
    }
  • 时间复杂度:O(n+qlogq),其中 nnums 的长度,qqueries 的长度。

疑问1:为什么在区间保留时不用执行 左区间边界对应的差分加一的操作?

答:在遍历到nums的下标i时,队列中的区间的左边界都大于等于i,执行了diff[left]++(假设left为对应的左边界),是为了让sum_d++,而这可以直接执行sum_d++,在遍历后面的下标i时,不会再用到diff[left],所以也就可以免去执行diff[left]++的操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值