【算法深练】分组循环:“分”出条理,化繁为简 - 教程
目录
引言
分组循环
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& nums, int threshold) { //找到最长数组,数组满足两个条件:1)奇偶交替;2)子数组最大值不超过threhold int i=0,n=nums.size(); int ret=0; while(ithreshold)) i++; //先找到满足题意的第一个位置 int start=i; //记录起始位置 while((i
1446. 连续字符

题解:经典的分组循环题目,从多个字符起点开始,找出最长的相同子字符串的长度。
class Solution {public: int maxPower(string s) { int n=s.size(); int i=0,ret=0; while(i
1869. 哪种连续子字符串更长

题解:与上一题一样,但是此题有两种字符需要进行记录;因此可以直接使用两个整形对两种字符的最长长度进行记录即可。
class Solution {public: bool checkZeroOnes(string s) { //分别记录1和0的最长字符串的长度 int n=s.size(); int i=0; int one=0,zero=0; while(izero; }};
2414. 最长的字母序连续子字符串的长度

题解:分组循环,只是子数组判断条件不同,要求字母序连续。
class Solution {public: int longestContinuousSubstring(string s) { //字母序连续 int n=s.size(); int i=0,ret=0; while(i
3456. 找出长度为 K 的特殊子字符串

题解:不需要控制长为K区间来判断时候满足条件,可以直接找出每一段字符串中相同字符的个数,判断该长度是否是K即可。
class Solution {public: bool hasSpecialSubstring(string s, int k) { //找长度为K的相同字符串的长度 int n=s.size(); int i=0; while(i
1957. 删除字符使字符串变好

题解:连续字符的个数不超过3,可以在内循环中判断以下该重复字符是第几个,能否进行插入。
class Solution {public: string makeFancyString(string s) { int n=s.size(); int i=0; string ret; while(i
674. 最长连续递增序列

题解:找出最长的子数组,遍历数组;通过外层循环记录起始位置,内层循环找出子数组结束的位置。
class Solution {public: int findLengthOfLCIS(vector& nums) { //找递增的子数组 int n=nums.size(); int i=0,ret=0; while(inums[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& arr) { //数组一增一减的趋势 int n=arr.size(); if(n==1) return 1; int i=0,ret=1; while(i0?1:-1; //记录上一个位置是递增还是递减 while(i0?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& prices) { //一段平滑下跌子数组的长度为k //其满足条件的子数组个数为:k+k-1+k-2+k-3....+1为(k+1)*k/2 int n=prices.size(); long long i=0,ret=0; while(i
228. 汇总区间

题解:与上一题类似,找连续递增的子数组,并且每次递增1。
class Solution {public: vector summaryRanges(vector& nums) { //进行分组循环 int n=nums.size(); int i=0; vector ret; while(i"; tmp+=to_string(nums[i]); ret.push_back(tmp); } i++; } return ret; }};
1887. 使数组元素相等的减少操作次数

题解:进行模拟;先对数组进行排序,从后往前遍历,当前位置与前一个位置不同时要对[i,n-1]进行改变;向前遍历直到遍历到0停止。
class Solution {public: int reductionOperations(vector& 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& arr) { //分别求单调递增和递减区间 int ret=0,n=arr.size(); int i=0; while(iarr[i+1]) //记录山脉右侧 { flag2=1; i++; } if(flag1&&flag2) ret=max(ret,i-start+1); if(i
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=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 getLongestSubsequence(vector& words, vector& groups) { int n=words.size(); int i=0; vector ret; //记录结果 while(i
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& 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]
1578. 使绳子变成彩色的最短时间

题解:分组循环,将相同颜色的气球作为一组,将相同颜色的气球进行删除,删除时为了花费最小的时间,将花费时间长的进行保留。
class Solution {public: int minCost(string colors, vector& neededTime) { //将有多余颜色的气球删除,保留连续颜色中移除时间最小的一个 int n=colors.size(); int i=0,ret=0; while(i1) 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/2 int n=s.size(); int i=0; long long ret=0; while(i
1839. 所有元音按顺序排布的最长子字符串

题解:分组循环,以每一个a开头的字符串作为一组,记录该字符串长度并判断是否由所有元音字母按顺序组成的。
class Solution {public: int longestBeautifulSubstring(string word) { vector vowel({'a','e','i','o','u'}); int n=word.size(); int i=0,ret=0; while(i
2765. 最长交替子数组

题解:根据s[m - 1] - s[m - 2] = (-1)^m可以推出
nums[ i - 1]==nums[ i + 1];一次对数组进行分组以s1=s0+1的位置为起始位置,进行循环找到最长子数组。
class Solution {public: int alternatingSubarray(vector& nums) { int i=0,n=nums.size(); int ret=-1; while(i
3255. 长度为 K 的子数组的能量值 II

题解:分组循环,将每一个递增为1的子数组作为一组,当该子数组的长度>=k的时候就有能量值,继续向后查找看子数组是否还能更长,如果能则nums[i]就是下一组的能量值。对于输出数组可以与原数组之间建立位置关系,i----->i-k+1的位置,这样就可以依据i位置确定答案更新到的具体下标位置。
class Solution {public: vector resultsArray(vector& nums, int k) { //使用分组循环,将连续递增的数据分为一组,当该组的长度>=k的时候就可以将最大元素放入即nums[i] //根据放入元素下标可以与返回结果下标建立联系,nums[i]放在i-k+1的位置 if(k==1) return nums; //对于k==1就不需要找子数组,直接进行返回即可 int n=nums.size(); vector ret(n-k+1,-1); //将数组初始化为-1 int i=0; while(i=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& nums) { //分组循环,对每次查找的递增数组进行记录 int n=nums.size(); int i=0; int prev=0,k=0; while(i
3105. 最长的严格递增或递减子数组

题解:找严格递增和严格递减的数组,使用分组循环,将每一个严格递增或递减的数组分为一组。
class Solution {public: int longestMonotonicSubarray(vector& nums) { //简单的分组循环 int n=nums.size(); int i=0,ret=1; while(inums[i+1]) i++; while(flag>0&&i
拓展 (加餐)
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,结尾一样插入一个R string s="L"+dominoes+"R"; int n=s.size(); int prev=0; //记录上一个字符位置 for(int i=1;i
467. 环绕字符串中唯一的子字符串

题解:因为不能有重复的子字符串所以要进行筛选,如果直接存储每个已经存在的子字符串效率太低;此处可以使用一个哈希表来存储以每一个字符为结尾的子字符串的个数;
以当前字符为结尾,使得循环可以一直向后走也能保证记录每一个子字符串。
class Solution {public: int findSubstringInWraproundString(string s) { //环绕字符串种唯一的子字符串 //因为不能有重复的子字符串所以要进行筛选 //如果直接存储每个已经存在的子字符串效率太低 //可以使用一个哈希表来存储每一个以当前字符结尾的子字符串的个数 //以当前字符为结尾,使得循环可以一直向后走也能记录每一个子字符串 int n=s.size(); int i=0,ret=0; int l=0; vector length(26,0); //记录以每一个字符为结尾字符串的长度 for(int i=0;i0&&(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 zero; //记录每一个区间中0的个数 while(imore) 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
2593. 标记所有元素后数组的分数

题解:对数组下标和元素大小进行排序,从小到大依次进行选择,将被选择的元素左右下标元素进行删除即可。
class Solution {public: long long findScore(vector& nums) { //带下标进行排序 int n=nums.size(); vector> tmp; for(int i=0;i x,pair y){ if(x.first!=y.first) return x.first del; //存储被删除元素的下标 for(int i=0;i
2948. 交换得到字典序最小的数组

题解:如果一组数字之间总有两个数之差<=limit则这组数可以进行任意交换,也就意味着可以将其交换为有序的;所以此题就转化为了找一组数组该组数字间中有两数之差<=limit,如果直接对整个数组进行查找效率太低,此次可以向进行排序,排序后就很容易查找了,但是排序之前还要将数组元素大小与数组下标绑定,否则在后面进行交换时找不到具体下标位置。
在排序后,通过分组循环找到子数组其相邻元素之差<=limit,这样的子数组就可以进行任意交换,再根据保留的下标将下标和元素大小进行对应。
class Solution {public: vector lexicographicallySmallestArray(vector& nums, int limit) { //可以先将数组进行排序,根据limit判断那一组的数字是可以进行交换的 //但是在排序后数组就是乱序的,所以排序时要带上下标 int n=nums.size(); vector> tmp; for(int i=0;i x,pair y){ return x.first ret(n); int i=0; while(i index; for(int j=start;j
总结
分组循环有点类似与同向双指针,在处理分组循环类题目时关键点在于正确设计内外层循环的分工,并处理好边界条件。该模板可解决大多数需要统计连续子数组特征的竞赛题目。