算法常用技巧:双指针法(尺取法)的经典应用

尺取法:

尺取法通常是指对数组保存一对下标(起点、终点),然后根据实际情况交替推进区间左右端点以得出答案。
尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的时候,所以说尺取法是一种高效的枚举区间的方法,是一种技巧,一般用于求取有一定限制的区间个数或最短的区间等等。而且很少会出现纯模板题,更多的还是在复杂的问题中运用蕴含在该技巧中的思想,很多看似复杂的题(包括时间复杂度要求高的题),运用这些小技巧会大有裨益。
当然任何技巧都存在其不足的地方,有些情况下尺取法不可行,无法得出正确答案,所以要先自己判断好是否可以使用尺取法再开始来coding。


下面给出几道经典的题目:

例题1:

给出一段只包含正整数的区间,要求出满足区间和为S的所有区间。
输入:
5,1,4,5,10,10,20
输出:
2,5
5,6
7,7

解法分析:

该题目的解法有很多种,比如:
1.直接做一次O(n3)的枚举
2.先做一次前缀和的预处理(输入的时候直接计算即可),然后做一次O(n2)的枚举
3.尺取法(双指针法):尺取法又有两种写法,个人推荐第二种(挑战书上的写法)

代码:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>

using namespace std;

typedef long long ll;
const int inf = 0x3f3f3f3f;
const ll  INF = 0x3f3f3f3f3f3f3f3f;

/** 尺取法求满足和的区间 */
void FindSum(int *a,int n,int s)
{
    int st,ed;
    st = ed = 0;
    /* 第一种写法:
    int sum = a[0];
    while(st <= ed && ed < n)
    {
        if(sum >= s)
        {
            if(sum == s)
            {
                cout << (st+1) << "," << (ed+1) << endl;
            }
            sum -= a[st++];
        }
        else
        {
            sum += a[++ed];//注意是先++
        }
    }*/
    /* 第二种写法 */
    int sum = 0;
    while(1)
    {
        while(ed < n && sum < s)
        {
            sum += a[ed++];
        }
        if(sum == s)
        {
       		//注意这里的ed没有+1
            cout << (st+1) << "," << ed << endl;
        }
        if(sum < s)
        {
        	//如果sum < s,也就意味着上面退出循环是由于遍历完成了(ed>=n),所以直接break
            break;
        }
        sum -= a[st++];//注意不同于第一种写法,这里是后++
    }
}

int main()
{
    int a[] = {5,1,4,5,10,10,20};
    FindSum(a,7,20);
    return 0;
}

例题2(poj3061):

题目大意:输入一个n表示区间数的个数,s表示要求的和,然后输入区间中的n个数,
要求:求出满足区间和>=s的最小区间长度。

解法分析:

这道题目也有多重解法(其实和上面的题目很类似),比如:
1.直接做一次O(n3)的暴力枚举
2.先做一次前缀和的预处理(输入的时候直接计算即可),然后做一次O(n2)的枚举
3.尺取法(双指针法) O(n)
4.前缀和 + 二分法 O(nlog2n)

先贴出二分法的代码:(题解在注释中)

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>

using namespace std;

typedef long long ll;
const int inf = 0x3f3f3f3f;
const ll  INF = 0x3f3f3f3f3f3f3f3f;
const int MAX = 1e5+5;
int a[MAX];
int sum[MAX];

int main()
{
    int t;
    cin >> t;
    while(t--)
    {
        int n,s;
        cin >> n >> s;
        for(int i = 0; i < n; i++)
        {
            cin >> a[i];
        }

        /** 前缀和 + 二分法 */
        sum[0] = 0;
        for(int i = 0; i < n; i++)
        {
            sum[i+1] = sum[i] + a[i];
        }

        if(sum[n] < s)// 总和还小于n,那么也就不可能会有>=n的了
        {
            cout << 0 << endl;
            continue;
        }

        int min_len = inf;
        /** 比如sum[n]=100,s=30,要求区间和>=30,但是如果起点>70,比如为71,那么就算终点为n,也才29,所以sum[st] <= sum[n]-s */
        for(int st = 0; sum[st] <= sum[n] - s; st++)
        {
            /** 找出s到n中第一个>=sum[st]+s的点的下标,因为要保证-sum[st]以后,结果还能>=s,而且区间长度尽可能的小 */
            int ed = lower_bound(sum+st,sum+n,sum[st]+s) - sum;
            min_len = min(min_len,ed-st);
        }
        cout << min_len << endl;
     }
    return 0;
}

