【算法系列总结之分组循环篇】
分组循环
分组循环指的是将整个数组或者字符串分为很多片段,这些片段的判断处理逻辑是一样的。分组循环需要使用同向双指针,但是与滑动窗口不同的是,滑动窗口是收集左右区间内连续数组或者字符串,当不满足收集要求时移动右指针,而当满足后移动左指针,此时左指针移动到原来左指针的下一位,而分组循环是左指针移动到右指针下一位。
// 模板 l、r分别表示左右指针
int l=0,r=0;
while(r<n)
{
// 每一次求新区间则重新赋值l
l=r;
// r表示最长连续区间最后一个
while(r<n-1&&s[r]==s[r+1])
r++;
// 求完区间后收集结果
res=max(r-l+1,res);
// 并移动r到下一个
r++;
}
1446.连续字符
思路:分组循环,相当于把字符串分为多个连续相同字符片段,利用左右端点l、r求解最长连续相同字符片段长度。
class Solution {
public:
int maxPower(string s) {
int n=s.size();
int l=0,r=0;
int res=0;
while(r<n)
{
// 每一次求新区间则重新赋值l
l=r;
// r表示最长连续区间最后一个
while(r<n-1&&s[r]==s[r+1])
r++;
// 求完区间后收集结果
res=max(r-l+1,res);
// 并移动r到下一个
r++;
}
return res;
}
};
1869.哪种连续子字符串更长
思路:分组循环,相当于把字符串分为多个连续1或者0字符片段,利用左右端点l、r和元素s[l]求解最长连续1或者0字符片段长度,再根据条件返回对应结果。
class Solution {
public:
bool checkZeroOnes(string s) {
// 分别记录最长1、最长0长度
int num1=0,num0=0;
int n=s.size();
// 区间左右端点
int l=0,r=0;
while(r<n)
{
// l表示每轮左区间
l=r;
// r表示当前符合条件区间右端点
while(r<n-1&&s[r]==s[r+1])
r++;
// 收集0或者1长度
if(s[l]=='0')
num0=max(num0,r-l+1);
else
num1=max(num1,r-l+1);
r++;
}
return num1>num0;
}
};
1957.删除字符使字符串变好
思路:分组循环,相当于把字符串分为多个连续相同片段,利用左右端点l、r求解每一个连续相同片段长度,如果长度小于3则直接将该片段加入结果,反之则只加入两个字符即可。
class Solution {
public:
string makeFancyString(string s) {
string res;
int n=s.size();
int l=0,r=0;
while(r<n)
{
l=r;
while(r<n-1&&s[r]==s[r+1])
r++;
if(r-l+1<3)
res+=s.substr(l,r-l+1);
else
{
// 加入两次
res.push_back(s[l]);
res.push_back(s[l]);
}
r++;
}
return res;
}
};
2038.如果相邻两个颜色均相同则删除当前颜色
思路:分组循环,相当于把字符串分为多个连续A或者B字符片段,利用左右端点l、r和元素colors[l]求解最长连续A或者B字符片段可选择的次数,再根据条件返回对应结果。注意,最长连续A或者B字符片段可选择的次数为在保留左右两边字符的情况下中间所有剩余字符,即一旦长度大于等于3则为长度减去2,反之则为0,由于A是先手,故A必须严格大于B。
class Solution {
public:
bool winnerOfGame(string colors) {
int n=colors.size();
// 转换为次数
int numA=0,numB=0;
int l=0,r=0;
while(r<n)
{
l=r;
while(r<n-1&&colors[r]==colors[r+1])
r++;
// 一旦>=3 则可移动的为长度减去左右两边2
if(colors[l]=='A')
numA+=(r-l+1)>=3?(r-l+1)-2:0;
else
numB+=(r-l+1)>=3?(r-l+1)-2:0;
r++;
}
return numA>numB;
}
};
1759.统计同质子字符串的数目
思路:分组循环,相当于把字符串分为多个连续相同片段,利用左右端点l、r求解每一个连续相同片段长度,然后利用1、3、6、10、15…规律根据长度n求出方案数n*(n+1)/2,最后累计方案数即可。
class Solution {
public:
const long long NUM=1e9+7;
int countHomogenous(string s) {
// 1 3 6 10 15 n*(n+1)/2
int n=s.size();
int l=0,r=0;
// long long 避免精度不够
long long res=0;
while(r<n)
{
l=r;
while(r<n-1&&s[r]==s[r+1])
r++;
int len=r-l+1;
// 注意取模
res+=(long long)(len+1)*len/2;
r++;
}
// 取模
return res%NUM;
}
};
2110.股票平滑下跌阶段的数目
思路:分组循环,相当于把数组分为多个连续公差为1的递减片段,利用左右端点l、r求解每一个连续公差为1的递减片段长度,然后利用1、3、6、10、15…规律根据长度n求出方案数n*(n+1)/2,最后累计方案数即可。
class Solution {
public:
long long getDescentPeriods(vector<int>& prices) {
int n=prices.size();
int l=0,r=0;
long long res=0;
while(r<n)
{
l=r;
// 当前日比前一日股价少一
while(r<n-1&&prices[r]==prices[r+1]+1)
r++;
long long len=r-l+1;
res+=len*(len+1)/2;
r++;
}
return res;
}
};
1578.使绳子变成彩色的最短时间
思路:分组循环,相当于把绳子分为多个连续相同气球片段,利用左右端点l、r求解每一个连续相同气球片段,并对每个大于1的片段求解总时间和最大时间从而得到最小时间,对这些时间累加即可得到结果。
class Solution {
public:
int minCost(string colors, vector<int>& neededTime) {
int n=colors.size();
int l=0,r=0;
int res=0;
while(r<n)
{
l=r;
while(r<n-1&&colors[r]==colors[r+1])
r++;
if(l!=r)
{
int maxn=0;
for(int i=l;i<=r;i++)
{
maxn=max(neededTime[i],maxn);
res+=neededTime[i];
}
res-=maxn;
}
r++;
}
return res;
}
};
1839.所有元音按顺序排布的最长子字符串
思路:分组循环,相当于把字符串分为多个按照元音字母递增的至少含有所有元音字母各一个的字符串,与前面几题不同的是,该题需要在记录符合区间的同时,也使用uset来记录元音字母个数,从而便于后序收集结果条件各个元音字母至少一个的判断。
class Solution {
public:
int longestBeautifulSubstring(string word) {
int n=word.size();
int l=0,r=0;
int res=0;
while(r<n)
{
l=r;
// 存储次数 保证各个字母至少出现一次
unordered_set<int> uset;
// 按照 a e i o u顺序
while(r<n-1&&word[r+1]>=word[r])
{
uset.emplace(word[r]);
r++;
}
// 加入最后一个 前几题都是只需r++ 不需额外处理
uset.emplace(word[r]);
// cout<<uset.size()<<endl;
if(uset.size()==5)
res=max(res,r-l+1);
r++;
}
return res;
}
};
2760.最长奇偶子数组
思路:分组循环,相当于把数组分为多个[l,r]区间,其中求出满足题目要求的长度最长的[l,r]区间。该题与上述题目不同的地方在于,该题中存在l、[l,r-1]、[l,r]三个判断,由于此处的差异就会导致在处理判断边界条件时可能会出现些许差异和错误。此处首先对于l不满足的情况进行特殊处理,由于已经处理过l,那么后续就是在l满足条件的情况下进行,此时将r加一,相当于比较r和r-1,而不是原先的r和r+1,那么相应的此处得到的r就是第一个不满足的,对应的长度变为r-l,同时也不需要继续对r++啦。(注意细节)
class Solution {
public:
int longestAlternatingSubarray(vector<int>& nums, int threshold) {
int n=nums.size();
int l=0,r=0;
int res=0;
while(r<n)
{
// 左区间都不满足情况直接下一个
if(nums[r]%2!=0||nums[r]>threshold)
{
// continue之前记得r++啊否则区间不动
r++;
continue;
}
// 此处才更新左区间
l=r;
// 左区间已经满足 此时r+1
r+=1;
// r是第一个不满足的 故此时长度为r-l 并且后续不需要r++
while(r<n&&nums[r]<=threshold&&(nums[r]%2!=nums[r-1]%2))
r++;
res=max(res,r-l);
}
return res;
}
};
2765.最长交替子序列
思路:分组循环,相当于把数组分为多个[l,r]区间,其中求出满足题目要求的长度最长的[l,r]区间。该题与2760不同的地方在于,该题所分得的区间可能重叠喔,比如2 3 4 3 4,其中2 3和3 4 3 4其重叠一个,但是其最多也只会重叠一个,故我们找到第一个不满足的r后回退1即可。建议在分组循环类别的题目中,遇到给区间[l,r]设置条件时先对首部条件进行不合法判断处理再继续喔!
class Solution {
public:
int alternatingSubarray(vector<int>& nums) {
int n=nums.size();
int l=0,r=0;
int res=-1;
// 数组长度至少为2!!!
while(r+1<n)
{
// 排除s1!=s0+1的情况
if(nums[r+1]-nums[r]!=1)
{
r++;
continue;
}
// 记录左端点
l=r;
r+=1; // 右端点加一 至少长度为2
int m=1;
// r是第一个不满足的
while(r<n&&nums[r]-nums[r-1]==m)
{
r++;
m*=-1;
}
res=max(res,r-l);
// 2 3 4 3 4 // 2 3和3 4 3 4重合一个 为什么能保证最多重复一个
r-=1; // 所以r需要回退一个
}
return res;
}
};
总结:灵老师的nums[i]==nums[i0+(i-i0)%2]也很灵性!!!
228.汇总区间
思路:分组循环,相当于把字符串分为多个连续公差为1的递增片段,利用左右端点l、r求解每一个连续公差为1的递增片段两端,并按照要求格式加入结果。
class Solution {
public:
vector<string> summaryRanges(vector<int>& nums) {
int n=nums.size();
int r=0,l;
string tmp;
vector<string> res;
// l 左端點 r右端點 同向雙指針
while(r<n)
{
l=r;
while(r<n-1&&nums[r]+1==nums[r+1])
r++;
tmp=to_string(nums[l]);
if(l<r)
tmp+="->"+to_string(nums[r]);
res.push_back(tmp);
r++;
}
return res;
}
};
有一说一,虽然现在力扣刷了四百多题,牛客刷了两百多题,包括前端和算法,但还是觉得遇到一些脑筋急转弯的简单或者中等题还是不会,对于一些题型也没有形成自己的解题思路逻辑,遇到困难题也是要看题解才行,除非自己刷过几次,感觉好挫败啊呜呜呜呜呜呜呜呜,要怎么办!!!