【算法深练】分组循环:“分”出条理,化繁为简
目录
引言
分组循环
2760. 最长奇偶子数组
1446. 连续字符
1869. 哪种连续子字符串更长
2414. 最长的字母序连续子字符串的长度
3456. 找出长度为 K 的特殊子字符串
1957. 删除字符使字符串变好
674. 最长连续递增序列
978. 最长湍流子数组
2110. 股票平滑下跌阶段的数目
228. 汇总区间
1887. 使数组元素相等的减少操作次数
845. 数组中的最长山脉
2038. 如果相邻两个颜色均相同则删除当前颜色
2900. 最长相邻不相等子序列 I
3011. 判断一个数组是否可以变为有序
1578. 使绳子变成彩色的最短时间
1759. 统计同质子字符串的数目
1839. 所有元音按顺序排布的最长子字符串
2765. 最长交替子数组
3255. 长度为 K 的子数组的能量值 II
3350. 检测相邻递增子数组 II
3105. 最长的严格递增或递减子数组
拓展 (加餐)
838. 推多米诺
467. 环绕字符串中唯一的子字符串
3499. 操作后最大活跃区段数 I
2593. 标记所有元素后数组的分数
2948. 交换得到字典序最小的数组
总结
引言
分组循环可以归为同向双指针这一类题目,不同的是将双指针优化为单指针并记录每组的起始位置。
适用场景:按照题目要求,数组会被分割成若干组,每一组的判断/处理逻辑是相同的。
核心思想:
- 外层循环负责遍历组之前的准备工作(记录开始位置),和遍历组之后的统计工作(更新答案最大值)。
- 内层循环负责遍历组,找出这一组最远在哪结束。
PS:本篇博客中的所有题目均来自于灵茶山艾府 - 力扣(LeetCode)分享的题单。
分组循环
2760. 最长奇偶子数组
题解:此题不再是只有一个起点,而是多起点;找出满足题意的最长子数组;外层循环即可起始位置,内层循环找该子数组满足题意的最长位置。
class Solution {
public:int longestAlternatingSubarray(vector<int>& nums, int threshold) {//找到最长数组,数组满足两个条件:1)奇偶交替;2)子数组最大值不超过threholdint i=0,n=nums.size();int ret=0;while(i<n){while(i<n&&(nums[i]%2!=0||nums[i]>threshold)) i++; //先找到满足题意的第一个位置int start=i; //记录起始位置while((i<n&&nums[i]<=threshold)&&(i==start||nums[i]%2!=nums[i-1]%2)) i++; //找出当前子数组的最长长度ret=max(ret,i-start); //更新答案}return ret;}
};
1446. 连续字符
题解:经典的分组循环题目,从多个字符起点开始,找出最长的相同子字符串的长度。
class Solution {
public:int maxPower(string s) {int n=s.size();int i=0,ret=0;while(i<n){int start=i; //记录当前位置while(i<n&&s[i]==s[start]) i++; //判断该子字符串有多少个相同的字符ret=max(ret,i-start); //更新答案}return ret;}
};
1869. 哪种连续子字符串更长
题解:与上一题一样,但是此题有两种字符需要进行记录;因此可以直接使用两个整形对两种字符的最长长度进行记录即可。
class Solution {
public:bool checkZeroOnes(string s) {//分别记录1和0的最长字符串的长度int n=s.size();int i=0;int one=0,zero=0;while(i<n){int start=i; //记录起始位置while(i<n&&s[i]==s[start]) i++;if(s[start]=='0') zero=max(zero,i-start); //判断对0还是1进行更新else one=max(one ,i-start);}return one>zero;}
};
2414. 最长的字母序连续子字符串的长度
题解:分组循环,只是子数组判断条件不同,要求字母序连续。
class Solution {
public:int longestContinuousSubstring(string s) {//字母序连续int n=s.size();int i=0,ret=0;while(i<n){int start=i; //记录起始位置while(i==start||s[i]-s[i-1]==1) i++; //判断是不是连续字母序ret=max(ret,i-start); //更新答案}return ret;}
};
3456. 找出长度为 K 的特殊子字符串
题解:不需要控制长为K区间来判断时候满足条件,可以直接找出每一段字符串中相同字符的个数,判断该长度是否是K即可。
class Solution {
public:bool hasSpecialSubstring(string s, int k) {//找长度为K的相同字符串的长度int n=s.size();int i=0;while(i<n){int start=i; //记录起始位置while(i<n&&s[i]==s[start]) i++; //找出该字符相同子字符串的长度if(i-start==k) return true; //判断相同字符的长度时候为K}return false;}
};
1957. 删除字符使字符串变好
题解:连续字符的个数不超过3,可以在内循环中判断以下该重复字符是第几个,能否进行插入。
class Solution {
public:string makeFancyString(string s) {int n=s.size();int i=0;string ret;while(i<n){int start=i; //记录起始位置while(i<n&&s[i]==s[start]) {if(i-start<2) ret+=s[i]; //连续字符个数小于3就可以插入结果i++;}}return ret;}
};
674. 最长连续递增序列
题解:找出最长的子数组,遍历数组;通过外层循环记录起始位置,内层循环找出子数组结束的位置。
class Solution {
public:int findLengthOfLCIS(vector<int>& nums) {//找递增的子数组int n=nums.size();int i=0,ret=0;while(i<n){int start=i; //记录起始位置while(i<n&&(i==start||nums[i]>nums[i-1])) i++; //判断是否是递增子数组ret=max(ret,i-start); //更新答案}return ret;}
};
978. 最长湍流子数组
题解:湍流数组,递增和递减是交替进行的。外层循环进行记录起始位置和过滤相同位置,内层循环找出子数组的最长长度。细节:用flag记录i位置前面的增长趋势,使用flag*(nums[i+1]-nums[i])<0时注意乘法可能会导致越界。
class Solution {
public:int maxTurbulenceSize(vector<int>& arr) {//数组一增一减的趋势int n=arr.size();if(n==1) return 1;int i=0,ret=1;while(i<n-1){if(arr[i]==arr[i+1]) //相等的数据直接过滤掉{i++;continue;}int start=i++; //记录起始位置int flag=arr[i]-arr[start]>0?1:-1; //记录上一个位置是递增还是递减while(i<n-1&&(flag*(arr[i+1]-arr[i])<0)) {flag=arr[i+1]-arr[i]>0?1:-1; //更新flag记录的位置i++;}ret=max(ret,i-start+1); //更新答案}return ret;}
};
2110. 股票平滑下跌阶段的数目
题解:外层循环记录起始位置,内层循环找从该位置开始最长的平缓数组长度K,根据子数组的长度再求出其子数组的个数:K(K+1)/2;
class Solution {
public:long long getDescentPeriods(vector<int>& prices) {//一段平滑下跌子数组的长度为k//其满足条件的子数组个数为:k+k-1+k-2+k-3....+1为(k+1)*k/2int n=prices.size();long long i=0,ret=0;while(i<n){int start=i; //记录数组起始位置while(i<n-1&&prices[i]-prices[i+1]==1) i++; //找出最长平缓数组长度long long k=i-start+1; //数组长度ret+=(k+1)*k/2; //子数组个数i++;}return ret;}
};
228. 汇总区间
题解:与上一题类似,找连续递增的子数组,并且每次递增1。
class Solution {
public:vector<string> summaryRanges(vector<int>& nums) {//进行分组循环int n=nums.size();int i=0;vector<string> ret;while(i<n){int start=i; //记录起始位置while(i<n-1&&(long long)nums[i+1]-nums[i]==1) i++; //找有多少连续的数if(i==start) //判断是区间还是一个数ret.push_back(to_string(nums[start]));else {string tmp=to_string(nums[start]);tmp+="->";tmp+=to_string(nums[i]);ret.push_back(tmp);}i++;}return ret;}
};
1887. 使数组元素相等的减少操作次数
题解:进行模拟;先对数组进行排序,从后往前遍历,当前位置与前一个位置不同时要对[i,n-1]进行改变;向前遍历直到遍历到0停止。
class Solution {
public:int reductionOperations(vector<int>& nums) {//先对数组进行排序int n=nums.size();sort(nums.begin(),nums.end());//从n-1开始往前找int i=n-1,ret=0;while(i>0){if(nums[i-1]!=nums[i]) ret+=n-i;i--;}return ret;}
};
845. 数组中的最长山脉
题解:使用分组循环,外循环记录起始位置,内循环用两个来分别记录左侧山脉长度和右侧山脉长度。
class Solution {
public:int longestMountain(vector<int>& arr) {//分别求单调递增和递减区间int ret=0,n=arr.size();int i=0;while(i<n-1){int start=i,flag1=0,flag2=0; //用flag来记录是否存在左右山脉while(i<n-1&&arr[i]<arr[i+1]) //记录山脉左侧{flag1=1;i++;}while(i<n-1&&arr[i]>arr[i+1]) //记录山脉右侧{flag2=1;i++;}if(flag1&&flag2)ret=max(ret,i-start+1);if(i<n-1&&arr[i]==arr[i+1]) i++; //对于后面数据相等时,向后移动防止死循环}return ret;}
};
2038. 如果相邻两个颜色均相同则删除当前颜色
题解:通过相同子字符串的长度就可以判断出一个玩家对于该子字符串可以进行多少次删除操作,比如AAAAA,Alice可以进行3次删除操作;因此通过分组循环分别记录两个玩家可以进行删除操作的次数就可以确定玩家。
class Solution {
public:bool winnerOfGame(string colors) {//通过记录重复字符的个数来判断每个人能够进行多少次删除操作int dela=0,delb=0;int n=colors.size();int i=0;while(i<n){int start=i; //存储起始位置while(i<n&&colors[i]==colors[start]) i++; //判断相同字符的长度if(i-start>=3&&colors[start]=='A') dela+=i-start-2; //判断A和B可以删除的次数else if(i-start>=3&&colors[start]=='B') delb+=i-start-2; }return dela>delb;}
};
2900. 最长相邻不相等子序列 I
题解:注意题目中:“不能出现连续的0或1”,不能出现连续的0或1,则对于连续的0或1,我们只取其中一个添加到答案中即可;使用分组循环找出连续的0或1。
class Solution {
public:vector<string> getLongestSubsequence(vector<string>& words, vector<int>& groups) {int n=words.size();int i=0;vector<string> ret; //记录结果while(i<n){int start=i; //记录起始位置while(i<n&&groups[i]==groups[start]) i++; ret.push_back(words[start]);} return ret;}
};
3011. 判断一个数组是否可以变为有序
题解:使用分组循环将数组依据二进制中1的个数进行分配,将每一组的元素大小与上一组中最大值进行比较,判断是否可以进行排序。关于二进制中1的计算可以使用内置函数
__builtin_popcount(int n)
class Solution {//计算二进制位中1的个数int setbit(int n){int count = 0;while (n){n = n & (n - 1);count++;}return count;}public:bool canSortArray(vector<int>& nums) {int pre_max = 0; //记录上一个最小值int n = nums.size(), i = 0;while (i < n ){int start = setbit(nums[i]); //记录开始位置的二进制位个数int in_max = 0;while (i < n && setbit(nums[i]) == start){if(nums[i]<pre_max) return false; //判断当前位置元素大小是否比上一组最大元素大in_max = max(in_max, nums[i]); //记录该组元素最大值i++;}pre_max = in_max;}return true;}
};
1578. 使绳子变成彩色的最短时间
题解:分组循环,将相同颜色的气球作为一组,将相同颜色的气球进行删除,删除时为了花费最小的时间,将花费时间长的进行保留。
class Solution {
public:int minCost(string colors, vector<int>& neededTime) {//将有多余颜色的气球删除,保留连续颜色中移除时间最小的一个int n=colors.size();int i=0,ret=0;while(i<n){int start=i; //记录起始位置long sum=0,in_max=0; //记录所有气球的总花费时间,以及最长花费时间while(i<n&&colors[i]==colors[start]) {sum+=neededTime[i];in_max=max(in_max,(long)neededTime[i]);i++;}if(i-start>1) ret+=sum-in_max;}return ret;}
};
1759. 统计同质子字符串的数目
题解:计算每一组相同字符串的长度K,每一组会使得结果增加k+k-1+k-2+k-3...+2+1 即(k+1)*k/2。
class Solution {#define MOD 1000000007
public:int countHomogenous(string s) {//计算每一组相同字符串的长度K,每一组会使得结果增加k+k-1+k-2+k-3...+2+1 即(k+1)*k/2int n=s.size();int i=0;long long ret=0;while(i<n){int start=i;while(i<n&&s[i]==s[start]) i++;long long len=i-start;ret+=(len+1)*len/2;}return ret%MOD;}
};
1839. 所有元音按顺序排布的最长子字符串
题解:分组循环,以每一个a开头的字符串作为一组,记录该字符串长度并判断是否由所有元音字母按顺序组成的。
class Solution {
public:int longestBeautifulSubstring(string word) {vector<char> vowel({'a','e','i','o','u'});int n=word.size();int i=0,ret=0;while(i<n){while(i<n&&word[i]!='a') i++; //找以a开头的字符串int start=i,pos=0;for(pos=0;pos<5;pos++) //遍历所有元音字符{if(word[i]!=vowel[pos]) break; //判断是否存在该元音字符while(word[i]==vowel[pos]) i++;} if(pos==5) ret=max(ret,i-start); //更新答案}return ret;}
};
2765. 最长交替子数组
题解:根据
s[m - 1] - s[m - 2] = (-1)^m可以推出
nums[ i - 1]==nums[ i + 1];一次对数组进行分组以s1=s0+1的位置为起始位置,进行循环找到最长子数组。
class Solution {
public:int alternatingSubarray(vector<int>& nums) {int i=0,n=nums.size();int ret=-1;while(i<n){while(i<n-1&&nums[i+1]-nums[i]!=1) i++; //找到第一个满足条件的位置if(i==n-1) return ret;int start=i++;while(i<n-1&&nums[i-1]==nums[i+1]) i++; //根据s[m - 1] - s[m - 2] = (-1)^m //可以推出nums[i-1]==nums[i+1]ret=max(ret,i-start+1);}return ret;}
};
3255. 长度为 K 的子数组的能量值 II
题解:分组循环,将每一个递增为1的子数组作为一组,当该子数组的长度>=k的时候就有能量值,继续向后查找看子数组是否还能更长,如果能则nums[i]就是下一组的能量值。对于输出数组可以与原数组之间建立位置关系,i----->i-k+1的位置,这样就可以依据i位置确定答案更新到的具体下标位置。
class Solution {
public:vector<int> resultsArray(vector<int>& nums, int k) {//使用分组循环,将连续递增的数据分为一组,当该组的长度>=k的时候就可以将最大元素放入即nums[i]//根据放入元素下标可以与返回结果下标建立联系,nums[i]放在i-k+1的位置if(k==1) return nums; //对于k==1就不需要找子数组,直接进行返回即可int n=nums.size();vector<int> ret(n-k+1,-1); //将数组初始化为-1int i=0;while(i<n){int start=i; //记录起始位置i++;while(i<n&&nums[i-1]+1==nums[i]) {//子数组长度为i-start+1;if(i-start+1>=k) ret[i-k+1]=nums[i]; //当子数组长度大于等于k时就可以进行插入i++;}}return ret;}
};
3350. 检测相邻递增子数组 II
题解:根据题意,找两个长度相同相邻且都是严格递增的数组;使用分组循环,计算出一个长度为len的连续递增子数组后,1)可以与上一个相邻子数组进行组合,找到合适的k,k就是两个连续数组长度的较小的哪一个;2)还可以将长度为len的数组拆分为两个长度为len/2的数组,也是满足条件的。
class Solution {
public:int maxIncreasingSubarrays(vector<int>& nums) {//分组循环,对每次查找的递增数组进行记录int n=nums.size();int i=0;int prev=0,k=0;while(i<n){int start=i++;while(i<n&&nums[i-1]<nums[i]) i++;int len=i-start;k=max(k,len/2); //对该数组进行拆分k=max(k,min(len,prev)); //当前数组长度与上一个数组长度对比prev=len;}return k;}
};
3105. 最长的严格递增或递减子数组
题解:找严格递增和严格递减的数组,使用分组循环,将每一个严格递增或递减的数组分为一组。
class Solution {
public:int longestMonotonicSubarray(vector<int>& nums) {//简单的分组循环int n=nums.size();int i=0,ret=1;while(i<n-1){int start=i++;int flag=nums[i]-nums[start]; //记录是递增还是递减的while(flag<0&&i<n-1&&nums[i]>nums[i+1]) i++;while(flag>0&&i<n-1&&nums[i]<nums[i+1]) i++;if(flag!=0) ret=max(ret,i-start+1); //相等不需要进行更新}return ret;}
};
拓展 (加餐)
838. 推多米诺
题解:可能会出现的情况有4种:1)[L,L],[R,R]这两种都是可以直接对数组进行修改的;2)[L,R]是不需要进行操作的;3)[R,L]是需要对中间进行操作的。
如果数组最前面一个非.字符是L的话,为了组成[L,L]的形式,在原数组前面插上一个哨兵为L,结尾一样插入一个R
class Solution {
public:string pushDominoes(string dominoes) {//将区间可以分为始终[L,L],[R,R]这两种都是可以直接对数组进行修改的//[L,R]是不需要进行操作的;//[R,L]是需要对中间进行操作的// 如果数组最前面一个非.字符是L的话,为了组成[L,L]的形式,在原数组前面插上一个哨兵为L,结尾一样插入一个Rstring s="L"+dominoes+"R";int n=s.size();int prev=0; //记录上一个字符位置for(int i=1;i<n;i++){if(s[i]=='.') continue;if(s[prev]==s[i]){//是[L,L]或者[R,R]//从[prev,i]都是相同的L或者Rfill(s.begin() + prev + 1, s.begin() + i, s[prev]);}else if(s[i]=='L'){//[R,L]的情况//一共有len=i-prev+1个元素int len=i-prev+1;//将其分为两半(prev,prev+len/2] [i-len/2+1,i)fill(s.begin() + prev + 1, s.begin() + prev + len / 2, s[prev]);fill(s.begin() + i - len / 2 + 1, s.begin() + i, s[i]);}prev = i;}return s.substr(1,n-2);}
};
467. 环绕字符串中唯一的子字符串
题解:因为不能有重复的子字符串所以要进行筛选,如果直接存储每个已经存在的子字符串效率太低;此处可以使用一个哈希表来存储以每一个字符为结尾的子字符串的个数;
以当前字符为结尾,使得循环可以一直向后走也能保证记录每一个子字符串。
class Solution {
public:int findSubstringInWraproundString(string s) {//环绕字符串种唯一的子字符串//因为不能有重复的子字符串所以要进行筛选//如果直接存储每个已经存在的子字符串效率太低//可以使用一个哈希表来存储每一个以当前字符结尾的子字符串的个数//以当前字符为结尾,使得循环可以一直向后走也能记录每一个子字符串int n=s.size();int i=0,ret=0;int l=0;vector<int> length(26,0); //记录以每一个字符为结尾字符串的长度for(int i=0;i<n;i++){if(i>0&&(s[i-1]-'a'+1)%26!=s[i]-'a') l=i; //如果不是环绕字符串重新开始int len=i-l+1; //计算前面环绕字符串的长度if(len>length[s[i]-'a']) //如果以当前字符结尾的字符串长度比之前数组种记录的更长就需要对结果进行更新{ret+=len-length[s[i]-'a'];length[s[i]-'a']=len;}}return ret;}
};
3499. 操作后最大活跃区段数 I
题解:根据题意就是找 0----1----0区段中1旁边两个0个数之和最大值,将这些0变成1后活跃区段就是最大值。可以先将所有的0区段的长度进行存储,再找出相邻区段和的最大值。
class Solution {
public:int maxActiveSectionsAfterTrade(string s) {//找0 ---- 1 ---- 0的区间,将每一个0区间段的个数进行记录,然后找到两个相邻区段最大和int n=s.size();int i=0,ret=0;vector<int> zero; //记录每一个区间中0的个数while(i<n){while(i<n&&s[i]=='1') i++,ret++;int start=i;while(i<n&&s[i]=='0') i++;if(i!=start) zero.push_back(i-start); }int r=0,l=0,more=0,tmp=0; //查找相邻两个区间最大和while(r<zero.size()){while(r<zero.size()&&r-l<2) tmp+=zero[r++];if(r-l==2&&tmp>more) more=tmp;tmp-=zero[l++];}return ret+more;}
};
可以对上面代码进行优化,不进行0的个数存储,只记录两个0区段之和的最大值。
class Solution {
public:int maxActiveSectionsAfterTrade(string s) {//进行优化int n=s.size(),i=0;int ret=0;int prev=INT_MIN,zero=0; //用prev记录上一个0区段的长度,zero记录相邻两个0区段和的最大值while(i<n){int start=i;while(i<n-1&&s[i]==s[i+1]) i++; int len=i-start+1; //计算该区段的长度if(s[start]=='1') ret+=len; //如果是1直接加入答案else{zero=max(zero,prev+len); //与最大值进行比较prev=len;}i++;}return ret+zero;}
};
2593. 标记所有元素后数组的分数
题解:对数组下标和元素大小进行排序,从小到大依次进行选择,将被选择的元素左右下标元素进行删除即可。
class Solution {
public:long long findScore(vector<int>& nums) {//带下标进行排序int n=nums.size();vector<pair<int,int>> tmp;for(int i=0;i<n;i++)tmp.push_back({nums[i],i});sort(tmp.begin(),tmp.end(),[](pair<int,int> x,pair<int,int> y){if(x.first!=y.first) return x.first<y.first;else return x.second<y.second;}); //根据元素大小和下标进行排序//进行选择int i=0;long long ret=0;unordered_set <int> del; //存储被删除元素的下标for(int i=0;i<n;i++){int index=tmp[i].second;if(del.count(index)==0){ret+=tmp[i].first;del.insert(index+1);del.insert(index-1);}}return ret;}
};
2948. 交换得到字典序最小的数组
题解:如果一组数字之间总有两个数之差<=limit则这组数可以进行任意交换,也就意味着可以将其交换为有序的;所以此题就转化为了找一组数组该组数字间中有两数之差<=limit,如果直接对整个数组进行查找效率太低,此次可以向进行排序,排序后就很容易查找了,但是排序之前还要将数组元素大小与数组下标绑定,否则在后面进行交换时找不到具体下标位置。
在排序后,通过分组循环找到子数组其相邻元素之差<=limit,这样的子数组就可以进行任意交换,再根据保留的下标将下标和元素大小进行对应。
class Solution {
public:vector<int> lexicographicallySmallestArray(vector<int>& nums, int limit) {//可以先将数组进行排序,根据limit判断那一组的数字是可以进行交换的//但是在排序后数组就是乱序的,所以排序时要带上下标int n=nums.size();vector<pair<int,int>> tmp;for(int i=0;i<n;i++)tmp.push_back({nums[i],i});//对数组进行排序sort(tmp.begin(),tmp.end(),[](pair<int,int> x,pair<int,int> y){ return x.first<y.first;}); //只需要根据元素大小进行排序即可//使用分组循环,根据limit判断那些数字是可以进行交换的vector<int> ret(n);int i=0;while(i<n){int start=i++;while(i<n&&tmp[i].first<=tmp[i-1].first+limit) i++; //提取出[start,i)位置的下标vector<int> index;for(int j=start;j<i;j++) index.push_back(tmp[j].second);//对数组下标进行排序sort(index.begin(),index.end());//将下标和元素大小的对应关系映射到答案上for(int k=0;k<index.size();k++)ret[index[k]]=tmp[k+start].first;}return ret;}
};
总结
分组循环有点类似与同向双指针,在处理分组循环类题目时关键点在于正确设计内外层循环的分工,并处理好边界条件。该模板可解决大多数需要统计连续子数组特征的竞赛题目。