一、定义
顾名思义,像尺子一样取一段,尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。尺取法比直接暴力区间效率高很多,尤其是数据量大的时候,所以说尺取法是一种高效的枚举区间的方法,一般用于求取有一定限制的区间个数或最短的区间等等。
二、使用的注意事项
使用尺取法时应清楚以下四点:
1、什么情况下能使用尺取法
2、何时推进区间的端点
3、如何推进区间的端点
4、何时结束区间的枚举
尺取法通常适用于选取区间有一定规律,或者说所选取的区间有一定的变化趋势的情况,通俗地说,在对所选取区间进行判断之后,我们可以明确如何进一步有方向地推进区间端点以求解满足条件的区间,如果已经判断了目前所选取的区间,但却无法确定所要求解的区间如何进一步得到根据其端点得到,那么尺取法便是不可行的。首先,明确题目所需要求解的量之后,区间左右端点一般从最整个数组的起点开始,之后判断区间是否符合条件在根据实际情况变化区间的端点求解答案。
三、习题
奇数偶数分类
题目链接:http://47.96.116.66/problem.php?cid=1807&pid=0
解题思路:
题意为:一个数组中有奇数也有偶数,将所有奇数放到数组左边,将所有偶数放到数组右边
此题是尺取法的基础练手题,我们可以定义两个指针i和j,i的初始值为1,j的初始值为n,然后利用快速排序的思想,i枚举到偶数时break,j枚举到奇数时break,然后交换即可
Code:
#include<bits/stdc++.h>
using namespace std;
int n,arr[25];
void ruler_get()
{
int i = 1;
int j = n;
while (i < j)
{
while(i<j && arr[i]%2 == 1)i++;
while(i<j && arr[j]%2 == 0)j--;
if(i < j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
int main()
{
cin >> n;
for(int i = 1;i <= n; ++i)cin>>arr[i];
ruler_get();
for(int i = 1;i <= n; ++i)cout<<arr[i]<<" ";
}
求和
题目链接:http://47.96.116.66/problem.php?cid=1807&pid=1
解题思路:
题意:求一个严格单调递增的数列中两个数的和等于m的下标,要求复杂度为O(n)
这题乍一看,似乎只能暴力了QAQ。
但是在题目中,包含了一个十分重要的条件:严格单调递增的序列
这就成了尺取法的基础条件
我们可以定义两个指针i和j,初始值为0和n-1,如果arr[i]+arr[j]>sum,说明arr[j]太大了,所以j--,如果arr[i]+arr[j]<sum,,说明arr[i]太小了,所以i++,当正好=sum,输出即可
#include<iostream>
#include<algorithm>
using namespace std;
int arr[200005];
int ans = 0;
void getsum(int arr[],int n,int sum)
{
for(int i=0,j=n-1;i<j;)
{
if(arr[i]+arr[j] == sum)
{
int a = arr[i];
int b = arr[j];
cout << i << " " << j << endl;
i++;
j--;
ans++;
}
else if(arr[i]+arr[j] <sum)i++;
else j--;
}
}
int main()
{
int n;
cin>>n;
int sum;
cin>>sum;
for(int i = 0;i<n;i++)cin>>arr[i];
getsum(arr,n,sum);
cout<<ans<<endl;
return 0;
}
有序数组去重合并
题目链接:http://47.96.116.66/problem.php?cid=1807&pid=2
解题思路:
题意:两个有序数组合并且去重合成新数组
注意:合并且去重!(不知道有木有像我一样我了去重的人QAQ)
这题我们同样可以使用尺取法
定义三个指针(注意:是3个)a1,b1,sum,初始值分别为m-1(即1号数组末尾),n-1(即2号数组末尾),n+m-1(即合并数组末尾,这里为了偷懒,不新开数组了),然后在循环时分三种情况讨论:
1.a[a1]>b[b1] 此时应该将a[a1]填到a[sum]里去,a1--
2.a[a1]<b[b1] 此时应该将b[b1]填到a[sum]里去,b1--
3.a[a1]=b[b1] 即重复,此时将a[a1](或b[b1])填入a[sum],a1--,b1--
4.sum--
最后别忘了b数组还可能有剩余元素,依次填入a[sum]即可
#include <bits/stdc++.h>
using namespace std;
int a[400005],b[200005],m,n;
void Merge_sort()
{
int sum = m+n-1;
int a1 = m-1;
int b1 = n-1;
while(a1 >= 0 && b1 >= 0)
{
if(a[a1] > b[b1])
{
a[sum] = a[a1];
a1--;
}
else if(a[a1]<b[b1])
{
a[sum] = b[b1];
b1--;
}
else if(a[a1]==b[b1])
{
a[sum] = b[b1];
b1--;
a1--;
}
sum--;
}
while(b1 >= 0)
{
a[sum]=b[b1];
b1--;
sum--;
}
}
int main()
{
cin >> m >> n;
for(int i = 0; i < m; i++)cin >> a[i];
for(int i = 0; i < n; i++)cin >> b[i];
Merge_sort();
for(int i = 2; i < m+n; i++)cout << a[i] << " ";
}
有序数组平方
题目链接:http://47.96.116.66/problem.php?cid=1807&pid=3
解题思路:
题意:给你一个有序整数数组,数组中的数可以是正数、负数、零,请实现一个函数,这个函数返回一个整数:返回这个数组所有数的平方值中有多少种不同的取值。
使用双指针,从两边向中间扫描。将绝对值大的数字删掉,计数即可,并记录刚才删除的数值的绝对值,以免出现多次相同的数据,重复计数的问题。
详情见代码
#include <bits/stdc++.h>
using namespace std;
int ver[200005],n;
int Merge_sort(int ver[])
{
if(n < 2)return n;
int i = 0;//定义初始值
int j = n - 1;
int pre = abs(ver[0]);
int num = 0;
while(i<=j)
{
if(abs(ver[i]) > abs(ver[j]))//删除绝对值大的数
{
if(pre != abs(ver[i]))//如果木有删过
{
num++;//种类加1
pre = abs(ver[i]);//记录数值
}
i++;//向中间推进
}
else//同上
{
if(pre != abs(ver[j]))
{
num++;
pre = abs(ver[j]);
}
j--;
}
}
return num;
}
int main()
{
cin>>n;
for(int i = 0;i < n;++i)cin>>ver[i];
int num = Merge_sort(ver);
cout<<num<<endl;
return 0;
}
不重复连续区间
题目链接:http://47.96.116.66/problem.php?cid=1807&pid=4
解题思路:
题意:给定一个长度为n的整数序列,请找出最长的不包含重复数字的连续区间,输出它的长度。
定义两个数组:
1.读取数组s
2.判重数组vis
存在一个性质:
不重复序列的子序列也一定是不重复序列,所以可以数组遍历右指针,如果出现重复数,左端点回退
#include <bits/stdc++.h>
using namespace std;
const int N = 10000005;
int s[N],vis[N];
int main()
{
int n;
int maxx=0;
cin >> n;
for (int i = 0; i < n; i++)cin >> s[i];
for (int i = 0,j = 0; i < n; i++)
{
vis[s[i]]++;
while(vis[s[i]] > 1)vis[s[j++]]--;
maxx = max(maxx, i - j + 1);
}
cout << maxx << endl;
}
子串
题目链接:http://47.96.116.66/problem.php?cid=1807&pid=5
解题思路:
题意:一个S的子串T是合法的,当且仅当T中包含了所有的小写字母。问子串最短长度。
跟上题思路有点像:数组遍历右指针,如果出现重复数,左端点回退
当一个字母未出现过时,num++,当num=26时,输出区间
#include<bits/stdc++.h>
#define inf 1e9
using namespace std;
int vis[260000];
int main()
{
string s;
cin>>s;
int len=s.length();
int ans=inf,l=0,r=0,num=0;
for(;r<len;r++)
{
if(!vis[s[r]])num++;
vis[s[r]]++;
while(vis[s[l]]>1)
{
vis[s[l]]--;
l++;
}
if(num==26)ans=min(ans,r-l+1);
}
cout<<ans<<endl;
}
区间和
题目链接:http://47.96.116.66/problem.php?cid=1807&pid=6
解题思路:
题意:给定一个数组和一个数s,在这个数组中找一个区间,使得这个区间之和等于s。分三种情况考虑,sum>s,sum<s,sum=s
1.sum-a[i] i++
2.sum+a[j] j++
3.sum-a[i] i++ 输出i和j
#include <bits/stdc++.h>
using namespace std;
int ver[200005],n,s;
int fun(int ver[],int n,int s)
{
int i=0,j=0,sum=ver[0];
int ret = 0;
while(i<=j&&j<n)
{
if(sum>s)
{
sum-=ver[i];
i++;
}
else if(sum<s)
{
j++;
sum+=ver[j];
}
else if(sum==s)
{
printf("%d %d\n",i,j);
sum-=ver[i];
i++;
ret++;
}
}
return ret;
}
int main()
{
cin>>n>>s;
for(int i=0;i<n;i++)cin>>ver[i];
int ans=fun(ver,n,s);
cout<<ans<<endl;
return 0;
}
子区间和
题目链接:http://47.96.116.66/problem.php?cid=1807&pid=7
解题思路:
题意:给定长度为n的数列整数a0,a1,a2,a3 ..... an-1以及整数S。求出总和不小于S的连续子序列的长度的最小值。如果解不存在,则输出0。
分为三步
1.向右移动j变量,使a[i] 到 a[j] 的元素之和 大于等于S,注意 根据模板写的循环中,j的值需要-1才为该序列中最后数的下标。
如果加到最后j=n时依旧小于S,则不存在。
2.向右移动i变量,使a[i]到a[j]的元素之和小于S,加上在这之前的元素,即为以j为右端点的不小于S的连续子序列的长度的最小值
3.使用第2步算出的小于S的数,继续重复第一步右移j,直至出现第一步中不存在的情况,或j遍历完成。
#include <bits/stdc++.h>
using namespace std;
int n,m;
int a[200005];
int l,r;
int findSumK(int a[],int n,int m)
{
int i,j;
i = 0;
j = 0;
int ans = INT_MAX;
int sum = 0;
while(j < n)
{
while(sum < m && j < n)
{
sum += a[j];
j++;
}
if(sum < m) break;
while(sum >= m)
{
sum -= a[i];
i++;
}
if(j - i + 1 < ans)
{
ans = j - i + 1;
l = i - 1;
r = j - 1;
}
}
return ans;
}
int main()
{
cin>>n>>m;
for(int i = 0;i < n;i++)cin>>a[i];
if(findSumK(a,n,m)==INT_MAX)cout<<0<<endl;
else
{
printf("%d\n",findSumK(a,n,m));
cout<<l<<" "<<r;
}
return 0;
}