双指针 学习笔记
概述
双指针是一种简单的技巧,通过两个指针维护一些具有单调性、可快速增删的区间信息。
一般要求的是求满足某个条件的最长/最短区间。当一个指针增加时,单调性要求满足这个条件的分界点是单调递增的。枚举一个指针,让另一个指针不断增加直到满足条件即可。两个指针都只会遍历一遍,复杂度 \(O(n)\)。
例题
题意:求最短的子区间使区间中出现了 \([1,m]\) 的所有数。
显然区间中数字出现的个数具有单调性。维护两个指针 \(l,r\)。\(l\) 移动时,\(r\) 是单调递增的。枚举 \(l\),每次让 \(r\) 不断增加直到满足条件。
#include<bits/stdc++.h>
using namespace std;
int n,m,a[1000005],cnt[2005],ansl=0,ansr=0x3f3f3f3f;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
for(int l=1,r=0,now=0;l<=n;){
while(now<m&&r<n)now+=!cnt[a[++r]]++;
if(now<m)break;
if(r-l+1<ansr-ansl+1)ansl=l,ansr=r;
now-=!--cnt[a[l++]];
}
return cout<<ansl<<' '<<ansr<<'\n',0;
}
题意:求最长的子区间使区间中的数没有重复。
类似上题,但是这题是求最长的区间。这时改为枚举 \(r\),拉动 \(l\) 移动。
#include<bits/stdc++.h>
using namespace std;
int t,n,m,a[1000005],ans;
unordered_map<int,int>cnt;
int main(){
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int l=1,r=0;r<=n;){
while(cnt[a[r]]>1)cnt[a[l++]]--;
ans=max(r-l+1,ans),cnt[a[++r]]++;
}
cout<<ans<<'\n',ans=0,cnt.clear();
}
return 0;
}
Baka's Trick
双指针时要维护当前区间的信息。假如这个信息不支持删除,但支持快速合并,可以用下面的方式:
假设要找满足条件的最长区间,维护指针 \(mid\),并维护两个栈表示 \([l,mid]\) 的后缀和,\((mid,r]\) 的前缀和。当 \(l\) 增加越过 \(mid\) 时,将 \(mid\) 设为 \(r\),清空右栈并枚举 \([l,r]\) 重构左栈。区间信息可以用两个栈内的信息拼起来。
复杂度主要多出了重构部分。每次重构时的 \(l\) 是上次重构时的 \(r+1\),因此重构的区间是不相交的,复杂度 \(O(n)\)。
差分后转化为求 \(\gcd>1\) 的最长区间,使用上面的技巧即可。
#include<bits/stdc++.h>
using namespace std;
int t,n,ans;
long long a[200005],st[200005];
template<typename T>T gcd(T m,T n){
return n?gcd(n,m%n):m;
}
int main(){
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
n--;
for(int i=1;i<=n;i++)a[i]=abs(a[i]-a[i+1]);
for(int i=1,j=1,mid=0;i<=n;i++){
st[i]=i==mid+1?a[i]:gcd(st[i-1],a[i]);
while(j<=mid&&gcd(st[j],st[i])==1)j++;
if(j>mid){
mid=i,st[i]=a[i];
for(int k=i-1;k>=j;k--)st[k]=gcd(a[k],st[k+1]);
while(j<=i&&st[j]==1)j++;
}
ans=max(ans,i-j+1);
}
cout<<ans+1<<'\n',ans=0;
for(int i=1;i<=n;i++)st[i]=0;
}
return 0;
}
[[杂项]]