双指针TwoPoints(尺取)入门与实战(C++)


方便自己预习也帮大佬们复习

概述

概念:
《双指针》是白鹿澄在晋江文学网上发布的网络小说个人理解双指针是在一个数组或链表中通过移动两个指针(有人爱说左右指针也有人爱说快慢指针)来对其中的区间或者两指针指向的数进行操作,来满足某种条件的做法。
过程:
已知某种要求,要选取合适的子区间,可用快指针首先跑到合适的上界再不断改变慢指针的位置来改变两指针位置,也可同时移动快慢指针,但其中要注意题目要求保持成立。
要求:
就目前入门情况看只需要
1.快慢指针左右关系不变。
2.能选取的两指针构造的区间或端点都必须满足题目要求。


目前题型不多,会慢慢更新
下列代码均为AC代码,请放心食用

双指针在查找区间查找端点时是一种便捷高效的算法,某种程度上虽然也算暴力,但却能将普通暴力所造成的O(nx)简化为O(n)。
双指针非常灵活,千万不能被传统做法的思维所框架,一定要发散才能发现双指针的巧妙应用方式

经典入门题

最丰富子数组

POJ3320测试链接

题意简述:
有一个数组,求能包含数组内所有出现过元素的子区间最短长度
(这个意思的题十分入门十分经典,不同的地方都会按这个题意设置故事背景来进行改变,简而言之换汤不换药)

输入描述:
单实例
第一行输入正整数n
第二行输入元素个数为n的数组
(1 ≤ n ≤ 1000000)

输出描述:
输出一个n代表求得子区间长度

样例:
输入:
5
1 8 8 8 1
输出:
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

思路:
1.右指针跑到全部元素都涵盖的位置
2.不断缩进左指针
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstdlib>
#include <cstring>
#include <map>
#include <set>
using namespace std;
const int maxn = 1000010;
set<int> cnt;           //记录一共多少个不同的元素
map<int, int> each_cnt; //记录一个元素出现多少次
int a[maxn];
int main()
{
    
    
    each_cnt.clear();
    cnt.clear(); //个人习惯先倒下垃圾
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
    {
    
    
        scanf("%d", &a[i]);
        cnt.insert(a[i]);
    }
    int all_cnt = cnt.size(); //set妙用,相同的元素只能成功插入一次
    //开始了,双指针,此为同向移动指针,left为慢指针(左指针),i为快指针(右指针)
    int left = 0;
    int sum = 0;   //sum统计两指针内不同元素的个数
    int min1 = n;  //min1用来维护最小值
    for (int i = 0; i < n; i++) //前进右指针
    {
    
    
        if (each_cnt[a[i]] == 0)
            sum++;
        each_cnt[a[i]]++;
        if (sum == all_cnt)
            min1 = min(min1, i - left + 1);
        while (sum == all_cnt && left < i) //压缩左指针
        {
    
    
            if (each_cnt[a[left]] > 1) //最左边的元素在区间内出现过不止一次,可以缩进
            {
    
    
                each_cnt[a[left]]--;
                left++;
            }
            else //若只出现过一次则不能再缩进了
                break;
            min1 = min(min1, i - left + 1);//维护最小值
        }
    }
    printf("%d", min1);
    return 0;
}

简单双指针题

1.不要npy!

牛客测试链接

题目描述
现在有一个长度为m的只包含小写字母‘a’-‘z’的字符串x,求字符串中不同时含有n,p,y三个字母的最长字串的长度是多少?。(对于字符串”abc”来说,”c”,”ab”都是原串的子串,但”ac”不是原串子串)

样例
输入:
“abcdefghijklmn”
输出:
14
说明:
因为所有子串都不同时含有n,p,y,所以最长子串的长度即为字符串x的长度14。

输入:
“ynp”
输出:
2
说明:
长度为2的字串”yn”,”np”都符合题意,不存在长度>=3的符合条件的子串。

输入:
“ypknnbpiyc”
输出:
7
说明:
“pknnbpi”为其符合条件的最长子串,长度为7。
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

思路:
与入门题一样,只不过是求max
class Solution {
    
    
public:
    int cnt[300]={
    
    0};
    bool check()//判断n,p,y是否同时存在
    {
    
    
        return cnt['n'-'a']&&cnt['p'-'a']&&cnt['y'-'a'];
    }
    int Maximumlength(string x){
    
    
        int len=x.size();
        int left=0;//左指针
        int max1=0;//维护最大值
        for(int i=0;i<len;i++)
        {
    
    
            cnt[x[i]-'a']++;
            while(check())
            {
    
    
                cnt[x[left]-'a']--;
                left++;
            }
            max1=max(max1,i-left+1);
        }
        return max1;
    }
};
2.寻找所有相等和

