双指针,尺取法小结

双指针,尺取法小结

  • 双指针介绍
    • 题型总结
    • 例题分析
    • 做题总结

双指针介绍

一般用于做具有单调性的,满足某一性质的区间问题。常见的快慢指针,对撞指针,滑动窗口)貌似有些人称之为尺取法

进入屡次被相关题目按在地上摩擦,便简单地进行整理小结,本文仅介绍一些极其基础的双指针题目,可能还有错误,大佬轻喷

题型总结

  1. 求满足某一性质的最短/最长连续子序列
  2. 字符串匹配
  3. 维护升序
  4. 寻找满足某一条件的点对/数值

例题分析

T1 数组截取
双指针,尺取法小结_第1张图片
以左端点为基准写法(这个想起来比较顺,但是貌似适应性不强?)
分析:
前缀和预处理
枚举遍历枚举左端点,扩展右端点
区间和三种状态
=k继续扩展直到不大于k(有可能后面为0呢)
> 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做比较好

另外:文末推荐可以看看洛谷的这篇日报:尺取法小结

你可能感兴趣的:(算法小结)