目录
一.子数组一连问题I
对于OJ链接:
题目描述:
解题思路:
首先了我们非常能够想到的是暴力枚举每一个子数组把他们的累加和算出来然后再一一比较获取最大值。
对应代码:
#include<iostream> #include<vector> using namespace std; int main() { int n ,k; cin>>n>>k; vector<int>arr(n); for(int i=0;i<n;i++) { cin>>arr[i]; } int N=arr.size(); int maxLen=0; for(int left=0;left<N;left++)//枚举每个位置开头 { for(int right=left;right<N;right++)//每个位置结尾 { int sum=0; for(int i=left;i<=right;i++)//求[left,right]这一段区间的累加和 { sum+=arr[i]; } if(sum==k) { maxLen=max(maxLen,right-left+1);//长度为right-left+1; } } } cout<<maxLen<<endl; return 0; }
但是非常遗憾超时。如果我们提取生成累加和数组不去枚举求累加和,将时间复杂度降到O(N^2)是否能够通过了?
对应代码:
#include<iostream> #include<vector> using namespace std; int main() { int n ,k; cin>>n>>k; vector<int>arr(n); for(int i=0;i<n;i++) { cin>>arr[i]; } int N=arr.size(); vector<int>Sum(N+1);//生成累加和多开一个方便后序计算累加和 for(int i=1;i<=N;i++) { Sum[i]=Sum[i-1]+arr[i-1]; } int maxLen=0; for(int left=0;left<N;left++)//枚举每个位置开头 { for(int right=left;right<N;right++)//每个位置结尾 { int sum=Sum[right+1]-Sum[left];//直接获取累加和 if(sum==k) { maxLen=max(maxLen,right-left+1);//长度为right-left+1; } } } cout<<maxLen<<endl; return 0; }
提交一下:
很不幸还是超时
这时我们注意到数组的每一个元素都是大于0的。如果我们一直累加那么累加和一定是递增的,一般遇到有单调性的问题我们可以往窗口这个方向想。首先我们可以定义一个窗口[left,right]这个窗口不会退,这样我们就没有了枚举行为时间复杂度为O(N)的这样应该能过了。那么我们怎么想了,首先我们使用一个变量sum记录累加和:对应三种情况
1.如果此时窗口的内的累加和大于k那么我们让一个数出窗口
2.如果此时窗口里面的累加和等于k,此时我们可以选择让一个数进窗口也可以选择让一个数出窗口。
3.如果窗口里面的累加和小于k此时直接让下一个进窗口,窗口往右括。下面以[1,2,1,1]为例,累加和要为3:其中left为窗口左边界,right为窗口右边界。
此时sum=1比我们要的累加和3要小right往右扩:
此时窗口累加和3刚好是我们要的累加和。此时我们更新答案,并且让我们的right向右扩:
此时累加和已经大于4此时我们让1这个数出窗口.
此时我们继续更新答案,right继续向右移动。此时right已经越界了答案更新结束。注意如果数组中有0当相等时需要让right往右扩让长度变得更长。
对应代码:
#include<iostream> #include<vector> using namespace std; int main() { int n ,k; cin>>n>>k; vector<int>arr(n); for(int i=0;i<n;i++) { cin>>arr[i]; } int left=0; int right=0; //left和right表示窗口[left,right] int Maxlen=0; int sum=arr[0]; while(right<arr.size()) { if(sum==k) { Maxlen=max(Maxlen,right-left+1); sum+=arr[++right]; } else if(sum>k)//大了一个数从窗口滑出 { sum-=arr[left++]; } else { ++right;//窗口向右动 if(right==arr.size())//如果以及越界了跳出循环 { break; } sum+=arr[right];//将当前数加上 } } cout<<Maxlen<<endl; return 0; }
二.子数组三连问题II
对应OJ链接:
题目描述:
解题思路:
本题和上题不同的是本题可能出现负数那么累加和就不是递增的了。此时我们就不能够使用窗口进行处理了,最简单的方法就是前面的方法,枚举出所有的子数组然后在一个一个判断,但是这样的时间复杂度是O(N^3)如果我们使用前缀和进行优化时间复杂度依然是O(N^2)依然会超时。但本题的思想依然是利用前缀和的思想:
我们只需要定义了一个Hash表记录数组从0位置的累加和,其思想和两数之和道题一模一样,将数组中的累加和依次加入道哈希表中,每次去哈希表里面去查有没有sum-k这这累加和如果有更新答案。当时在这里我们需要注意的是我们需要提前加入一条记录map[0]=-1即0这个累加和最早出现在-1位置,如果不加我们将错过以0开头的答案(注意我们定义的这个哈希表的含义是key代表的是累加和,val代表的是最早出现的位置)。下面以[1,1,1],k=3为例:
此时我们已经找到了累加和为3的子数组我们去哈希表里面去查发现竟然没有0这个累加和,此时我们将错过这个答案,所以我们需要将0这个累加和提取加入道哈希表里面。
对应代码:
#include<iostream> #include<unordered_map> #include<vector> using namespace std; int main() { int n,k; cin>>n>>k; vector<int>arr(n); for(int i=0;i<n;i++) { cin>>arr[i]; } unordered_map<int,int>Hash;//记录累加和 int sum=0; int maxLen=0; Hash[0]=-1;//注意一定要加上这一条记录否则会错过以0开头的答案 for(int i=0;i<n;i++) { sum+=arr[i]; if(Hash.count(sum-k)) { maxLen=max(maxLen,i-Hash[sum-k]);//更新答案 } if(!Hash.count(sum))//如果当前的累加和在哈希表里不存在直接加进去 { Hash[sum]=i; } } cout<<maxLen<<endl; return 0; }
扩展1:
对应OJ链接:
题目描述:
解题思路:
本题其实是上题的一个变种,上题求的是累加和为k的最长子数组的长度。本题求的是正数和负数个数相等的子数组的最长长度,如果我们将正数设置为1,负数设置为-1,这不就等价于上题的求累加和为0的最长子数组的长度了吗。
对应代码:
#include<iostream> #include<unordered_map> #include<vector> using namespace std; int main() { int n; cin>>n; vector<int>arr(n); for(int i=0;i<n;i++) { cin>>arr[i]; } for(int i=0;i<n;i++) { if(arr[i]<0) { arr[i]=-1; } else if(arr[i]>0) { arr[i]=1; } } //定义一个哈希表 int maxlen=0; unordered_map<int,int>Hash; Hash[0]=-1; int sum=0; for(int i=0;i<n;i++) { sum+=arr[i]; if(Hash.count(sum)) { maxlen=max(maxlen,i-Hash[sum]); } if(!Hash.count(sum)) { Hash[sum]=i; } } cout<<maxlen; }
扩展 II:
题目描述:
解题思路:
本题和上题基本一模一样,同样的是对上题的改写我们只需要将0改为-1.就又变成上一题了。
对应代码:
#include<iostream> #include<unordered_map> #include<vector> using namespace std; int main() { int n; cin>>n; vector<int>arr(n); for(int i=0;i<n;i++) { cin>>arr[i]; } for(int i=0;i<n;i++) { if(arr[i]==0) { arr[i]=-1; } } //定义一个哈希表 int maxlen=0; unordered_map<int,int>Hash; Hash[0]=-1; int sum=0; for(int i=0;i<n;i++) { sum+=arr[i]; if(Hash.count(sum)) { maxlen=max(maxlen,i-Hash[sum]); } if(!Hash.count(sum)) { Hash[sum]=i; } } cout<<maxlen; }
扩展iii:
题目描述:
解题思路:
本题和原型基本不变只不过我们需要补的初始值为Hash[0]=1;key值为累加和出现的次数。具体请看代码
对应代码:
class Solution { public: int subarraySum(vector<int>& nums, int k) { unordered_map<int,int>Hash; int sum=0; int cnt=0; Hash[0]=1;//注意一定要加上这个 for(int i=0;i<nums.size();i++) { sum+=nums[i]; cnt+=Hash[sum-k]; Hash[sum]++; } return cnt; } };
三.子数组三连问题III
题目描述:
解题思路:
本题解题思路真的非常的难想,首先需要定义一个minSum数组,minSum[i]的含义是指以i开头所取得的最小累加和。minEnd数组是指取得最小累加和时结尾的下标。大题流程如下:定义一个sum变量记录累加和,end变量,记录迟迟扩不进来的开始位置。每次开始让sum往右扩,尝试能不能将右边的区域扩进来,如果扩不进来,窗口出去一个数在尝试能不能扩进来,直到窗口扩不进来为止。如果窗口此时已经没有数了从下一个位置开始扩看能不能把迟迟扩不进来的区域扩进来,具体请看代码。
#include<iostream> #include<vector> using namespace std; int main(){ int n,k; cin>>n>>k; vector<int>arr(n); for(int i=0;i<n;i++){ cin>>arr[i]; } vector<int>minSum(n);//以某一个位置开头往右能取到的最小累加和 vector<int>minEnd(n);//这个累加和是以谁结尾 minSum[n-1]=arr[n-1];//以最后一个元素开头的最小累加和就是自己 minEnd[n-1]=n-1;//结尾位置也是自己 for(int i=n-2;i>=0;i--){ if(minSum[i+1]<0){//有利可图 minSum[i]=arr[i]+minSum[i+1]; minEnd[i]=minEnd[i+1]; } else{ minSum[i]=arr[i]; minEnd[i]=i; } } int end=0;//i是窗口的最右块的最后一个位置,在下一位置,即下一块开始的位置 int sum=0; int ans=0; for(int i=0;i<n;i++){ while(end<n&&sum+minSum[end]<=k){//如果加上后面那一块还没有超过k sum+=minSum[end];//将另外一部分吸收进来 end=minEnd[end]+1;//end始终是迟迟扩不进来的那一块的开头位置 } //不能在扩了 ans=max(ans,end-i);//更新答案 if(end>i){ sum-=arr[i];//左测一个数出窗口 } else{//说明一上来就扩失败了,此时i即将++所以end跟着i一起走 end=i+1; } } cout<<ans<<endl; }
扩展:给定数组arr,给定一个值为v求子数组的平均值小于等于v的最长子数组的长度。解题思路:将数组的每个元素减个v就等价于求子数组累加和小于等于0的最长子数组的长度就转化为上面这个题