zzuli2744测试链接
题目描述:
有一个数num
将所有正整数中区间和为num的区间打出来

输入描述:
一个整数n
10 <= n <= 200000000

输出描述:
每一行都输出满足和为n的区间

样例:
输入:
10000
输出:
18 142
297 328
388 412
1998 2002
说明:
1998+1999+2000+2001+2002 = 10000
所以1998到2002为10000的一个解
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

思路:
1.等差数列求和
2.利用与查找的数n的关系判断该向右移动右指针还是左指针
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstdlib>
#include <cstring>
#include <map>
#include <set>
using namespace std;
typedef long long ll;
int main()
{
    
    
    ll num;
    cin >> num;
    int left = 1, right = 2;//左右指针
    while(left<right)
    {
    
    
        ll ans = (left + right) * (right - left + 1) / 2;//两指针之间的和
        if(ans==num)
            cout << left << " " << right << endl, right++;//十分重要,不加right++了话就会在碰到第一个成立的区间时死循环
        else if(ans<num)
            right++;//小了就推进右指针来扩大ans
        else
            left++;//大了就缩进左指针来减小ans
    }
    return 0;
}
3.寻找近似和

POJ2566测试链接

题目描述
在一数组内
寻找与某个数num最接近的区间和的子区间

输入描述:
多实例,输入0 0截止
第一行输入n,k,n表示数组长度,k表示要查找的数的个数
0<=n<=1000000
第二行输入数组,含n个整数
第三行输入k个整数num
0<=num<=1000000000

输出描述:
每一行输出对应num查找到的区间和、区间左边界、区间右边界。

样例
输入
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
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

思路:
//看见区间和就想到转化为前缀和计算
1.记录前缀和
2.排序前缀和
3.利用与查找的数num的关系判断该向右移动右指针还是左指针
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstdlib>
#include <cstring>
#include <map>
#include <set>
using namespace std;
const int INF = 0x3f3f3f3f;
pair<int, int> sum[100010];//涉及子区间和的问题想前缀和
int main()
{
    
    
    int n, k, num;
    while(scanf("%d%d",&n,&k),n+k)
    {
    
    
        sum[0].first = sum[0].second = 0;//first记录前缀和,second记录编号
        for (int i = 1; i <= n;i++)
        {
    
    
            cin >> num;
            sum[i].first = sum[i - 1].first + num;
            sum[i].second = i;
        }
        sort(sum, sum + n + 1);//升序排列方便寻找
        while(k--)
        {
    
    
            cin >> num;
            //开始双指针
            int left = 0, right = 1;//left为左指针,right为右指针
            int son_ans, son_left, son_right;
            int min1 = INF;//维护最小值
            while(right<=n&&min1)
            {
    
    
                int dir = sum[right].first - sum[left].first;//两位置前缀和差为其内部区间和
                if((int)abs(dir-num)<min1)
                {
    
    
                    min1 = (int)abs(dir - num);
                    son_ans = dir;
                    son_left = sum[left].second;
                    son_right = sum[right].second;
                }
                if(dir>num)//区间和大于num便把左指针右移动以减小区间和
                    left++;
                else//区间和小于num便把右指针右移动以增大区间和
                    right++;
                if(left==right)
                    right++;
            }
            if(son_left>son_right)//排序后遗症,容易颠倒位置
                swap(son_right, son_left);
            printf("%d %d %d\n", son_ans, son_left + 1, son_right);
        }
    }
    return 0;
}
4.最长连续递增序列(可转换为取单词)

Leetcode测试链接

题目描述:
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。

样例:
输入:nums = [1,3,5,4,7]
输出:3
解释:
最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

输入:nums = [2,2,2,2,2]
输出:1
解释:
最长连续递增序列是 [2], 长度为1。

0 <= nums.length <= 104
-109 <= nums[i] <= 109

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

思路:
1.右指针先跑到出现递减数列为止
2.计算此时左右指针之间的距离
3.左指针代替右指针的位置来标注新的起点
class Solution {
    
    
public:
    int findLengthOfLCIS(vector<int>& nums) {
    
    
         int len=nums.size();
         if(len<=1) return len;
         int max1=0;//维护最大值
         int left=0;
         int right=1;
         while(right<len)
         {
    
    
             while(right<len&&nums[right-1]<nums[right]) right++;//右指针先跑
             max1=max(max1,right-left);
             left=right;//左指针替代右指针位置作为起点
             right++;//右指针开始了它新的征程
         }
         return max1;
    }
};
5.反转单词(上题变形)

