
尺取法(又称滑动窗口、双指针)是一种用于处理连续子序列问题的高效算法技巧。它通过维护一个区间(窗口),用两个指针动态调整窗口的边界,从而避免不必要的重复计算。
核心思想:
用两个指针(左指针L和右指针R)定义一个窗口[L,R],然后:
**1.**右指针R向右移动,扩大窗口,直到满足条件
**2.**左指针L向右移动,缩小窗口,直到不满足条件
3.在移动过程中记录答案
适用场景
尺取法适用于解决以下类型的问题:
| 问题类型 | 示例 | 条件 |
|---|---|---|
| 最长不重复子串 | "abcabcbb"的最长不重复子串 | 窗口内字符不重复 |
| 最短满足条件的子数组 | 和≥K的最短子数组长度 | 窗口内元素和≥K |
| 固定长度的统计 | 长度为K的子数组的最大平均值 | 窗口长度固定为K |
| 计数问题 | 包含所有字符的最短子串 | 窗口包含目标所有字符 |
滑动窗口又分为:定长窗口和不定长窗口
滑动窗口模板部分
定长滑动窗口模板
窗口固定长度为k
cpp
vector<int> fixedWindowSum(vector<int>& nums, int k) {
int n = nums.size();
if (n < k) return {}; // 数组长度小于窗口大小,返回空
vector<int> result;
int window_sum = 0;
// 计算第一个窗口的和
for (int i = 0; i < k; i++) {
window_sum += nums[i];
}
result.push_back(window_sum);
// 滑动窗口
for (int i = k; i < n; i++) {
// 加上新元素,减去旧元素
window_sum += nums[i] - nums[i - k];
result.push_back(window_sum);
}
return result;
}
不定长滑动窗口模板
和>=taget
窗口结束标志位r<n(r是窗口右边界,n是整个数组的大小,r从0开始),当r=n就代表窗口结束
循环内部里的while是窗口移动的条件,我们要满足这个窗口>=taget所以r移动到>=taget时,内部的left开始向右移动,使得窗口进行下去
cpp
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, right = 0;
int sum = 0;
int min_len = INT_MAX;
while (right < nums.size()) {
// 扩大窗口
sum += nums[right];
right++;
// 当窗口满足条件时,尝试收缩以找到最小窗口
while (sum >= target) {
min_len = min(min_len, right - left); // right已++,所以是right-left
sum -= nums[left];
left++;
}
}
return min_len == INT_MAX ? 0 : min_len;
}
其实大多数人都学过尺取法,也就是通常大家听到的滑动窗口,但是很多人做一些简单的题后,以为他就用到了简单题中,但其实有时候一道较难的题中用到后我们很难看出这题要用这个方法,所以下面的五道题目可以供大家去运用一下,下面的题也不算简单,先看一个举例的简单题
举例部分
我们拿一个例子来看,下面是题目链接
Palindromes _easy version - HDU 2029 - Virtual Judge
题目展示
"回文串"是一个正读和反读都一样的字符串,比如"level"或者"noon"等等就是回文串。请写一个程序判断读入的字符串是否是"回文"。
Input
输入包含多个测试实例,输入数据的第一行是一个正整数n,表示测试实例的个数,后面紧跟着是n个字符串。
Output
如果一个字符串是回文串,则输出"yes",否则输出"no".
输入
4
level
abcde
noon
haha
输出
yes
no
yes
no
思路:很简单的入门题,就是两个指针一个从前一个从后往中间缩窗口,一个一个判断就行
cpp
#include<iostream>
#include<string>
using namespace std;
string s;
int t;
int main(){
cin>>t;
while(t--){
cin>>s;
int j=s.size()-1;
int flag=0;
for(int i=0;i<=j;i++,j--){
if(s[i]!=s[j]){
flag=1;
break;
}
}
cout<<(flag==1?"no\n":"yes\n");
}
}
练习部分
题目一
链接 :Subsequence - POJ 3061 - Virtual Judge
题目展示
给你一个包含 N 个正整数的序列(10 < N < 100000),每个数都不超过 10000,还有一个正整数 S(S < 100000000)。请写个程序,找出序列中连续子序列的最短长度,使得这段子序列的和大于或等于 S。
输入
第一行是测试用例的数量。每个测试用例的第一行包含两个数字 N 和 S,用空格分开。第二行是序列中的 N 个数字,也用空格分开。输入以文件结束符结束。
输出
每个测试用例输出一行结果,表示满足条件的最短子序列长度。如果找不到符合条件的子序列,就输出 0。
样例
输入
2
10 15
5 1 3 5 10 7 4 9 2 8
5 11
1 2 3 4 5
输出
2
3
思路:这个是不定长滑动窗口,要找到大于等于s的最短窗口,这个就相当于模板了
cpp
#include<iostream>
#include<string>
#include<string.h>
#include<cstdio>
using namespace std;
int a[100030];
int n,s,t;
int main(){
cin>>t;
while(t--){
cin>>n>>s;
memset(a,0,sizeof(a));
for(int i=0;i<n;i++){
cin>>a[i];
}
int sum=0;
int r=0,l=0,mini=n+1;
while(r<n){
sum+=a[r++];
while(sum>=s){
mini=min(mini,r-l);
sum-=a[l++];
}
}
cout<<(mini==n+1?0:mini)<<endl;
}
}
题目二
链接:Bound Found - POJ 2566 - Virtual Judge
题目展示
接收到的信号很可能是外星起源,并已由航空航天管理局进行数字化(他们一定正在经历一个叛逆的阶段:"但我想使用英尺,而不是米!")。每个信号似乎分为两部分:一系列 n 个整数值和一个非负整数 t。我们不打算深入细节,但研究人员发现信号编码了两个整数值。这两个值可以通过找到序列中绝对值和最接近 t 的子范围的下界和上界来获得。
给定 n 个整数的序列和非负目标 t。你需要找到序列的一个非空范围(即一个连续的子序列),并输出其下标 l 和上标 u。从 l 到 u 的元素(包括 l 和 u)的值的绝对值和必须至少与任何其他非空范围的绝对值和一样接近 t。
输入
输入文件包含多个测试用例。每个测试用例以两个数字 n 和 k 开始。输入以 n=k=0 结束。否则,1<=n<=100000,接下来是 n 个绝对值 <=10000 的整数,构成序列。然后对该序列进行 k 次查询。每个查询是一个目标 t,满足 0<=t<=1000000000。
输出
对于每个查询,在一行中输出 3 个数字:某个最接近的绝对和以及实现该绝对和的某个范围的下标和上标。可能的下标从 1 开始,到 n 结束。
样例
输入
5 1
-10 -5 0 5 10
3
10 2
-9 8 -7 6 -5 4 -3 2 -1 0
5 11
15 2
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
15 100
0 0
输出5 4 4
5 2 8
9 1 1
15 1 15
15 1 15
思路 :滑动窗口的使用之前,必须要有单调性,因为你扩窗口时候要保证增大值,缩窗口值要减小值,题目中有负数,所以无法保证扩窗口增加值,所以我们可以进行前缀和操作,然后对前缀和数组进行排序,为什么能排序呢?,我们要求的是一段区间和的绝对值最接近t,我们对前缀和作差即是区间和,如果再进行绝对值操作,就能满足题目要求了,并且排序后也能使其具有单调性
cpp
#include<iostream>
#include<cmath>
#include<algorithm>
#include<climits>
using namespace std;
#define pii pair<int,int>
const int N=1e5+10;
pii sum[N];
int num[N];
void solve(int n,int k){
for(int i=1;i<=n;i++){
cin>>num[i];
}
for(int i=1;i<=n;i++){
sum[i]=make_pair(sum[i-1].first+num[i],i);
}
sort(sum,sum+n+1);
while(k--){
int t;
cin>>t;
int l=0,r=1;
int min_diff=0x3f3f3f3f;
int min_l,min_r;
int ans=0;
while(r<=n){
int now_diff=sum[r].first-sum[l].first;//区间和
int tmp=abs(now_diff-t);//区间和与t的差值
if(tmp<min_diff){
min_diff=tmp;//记录最小的差值
ans=now_diff;//ans=当前的区间和
min_l=sum[l].second;
min_r=sum[r].second;
}
if(now_diff>t){
l++;
} else if(now_diff<t){
r++;
} else break;
if(l==r){
r++;
}
//确保起始索引小于结束索引
if(min_l>min_r) swap(min_l,min_r);
}
// 区间和 = sum[min_r] - sum[min_l],对应原数组区间 [min_l+1, min_r]
cout<<ans<<' '<<min_l+1<<' '<<min_r<<endl;
}
}
int main(){
int n,k;
while(cin>>n>>k){
if(n==0&&k==0) break;
solve(n,k);
}
}
题目三
链接:First One - HDU 5358 - Virtual Judge
题目展示
soda有一个整数数组a1,a2,...,an。令S(i,j)为ai,ai+1,...,aj的和。现在soda想要知道下面的值:
注意:在这个问题中,你可以将log20视为0。
输入
有多个测试用例。输入的第一行包含一个整数T,表示测试用例的数量。对于每个测试用例:
第一行包含一个整数n(1≤n≤10^5),表示数组中整数的数量。
接下来一行包含n个整数a1,a2,...,an(0≤ai≤105)。
输出
对于每个测试用例,输出该值。
样例
输入
1
2
1 1
输出
12
思路 log₂的阶梯性:当S在[2ᵏ, 2ᵏ⁺¹)范围内时,⌊log₂(S)⌋都是k
S=0: k=0
1≤S<2: k=0
2≤S<4: k=1
4≤S<8: k=2
我们看这道题给的数据是1e5我们肯定不能按题中给的公式那样直接两层for循环,那样会超时,我们就可以利用题中给的这个log2来想到我们去找这些区间[2ᵏ, 2ᵏ⁺¹),就不需要让右端点一个一个移动,我们可以一个区间一个区间移动,具体可以去看代码,上面有较为详细的注释
cpp
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
typedef long long ll;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n;
cin>>n;
vector<ll>a(n+1);
vector<ll>pre(n+1,0);
for(int i=1;i<=n;i++) {
cin>>a[i];
pre[i]=pre[i-1]+a[i];
}
ll ans=0;
for(int i=1;i<=n;i++){//左端点还是要遍历的
int j=i;//j从i开始,去找区间
while(j<=n){
ll sum=pre[j]-pre[i-1];//得到j当前位置,i到j区间的和
int k=0;
if(sum>0){
k=(int)log2(sum);//计算对应k值
}
//然后我们要去找j到哪个位置正好区间和被log2还是k值
// 二分查找最大的(上面说的j到哪个位置)jj,使得pre[jj] - pre[i-1] < 2^(k+1)
int l=j,r=n;
int jj=j;
while(l<= r){
int mid=(l+r)/2;
if( pre[mid]-pre[i-1] < (1LL<<(k+1))){//判断i到mid这个范围的区间和是不是小于2^(k+1)
jj=mid;
l=mid+1;
}else{
r=mid-1;
}
}
// 计算区间 [j, j_end] 的贡献
int cnt = jj-j + 1;//计算一下 j到jj这个等于k的区间的长度,
//因为在这个区间都是k所以直接统一计算,就可以避免时间复杂度过高
//那个公式要*(i+j)那我们让 i*cnt,然后j会变也就是求出来j到jj的和,也就是等差数列求和
// = cnt * i + (j + j_end) * cnt / 2
ll sum_i_j=(ll)cnt*i+(ll)(j+jj)*cnt/2;
ans+=(k+1)*sum_i_j;
j=jj+1;//j跳到下一个区间的左边界
}
}
cout<<ans<<"\n";
}
return 0;
}
题目四
链接:A-B 数对 - 洛谷 P1102 - Virtual Judge
题目展示
给出一串正整数数列以及一个正整数 CC,要求计算出所有满足 A−B=CA−B=C 的数对的个数(不同位置的数字一样的数对算不同的数对)。
Input
输入共两行。
第一行,两个正整数 N,CN,C。
第二行,NN 个正整数,作为要求处理的那串数。
Output
一行,表示该串正整数中包含的满足 A−B=CA−B=C 的数对的个数。
样例
输入
4 1
1 1 2 3
输出3
数据
对于 75% 的数据,1≤N≤2000。
对于 100%的数据,1≤N≤2×10^5,0≤ai<2^30,1≤C<2^30。
思路
我们要找到是对应的A和B所以可以直接排序,然后有一个for循环去找A,对应的B就是A-C,然后取利益滑动窗口找到一个区间都是B的值,具体看代码
cpp
#include<iostream>
#include<algorithm>
#include<cstdio>
#define int long long
const int N=2e5+10;
int a[N];
using namespace std;
signed main(){
int n,c;
cin>>n>>c;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a,a+n+1);
int l=1,r=1;
int ans=0;
for(int i=1;i<=n;i++){
int B=a[i]-c;//将a[i]作为A
//下面a[r]和a[l]作为B去找到值都是B的区间
while(r<=n&&a[r]<=B){
r++;
}
while(l<=n&&a[l]<B){
l++;
}
ans+=(r-l);
}
cout<<ans;
}
题目五
链接:Unique Snowflakes - UVA 11572 - Virtual Judge
题目展示
企业家艾米莉有一个很酷的商业点子:包装并出售雪花。她设计了一台机器,可以捕捉飘落的雪花,并将其序列化成一个雪花流,一个一个地流入包装中。一旦包装满了,它就被封口并运走出售。
公司的营销口号是"独一无二的袋子"。为了兑现这一口号,包装中的每个雪花都必须与其他雪花不同。不幸的是,说起来容易做起来难,因为实际上,机器中流动的许多雪花是相同的。艾米莉想知道可以创建的独特雪花的最大包装尺寸是多少。机器可以在任何时间开始填充包装,但一旦开始,从机器流出的所有雪花都必须进入包装,直到包装完成并封口。包装可以在所有雪花从机器流出之前完成并封口。
输入
输入的第一行包含一个整数,指定要跟随的测试用例数量。每个测试用例以一行包含一个整数 nn 开始,表示机器处理的雪花数量。接下来的 nn 行每行包含一个整数(在 0 到 10^9之间,包括两端)唯一标识一个雪花。当且仅当两个雪花相同时,它们由相同的整数标识。
输入将总共包含不超过一百万个雪花。
输出
对于每个测试用例,输出一行包含一个整数,即包装中可能的最大独特雪花数量。
样例输入
1
5
1
2
3
2
1
样例输出
3
思路:题目要找的是最长的无重复元素的序列的长度,这是很经典的滑动窗口题,类似的题还有力扣上的找无重复的字母的最长序列,我们只需做一个哈希表记录每个数字上次出现的位置,然后进行窗口的移动,如果当前r遇到一个出现过的数字,判断一下这个数字上次出现的位置是不是比窗口的l位置大,如果大的话就直接让窗口嘴边移到 l 的下一个索引
cpp
#include<iostream>
#include<unordered_map>
#include<algorithm>
#include<cstdio>
#define int long long
int a[1000020];
using namespace std;
signed main(){
int t;
cin>>t;
while(t--){
int n;
cin>>n;
unordered_map<int,int>cnt;
int l=0,maxi=0;
for(int i=0;i<n;i++) cin>>a[i];
for(int r=0;r<n;r++){
int x=a[r];
if(cnt.find(x)!=cnt.end()&&cnt[x]>=l){
l=cnt[x]+1;
}
cnt[x]=r;
maxi=max(maxi,r-l+1);
}
cout<<maxi<<endl;
}
return 0;
}
写完了这五个题后其实也不算完,滑动窗口作为一种基础算法,其实是经常掺杂在许多的难题中,只不过不是很容易发现,这五道题也是让大家知道了滑动窗口不仅仅是像模板那样用,它还可以以很多形式出现。
希望讲解能帮助大家,对题目有不理解也可以来问我哦