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