leetcode测试链接

这可是道剑指offer哦,看来既然学会双指针基础应用之后也没那么遥不可及,别害怕,不难

题目描述:
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。

样例:
输入: “the sky is blue”
输出: “blue is sky the”

输入: " hello world! "
输出: “world! hello”
解释:
输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

输入: “a good example”
输出: “example good a”
解释:
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

说明:
无空格字符构成一个单词。
输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

思路:
1.利用双指针截取单词(与上题一样)
2.反转单词顺序
class Solution {
    
    
public:
    string reverseWords(string s) {
    
    
        string ss[1000];//截取单词
        int len=s.size();
        int left=-1;
        int right=0;
        int cnt=0;//存放单词的个数
        while(right<len)
        {
    
    
            int flag=1;//用来特判这一段s(left---right)是不是空格
            while(right<len&&s[right]!=' ') right++,flag=0;//如果非空格就进行前进
            if(flag==0) //非空格
            {
    
    
                ss[cnt]=s.substr(left+1,right-left-1);//截取这一段存放进ss[cnt]作为一个单词
                cnt++;
            }
            left=right;//左指针代替右指针原来的位置
            right++;//开始了,新的征程
        }
        string outs;
        for(int i=cnt-1;i>=0;i--)
        {
    
    
            outs+=ss[i];
            if(i!=0) outs+=" ";//因为我们做的只存放了单词,所以在输出的单词之间应该加上空格
        }
        return outs;
    }
};

思维双指针题

倒装双指针+前缀最大值

CodeForces《Card Deck》测试链接

题目描述:
有两个队列a、b,a内有n个整数牌pi,每次可以移动任意张牌顺序不变地移动到b牌队,求一种最大排列方式b使得∑(i=1~n)nn−i⋅pi

输入描述:
第一行一个cass代表实例数
之后每个实例先输入一个n表示a牌列内的牌数,再输入n个整数代表a牌列内的排列方式

输出描述:
每个实例输出一种排列方式(必须满足题目要求)

样例:
输入
4
4
1 2 3 4
5
1 5 2 4 3
6
4 2 5 3 6 1
1
1
输出
4 3 2 1
5 2 4 3 1
6 1 5 3 4 2
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

这道题在刚拿到手的时候一直用STL来写(find函数、erase函数),但是TLE肯定
于是优化

用mx数组储存前缀最大值保证不用每次都用个复制数组sort一下并找当前的最大值
用一个慢指针表示a现在的牌数代替erase
#include <stack>
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <cstring>
#include <cstdio>
#include <cmath>
#define rep1(i, a, n) for (int i = a; i <= n; i++)
#define rep2(i, a, n) for (int i = a; i >= n; i--)
#define mm(a, b) memset(a, b, sizeof(a))
#define elif else if
typedef long long ll;
const int INF = 0x7FFFFFFF;
const double G = 10;
const double eps = 1e-6;
const double PI = acos(-1.0);
using namespace std;

int n;//总牌数
int cass;//样例数
int len;//a牌栈内牌数
int a[100005];//a牌栈
int mx[100005];//前缀最大值

int main()
{
    
    
    for (scanf("%d", &cass); cass; cass--)
    {
    
    
        mm(mx, 0);
        scanf("%d", &n);
        rep1(i,1,n)
        {
    
    
            scanf("%d", &a[i]);
            mx[i] = max(mx[i - 1], a[i]);
        }

        len = n;
        rep2(i,n,1)//模拟栈思想,从后往前
        {
    
    
            if(mx[i]==a[i])//如果a[i]就是当前栈内最大值,就开始放进b栈(输出)
            {
    
    
                rep1(j, i, len) printf("%d ", a[j]);//从i到目前a栈牌数
                len = i-1;//更新a栈牌数(被移走了)
            }
        }
        printf("\n");
    }
    return 0;
}
1.分配休息日(非常规双指针)

CodeForces1041C测试链接

题目描述:
Monocarp想休息,但他每次只能休息一小时,他的老板不让他连着休息,并且给他安排了两次休息不能小于的间隔时间d,但Monocarp向老板申请了n次休息的机会,每天工作m个小时,请问他如何安排每天的休息时刻(也就是如何把每个休息时刻安排在合理的日子),才能在最少的天数内用完这n次休息时间