然后是尺取法的代码:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;

typedef long long ll;
const int inf = 0x3f3f3f3f;
const ll  INF = 0x3f3f3f3f3f3f3f3f;
const int MAX = 1e5+5;
int a[MAX];
int sum[MAX];

int main()
{
    int t;
    cin >> t;
    while(t--)
    {
        int n,s;
        cin >> n >> s;
        for(int i = 0; i < n; i++)
        {
            cin >> a[i];
        }
        
        /** 尺取法 */
        int st = 0;
        int ed = 0;
        int sum = 0;
        int min_len = inf;
        while(1)
        {
            while(ed < n && sum < s)
            {
                sum += a[ed++];
            }
            if(sum < s)
            {
                break;
            }
            min_len = min(min_len,ed-st);
            sum -= a[st++];
        }
        if(min_len > n)
        {
            //解不存在,其实就是sum加到了n个数,但是还是小于s,所以min_len=inf。
            min_len = 0;
        }
        cout << min_len << endl;
    }
    return 0;
}

例题三:poj3320

题意:输入页数p,每一页上有一个p[i]知识点,不同页可能有相同的知识点,要求,求出最少的连续页数,要覆盖到所有的知识点。
输入:
5
1 8 8 8 1
输出:
2

解法分析:首先先解释下样例(因为题目有可能不是那么好理解)
5-表示总页数
1 8 8 8 1-表示每一页上的知识点编号
所以可以知道知识点总共有2个,那么要怎么才能以最少的连续页数覆盖掉1、8这两个知识点呢?
很容易看出最少的页数就是2

解法:
首先要求出知识点个数,这里用个set来记录就ok了,num=set.size()
1.直接开一个O(n3)的循环就出来了。
2.尺取法(双指针法)
可能该代码实现最不好理解的就是,
while(1)里面的内容了,我逐条解释(请看注释)
(其实关键是map的使用技巧:实时记录下,每个知识点出现的个数)

代码:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <set>
#include <map>
using namespace std;

typedef long long ll;
const int inf = 0x3f3f3f3f;
const ll  INF = 0x3f3f3f3f3f3f3f3f;
const int MAX = 1e6+5;
int p;
int a[MAX];
set <int> id;//记录每页书上的知识点,id的长度就是知识点个数
map <int,int> mp;//实时每个知识点的出现次数

int main()
{
    while(cin >> p)
    {
        for(int i = 0; i < p; i++)
        {
            cin >> a[i];
            id.insert(a[i]);
        }
        int st,ed;
        st = ed = 0;
        int min_page = inf;
        int num = id.size();//知识点个数
        
        int sum = 0;
        while(1)
        {
        	//ed < 页数p && 实时的知识点个数 < 总知识点个数 
        	//=> 进入循环判断知识点sum是否++
            while(ed < p && sum < num)
            {
                if(mp[a[ed++]]++ == 0)
                {
                    //ed原本指向的值a[ed](知识点)还没有被用(mp[a[ed]]==0),说明该知识点可以加上
                    sum++;
                }
            }
            if(sum < num)
            {
            	//进入了该if语句,表示sum<num,
            	//但是退出了上面的循环,说明ed >= p,表示遍历完了(st和ed都是从0开始的,只能到p-1)
                break;
            }
            min_page = min(min_page,ed-st);//计算长度ed-st(自己按照样例模拟运行一遍就知道了)
            if(--mp[a[st++]] == 0)
            {
	 			//st原本指向的值a[st](知识点)在--以后没有了,
	 			//说明原来是1也就是该知识点只被用了一次,现在st往前面走,说明该知识点不用了,sum就--
                sum--;
            }
        }
        cout << min_page << endl;
    }
    return 0;
}

例题4:(盛最多水的容器)

题意:给定 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
在这里插入图片描述
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

