双指针是算法中常见的处理方式。
在处理线性的题目时,通常会有提到另外的说法,滑动窗口
(或尺取)。
以常见的字符串为例,我们设定两个指针,左指针left
,右指针right
当两个指针都确定了在串中的位置,则可以截取一个字串。我们可以把它比作是一个窗口,而当两个指针进行移动的时候,那这个窗口也就滑动了。
下面我将以例题驱动讲解算法思路和实现过程。(C++描述)
常见题目分析
- 存在一个指定序列
- 是否指定
子序列
长度
- 确定长度,
固定窗口
- 不确定长度,但有范围,
不定长窗口
- 需要对子序列进行访问和操作
- 只有当我们处理完所有子序列时才能保证获得最终答案
这些题目通常都比较模板,不同点往往在于 不同题对子序列的不同处理需求
固定窗口型是不定长窗口型的学习基础,当然思路和实现也比较简单
固定窗口
例题 :力扣:239. 滑动窗口最大值
题意:
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
这题虽然时hard题,但是却是hard中的easy题,连很多中等题的难度都达不到。要是还是怕,后面我会放同类型的easy和normal题来先练习。
分析:
需要一种数据结构满足以下条件
- 能够存储子序列的所有值
- 能够方便的
移除left
和加入right
- 能够获得该数据结构中的
最值
一般来说我们会优先想到
set
map
priority_queue
这三类。但
priority_queue
无法直接移除left
,set
无法记录重复的值。因为我们可以使用
map
处理,key存放数值,value记录出现次数但在C++中有
multiset
这么一种可以记录重复数据的特殊set
因此以下代码将以multiset
来展示。
实现:
用确定的数据结构初始化序列
移动窗口
- 窗口
右端点进入
由数据结构记录- 窗口
左端点移出
数据结构每次移动窗口时对数据结构进行算法操作,不断迭代操作获得最后的结果
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
// greater<int> 由大到小排序
multiset<int, greater<int>> mst;
// 初始化第一个窗口
// 由此窗口的长度确定
for (int i = 0; i < n && i < k; i++) {
mst.insert(nums[i]);
}
vector<int>ans;
ans.push_back(*mst.begin());
for (int right = k; right < n; right++) {
// multiset 直接 erase数值,会将所有的数值均删除
// 因此只删除期中一个值最好使用迭代器
// 左端点left移出
int left = i-k;
auto it = mst.find(nums[left]);
mst.erase(it);
// 右端点right进入
mst.insert(nums[right]);
// 每次移动处理窗口处理窗口
ans.push_back(*mst.begin());
}
return ans;
}
};
相关练习:
力扣:567. 字符串的排列
不定长窗口
例题:牛客:智乃的密码
题目描述:
智乃去注册账号,他发现网站的的密码必须符合以下几个条件
密码是仅包含大小写英文字母、数字、特殊符号的字符串。
密码的长度不少于L个字符,并且不多于R个字符。
密码中应该至少包括①大写英文字母、②小写英文字母、③数字、④特殊符号这四类字符中的三种。
所谓特殊字符,是指非大小写字母、数字以及空格回车等不可见字符的可见字符,包括但不限于"~!@#$%^&*()_+"。
现在智乃有一个长度大小为N的字符串S,她想知道S串中有多少个子串是一个符合条件的密码,请你帮助智乃统计符合条件的密码数目。
子串是指字符串中某一段连续的区间,例如对于字符串"abcde"来说,“abc”,"cde"都是它的子串,而"ace"不是它的子串。
输入描述:
第一行输入三个正整数N,L,R(1≤N≤10^5,1≤L≤R≤N)表示S串的长度,合法密码长度应该在L到R个字符之间。
接下来一行输入一个长度为N的字符串S,字符串仅包括①大写英文字母、②小写英文字母、③数字、④特殊符号四类字符。
输出描述
仅一行一个整数,表示有多少子串是一个合法的密码。
示例1
输入
10 6 8
asdfeg111*输出
3
说明
“eg111*”
“feg111*”
“dfeg111*”
该题出自 2022牛客寒假算法基础集训营3 (付费比赛)可能要付费才能做
以后会寻找更好的免费题来替换
分析: 这题还好没有玩什么文字游戏和考验语文阅读能力
串中的每个字符可以划分为4种状态,因为可以预处理,将字符串化为4种标记的状态数组
该题属于统计合格子串数量的题型
判断字串合格的check()
通常不同题的要求也不同在这里。
该题比较好写,这里不多赘述。
如果是定长的字串,那就是上面一种滑动窗口的题型了,相对来说好些很多,但这里是不定长。
对于不定长的题目通常一个默认条件就是字串长度的范围在
[0, str.length()]
之间但本题给出了
最短L
和最长R
(通常题目也都会给这些条件)
实现思路:
- 设定两个指针,
左指针left
右指针right
- 固定
左指针left
让右指针right
不断探索,直到找到合格的字串- 直到check()到合格的字串,右指针停下,此时有3种状态 (注意这里的check()不考虑长度)
right < L <= R
长度不合格L <= right <= R
长度合格L <= R < right
长度不合格- 注意,当
右指针right
到最后都没check()成功的时候,说明之后再也没各个的字串了,可以直接break掉- 先思考,如果是理想的状态那就是
left+L
到left+R
都合格- 但实际是需要根据
右指针right
来获得合格串
- 左端点的最小值
int rightMin = max(right, left+L-1);
- 右断点的最大值
int rightMax = min(n-1, left+R-1);
- 最后判断
rightMin <= rightMax ? true : false
即可
#include <bits/stdc++.h>
#define int long long
using namespace std;
signed main (void) {
int n;
int L, R;
cin >> n >> L >> R;
string s;
cin >> s;
vector<int> kinds(n);
for (int i = 0; i < n; i++) {
if (isupper(s[i])) {
kinds[i] = 0;
} else if (islower(s[i])) {
kinds[i] = 1;
} else if (isdigit(s[i])) {
kinds[i] = 2;
} else {
kinds[i] = 3;
}
}
function<bool(vector<int>)> check = [](const vector<int>& arr) {
int cnt = 0;
for (int num : arr) {
if (num > 0) {
cnt++;
}
}
return cnt >= 3;
};
vector<int> cnt(4);
int ans = 0;
// [left, right]闭区间
// 以left左指针来枚举可行的状态
for (int left = 0, right = -1; left < n; left++) {
// 直到合法为止
while (!check(cnt)) {
// 此处保证出循环时right下标处于已经计算的状态
right++;
if (right < n) {
cnt[kinds[right]]++;
} else {
break;
}
}
// right到最后都没有合法的check
if (!check(cnt)) {
break;
}
// 保证合法的左端点 && 具有L的长度
int rightMin = max(right, left+L-1);
// 右断点不越界 && 在R的范围内
int rightMax = min(n-1, left+R-1);
ans += max(1LL*0, rightMax-rightMin+1);
cnt[kinds[left]]--;
}
cout << ans << endl;
return 0;
}
本题注意要开下long long 不然不能过
虽然我自己把 N=1e5 L=1 R=1e5 串是4种状态循环的长度1e5的串 测了一下
int 和 long long 答案是一样的。
还是有比我上述case更大的答案case?总不见得是平台或是出题人的问题吧?
这里right的操作比较自由,我强定了right必须同步记录状态
比较好写的方式是先记录完再right++则此时合法check()退出循环,那right指向的是合法串的后一个位置
下面计算rightMin也要注意修改写法上的细节