输入描述:
第一行n,m,d
1≤n≤2⋅105,n≤m≤109,1≤d≤m
第二行输入n个数,代表他想休息的时刻

输出描述:
第一行输出用完这n次休息时刻的最少天数
第二行输出n个数,为对应输入中每个时刻应安排在第几天

样例:
输入:
4 5 3
3 5 1 2
输出:
3
3 1 1 2
说明:
第一天:1 5
第二天:2
第三天:3

输入:
10 10 1
10 5 7 4 6 3 2 1 9 8
输出:
2
2 1 1 2 2 1 2 1 1 2
说明:
第一天:1 3 5 7 9
第二天:2 4 6 8 10
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

思路:
//学会双指针要灵活应用,思考如何利用双指针来卡区间
//此时我们要卡的区间便是不能被赋值指针赋值的空区间,
//增加其内部天数,
//所以此双指针用来双向赋值
1.贪心。升序排列出时刻大小,便于赋值指针移动赋值“第几天”
{
    
    
    2.主指针确定某个时刻应放到第几天
    3.赋值指针后移对可以匹配上主指针那一天的时刻赋值
}循环
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstdlib>
#include <cstring>
#include <map>
#include <set>
using namespace std;
typedef long long ll;
struct node
{
    
    
    int num;//表休息的时刻
    int day;//表该放在第几天
    int ii;//表原数组的顺序i
} no[200005];
bool cmp1(node a, node b)//将休息的时刻从小到大
{
    
    
    return a.num < b.num;
}
bool cmp2(node a, node b)//将数组恢复为输入顺序
{
    
    
    return a.ii < b.ii;
}
int main()
{
    
    
    int n, m, d;
    cin >> n >> m >> d;
    for (int i = 1; i <= n; i++)
        cin >> no[i].num, no[i].ii = i, no[i].day = 0;
    sort(no + 1, no + n + 1, cmp1);
    int cnt = 0;//可用来利用的天数
    for (int i = 1, j = 1; i <= n; i++)//注意:此时i为主指针,j为赋值指针(用来判断将第j个休息时刻放在第几天)
    {
    
    
        if (!no[i].day)
            no[i].day = ++cnt;
        while (j <= n && (no[j].day || no[j].num - no[i].num <= d))
            j++;//j移动到可以再次被放在第no[i].day天为止
        no[j].day = no[i].day;
    }
    sort(no + 1, no + n + 1, cmp2);
    cout << cnt << endl;
    for (int i = 1; i <= n; i++)
        cout << no[i].day << " ";
    return 0;
}

2.将数组分成三个子数组的方案数(浅议三指针)(寻找近似和的升级版)

leetcode测试链接

题目描述:
给定一个连续数组nums,可以被两刀劈成三个部分,问有多少种劈法可以使得左子数组和<=中子数组和<=右子数组和,将结果对1e9+7取模

样例:
输入:nums = [1,1,1]
输出:1
解释:唯一一种好的分割方案是将 nums 分成 [1] [1] [1] 。

输入:nums = [1,2,2,2,5,0]
输出:3
解释:nums 总共有 3 种好的分割方案:
[1] [2] [2,2,5,0]
[1] [2,2] [2,5,0]
[1,2] [2,2] [5,0]

输入:nums = [3,2,1]
输出:0
解释:没有好的分割方案。

(3 <= nums.length <= 105 ,0 <= nums[i] <= 104
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

思路:
1.又是区间和,首想前缀和
2.用两个左指针卡出右指针在固定位置时的左指针可能性
class Solution {
    
    
public:
    const int mod=1e9+7;
    int sum[100010]={
    
    0};//前缀和
    int waysToSplit(vector<int>& nums) {
    
    
        int len=nums.size();
        int ans=0;
        for(int i=1;i<=len;i++) sum[i]=sum[i-1]+nums[i-1];
        for(int i=3,left1=2,left2=2;i<=len;i++)//left1,left2为左指针可取范围的左右边界
        {
    
    
            while(sum[len]-sum[i-1]<sum[i-1]-sum[left1-1]) //题意
                left1++;
            while(left2+1<i&&sum[i-1]-sum[left2]>=sum[left2]) //题意
                left2++;
            if(left1<=left2&&sum[len]-sum[i-1]>=sum[i-1]-sum[left2-1]&&sum[i-1]-sum[left1-1]>=sum[left1-1])//查看是否满足要求 
                ans=(ans+left2-left1+1)%mod;
        }
        return ans;
    }
};

猜你喜欢

转载自blog.csdn.net/SnopzYz/article/details/113060277