一般用于做具有单调性的,满足某一性质的区间问题。常见的快慢指针,对撞指针,滑动窗口)貌似有些人称之为尺取法
进入屡次被相关题目按在地上摩擦,便简单地进行整理小结,本文仅介绍一些极其基础的双指针题目,可能还有错误,大佬轻喷
T1 数组截取
以左端点为基准写法(这个想起来比较顺,但是貌似适应性不强?)
分析:
前缀和预处理
枚举遍历枚举左端点,扩展右端点
区间和三种状态
> k停止扩展
答案维护:如果=k则对答案进行更新维护
#include
#pragma GCC optimize(3,"Ofast","inline")
using namespace std;
typedef long long ll;
inline char nc(){
static char buf[100000],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++;
}
inline ll _read()
{
char ch=nc();
ll sum=0;
while(!(ch>='0'&&ch<='9'))ch=nc();
while(ch>='0'&&ch<='9')sum=sum*10+ch-48,ch=nc();
return sum;
}
ll n,k;
ll R=1,Ans=-1;
const int N=2e7+10;
ll sum[N];
int main()
{
n =_read() , k = _read();
for(int i = 1 ; i <= n ; i ++) sum[i] = sum[i - 1] + _read();
for(int i = 1 ; i <= n ; i ++)
{
while(R < n && sum[R + 1] - sum[i - 1] <= k) R ++;
if(sum[R] - sum[i - 1] == k)Ans = max(Ans,R - i + 1);
}
cout << Ans;
return 0;
}
以右端点为基准写法(这个写法的话普适性比较强)
遍历右端点
边界:左端点<=右端点,和>k
操作:在边界内,sum>k就一直缩小,当sum=k时更新答案
#include
#pragma GCC optimize(3,"Ofast","inline")
using namespace std;
typedef long long ll;
inline char nc(){
static char buf[100000],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++;
}
inline ll _read()
{
char ch=nc();
ll sum=0;
while(!(ch>='0'&&ch<='9'))ch=nc();
while(ch>='0'&&ch<='9')sum=sum*10+ch-48,ch=nc();
return sum;
}
ll n,k;
int R=1,Ans=-1;
const int N=2e7+10;
ll sum[N];
int main()
{
n =_read() , k = _read();
for(int i = 1 ; i <= n ; i ++) sum[i] = sum[i - 1] + _read();
for(int i = 1,j=1; i <= n ; i ++)
{
while(j<=i&& sum[i] - sum[j - 1]>k) j++;
if(sum[i]-sum[j-1]==k)Ans=max(Ans,i-j+1);
}
cout << Ans;
return 0;
}
T2 Blash数集
分析:
快慢指针,作用维护升序
哪个小移动哪个,相同两个都移动(集合的概念无重复)
边界:cnt达到n
#include
using namespace std;
typedef long long ll;
ll n,a,cnt;
const int N=1e5+10;
int q[N];
int main()
{
int posa=1,posb=1,cnt=1;
scanf("%lld%lld",&a,&n);
q[1]=a;
while(cnt<=n)
{
int x=q[posa]*2+1;
int y=q[posb]*3+1;
if(x<y)
{
q[++cnt]=x;
posa++;
}
else if(x>y)
{
q[++cnt]=y;
posb++;
}
else
{
q[++cnt]=x;
posa++;
posb++;
}
}
printf("%lld\n",q[n]);
return 0;
}
T3 相似的数集高级版
分析:
学长的博客
快慢指针找两个集合交集元素个数(相等元素个数)
边界:其中一个指针到达最后一个元素就结束所以复杂度约为O(N)
原理利用单调性,两个集合元素单调递增
即存在这样关系
①A[posA] ②B[posB] 对于两个位置的元素他们右如下关系
A[posA]
A[posA] 操作:为了令A=B,A向右移动
A[posA]=B[posB]:
A[posA]=B[posB] A[posA]=B[posB] 操作:维护更新答案,A,B一起向右移动(其实随便移动一个后另外一个必定还要移动)
A[posA]>B[posB]:
B[posB] 操作:为了令A=B,B向右移动
T4 判断子序列
分析:
边界:两个指针不越界pa<=n&&
如果相同则子序列和原序列指针同时右移动进行下一位匹配
如果不同,移动原序列
#include
using namespace std;
const int N=1e5+10;
int a[N],b[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=m;i++)cin>>b[i];
int pa=1;
int pb=1;
while(pa<=n&&pb<=m)
{
if(a[pa]!=b[pb])pb++;
else pa++,pb++;
}
if(pa==n+1)puts("Yes");
else puts("No");
return 0;
}
T5 最长连续不重复子序列
假的双指针(1800+ms)
分析:
以左端点为基准(可能是我不会写左边的缘故)
遍历左端点,扩展右端点直到碰到重复
更新答案(注意此时右端点是已经重复的那个),询问下一左端点
#include
using namespace std;
const int N=1e5+10;
bool tj[N];
int a[N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
int l=1,r=1,ans=1;
while(l<=n)
{
memset(tj,0,sizeof tj);
while(!tj[a[r]]&&r<=n)
{
tj[a[r]]=1;
r++;
}
ans=max(ans,r-l);
l++;r=l;
}
cout<<ans;
return 0;
}
真的双指针(50+ms)
分析:
以右端点为基准遍历
边界:左端点<=右端点,包含重复
只要区间包含重复,左端点缩减,并消除标记
当不包含重复的时候更新答案
#include
using namespace std;
const int N = 100010;
int a[N], s[N];
int main()
{
int n = 0, res = 0;
cin >> n;
for(int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
for(int i = 0, j = 0; i < n; i ++ )//右端点
{
s[a[i]] ++;
while(j <= i && s[a[i]] > 1) //不满足条件
{
s[a[j]] -- ;
j ++ ;
}
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
T6 数组元素的目标和
分别从左端和右端开始找
如果a[i]+b[j]>x j向左移动尽可能让他变小
接下来两种可能(等于x和小于x)
如果a[i]+b[j]==x 输出答案
如果直接进入下一循环,i向右移动
#include
using namespace std;
const int N=1e6+10;
typedef long long ll;
ll a[N],b[N];
ll n,m,x;
int main()
{
scanf("%lld%lld%lld",&n,&m,&x);
for(int i=0;i<n;i++)scanf("%lld",&a[i]);
for(int i=0;i<m;i++)scanf("%lld",&b[i]);
for(int i=0,j=m-1;i<n;i++)
{
while(j>=0&&a[i]+b[j]>x)j--;
if(a[i]+b[j]==x)
{
printf("%d %d",i,j);
break;
}
}
return 0;
}
比较通用的一个代码结构
for(int i = 0, j = 0; i < n; i ++ )//考虑起点,都为开头/两端什么的
{
//check函数一般反着写,while循环一直缩小边界,直到可能为答案的区间
//注意是可能,比如上面一些题目<和=就是可能的情况,但是更新答案只在=的时候
while (j < i && check(i, j)) j ++ ;//注意边界问题
//更新答案
}
需要区分的一个情况
求满足某一性质的最短/最长连续子序列,注意这里的连续。
区分几个概念
子序列:从原序列中抽掉几个元素,剩下的序列为子序列,子序列不一定连续
子序列要特别说明了才算连续
子串:字符串中任取l,r他们截出来的连续字符串就是子串
比较典型的就是:
最长上升子序列
我在写这篇博客的时候瞄了眼这题,起手打了个双指针后来发现貌似不对,这题用DP做比较好
另外:文末推荐可以看看洛谷的这篇日报:尺取法小结