解题分析:其实这题单看很容易,就是找出两条线使得中间的面积最大,比如说我找上述样例的x=1,x=9,那么面积就为(9-1) * min(1,7) = 8 * 1 = 8,但是如果找x=2,那么面积就为(9-2) * min(8,7) = 7 * 7 = 49的,所以直接得出:
解法1: 直接做一次O(2)的暴力枚举就ok了,但是不吃TLE,我就请你吃饭(嘻嘻)
解法2: 双指针法,设置一个st=1,ed=n,st向右边移动(++),ed向左边移动(- -),考虑,哪个先移动,如果是较大的高度向里面移动(st右移,ed左移),那么首先宽度会变小,其次和另一个的最小的高度只可能不变或者更小,所以面积就更小了,但是如果是较小的高度向里面移动,虽然宽度会变小,但是有可能会把高度提上来,所以面积可能会变大
总结:
要结合出面积的最大值,就是面积 = 宽 x 高 最大,那么一开始尽可能地宽最大(一个1和一个n),然后宽最大,可能高度太小了,然后就宽减小(st或者ed移动),要使面积增大,根据上述的解释就必须较小的高度对应的指针(st或者ed)移动,所以双指针的做法就很明显了。
(我写的可能比较冗余,如果看不明白可以直接看leetcode的题解,上面的解释很不错)

代码:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;

typedef long long ll;
const int inf = 0x3f3f3f3f;
const ll  INF = 0x3f3f3f3f3f3f3f3f;
const int MAX = 1e5+5;
int a[MAX];


int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    
    int max_area = -inf;
    int st,ed;
    st = 1; ed = n;
    while(st < ed)
    {
        max_area = max(max_area , (ed-st)*min(a[st],a[ed]));
        if(a[st] <= a[ed])//这里如果相等,还是st右移(ed左移其实也是一样的)
        {
            st++;
        }
        else
        {
            ed--;
        }
    }
    cout << max_area << endl;
    return 0;
}

例题5:51nod 2478

题意:小b将n个宽度相同的积木顺序摆在一起,如下图所示。
现在她告诉你每个积木的高度(可能为0)。
她想知道如果她从高处倒下一杯水,最多有多少单位的水能被积木接住?
假设每个积木的宽度都为1。
在这里插入图片描述
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,最多可以接 6 个单位的水(蓝色部分表示水)。

解题分析:
本题乍一看,不是跟上面那道题一样的嘛?
但其实并不是,分析题意就可以看出端倪,那道题是要求哪两根柱子中容纳的水量最多,这里是求出这么多方块可以容纳多少水。


解法:
还是双指针法,但是这里可能不是很明显,请继续往下看解析。
解法的关键所在就是:找出柱子最高的那条,并记录下它的下标位置
然后从左边(1)开始遍历到maxheightindex-1,实时记录下当前的最大值,
如果遇到height[i]>=tmpmaxheight,就更新当前的最大值,如果遇到<的就表示此时的柱子小,那么一定可以存储水块(左高-右低),所以sum+=tmpmaxheight-height[i]即可。


然后从右边(n)开始遍历到maxheightindex+1,也是实时记录下当前的最大值,如果遇到height[i]>=tmpmaxheight,就更新当前的最大值,如果遇到<的就表示此时的柱子小,那么一定可以存储水块(右高-左低),所以sum+=tmpmaxheight-height[i]即可。

这里的解法我没有说是尺取法,因为我觉得尺取法更偏向于从相同的方向出发,然后以不同的进度前进,但是这里都是从两边出发,然后到达最大值的位置(上一题也是从两边出发,控制st<ed即可),所以称做双指针法更合适。

代码:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;

typedef long long ll;
const int inf = 0x3f3f3f3f;
const ll  INF = 0x3f3f3f3f3f3f3f3f;
const int MAX = 5e4+5;
int n;
int height[MAX];

int main()
{
    cin >> n;
    int maxHeight = 0;
    int maxHeightIndex = 1;
    for(int i = 1; i <= n; i++)
    {
        cin >> height[i];
        if(height[i] > maxHeight)
        {
            maxHeight = height[i];//记录下最高的高度
            maxHeightIndex = i;	  //记录下最高高度的下标位置
        }
    }
    int res = 0;
    //从左边开始累加水
    int tmpMaxHeight = height[0];
    for(int i = 1; i < maxHeightIndex; i++)
    {
        if(height[i] >= tmpMaxHeight)
        {
            tmpMaxHeight = height[i];
        }
        else
        {
            res += tmpMaxHeight - height[i];
        }
    }
    //从右边开始累加水
    tmpMaxHeight = height[n];
    for(int i = n; i > maxHeightIndex; i--)
    {
        if(height[i] >= tmpMaxHeight)
        {
            tmpMaxHeight = height[i];
        }
        else
        {
            res += tmpMaxHeight - height[i];
        }
    }
    cout << res << endl;
    return 0;
}
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值