基本知识
- 定义: 反复推进区间的开头和末尾,来求满足条件的最小区间,最大区间的方法被称为尺取法。
- 用法: 故名思义,尺取法的有两个端点,一个代表头 i ,一个代表尾 j 。与二分法很相像,二分法是求满足条件的最小值最大值,而尺取法是求满足条件的最小区间最大区间。其题目通常要满足:连续(在一个区间上),单调(区间越大越容易满足条件或者越不容易满足条件)。
- 思路: 与二分法做对比
- 二分法: 枚举左端点,二分找到满足条件的最小或最大右端点,更新答案,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
- 尺取法: 枚举左端点,单调向右移动右端点直到刚好满足要求或刚好不满足要求,由于左右端点均递增,时间复杂度 O ( n ) O(n) O(n)
模板
模板1(单调的找到符合条件为止)
int main(){
int j=0;//右端点
for(int i=1;i<=n;i++){
while(没满足条件&&j<n)区间右端点右移,并计算影响
if(没满足条件)break;
计算答案
去掉左端点右移的影响
}
}
例题汇总
枚举左端点,尺取右端点
例题1:和大于等于 S 的最小区间
- 题目描述: 有一个 n 个数的数字 ai,求长度最小的一个区间,使得该区间的和 > = S >=S >=S。其中 a i > = 0 ai>=0 ai>=0。
- 问题分析: 尺取模板题
int j=0,ans=1e9,sum=0;
for(int i=1;i<=n;i++) {
while(j<n&&sum<s)sum+=a[++j];
if(sum<s)break;
ans=min(ans,j-i+1);
sum-=a[i];
}
例题2:能覆盖所有值的最小区间
- 题目描述: 一共有 m 个知识点。一个书有 n 页纸,每页纸都有一个知识点 ai。求最少需要阅读连续的多少页纸,使得所有知识点均覆盖。
- 问题分析: 来个桶对知识点计数,桶从0到1表示知识点数量+1,桶从1到0表示知识点数量-1。
int j=0,ans=1e9,num=0;
for(int i=1;i<=n;i++){
while(j<n&&num<m){
count[a[++j]]++;
if(count[a[j]]==1)num++;
}
if(num<m)break;
ans=min(ans,j-i+1);
count[a[i]]--;
if(count[a[i]]==0)num--;
}
例题3:通过价值排序,使得尺取过程具有单调性
- 题目描述: x 轴上有 1 − m 1-m 1−m ,有 n 个线段,每条线段从 l i l_i li 到 r i r_i ri ,价值为 w i w_i wi 。你可以选择若干条线段,当你选择了一条线段之后,你可以在这条线段上随意的移动,而此时选择的花费为:选择的若干条线段的最大价值与最小价值之差。问最少要多少的花费,使得可以从 1 走到 m 。
- 问题分析: 最大值与最小值差的最小值 。我们对线段按价值排个序,即可转化为最小区间问题。很显然,选择的线段数量越多,条件越任意满足,而价值又单调,那么可以直接尺取了。如何判断条件是否满足呢?线段树维护区间加,然后区间最小值即可判断出是否覆盖了 1 − m 1-m 1−m 的所有点。
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)scanf("%d%d%d",&a[i].l,&a[i].r,&a[i].w);
sort(a+1,a+n+1,cmp);
int j=0;
for(int i=1;i<=n;i++){
while(j<n&&tr[1]==0){
end++;
update(1,1,m,a[j].l+1,a[j].r,1);
}
if(tr[1]==0)break;
ans=min(ans,a[j].w-a[i].w);
update(1,1,m,a[i].l+1,a[i].r,-1);
}
cout<<ans;
}
例题4:通过对前缀和排序,使得尺取过程具有单调性
- 题目描述: N个数字组成的数列,T个询问,每次询问给定一个 k,找出数列中一段和的绝对值与 k 相差最小的,输出和的绝对值以及起点和终点。
- 思路: 一般尺取法只适用单调数列(一般和值递增),为了构造这样的性质:我们采用对前缀和从小到大排序的思路,这样枚举前缀和就可以枚举出区间,而区间越大,差值越大,符合单调性。
int main() {
int n,T,k;
cin>>n>>T>>k;
for(int i=1; i<=n; i++)scanf("%d",&a[i]);
for(int i=1; i<=n; i++) {
p[i].v=p[i-1].v+a[i];
p[i].id=i;
}
sort(p+1,p+n+1,cmp);
int j=0,sum=0,ansl,ansr;
for(int i=0; i<=n; i++) {
while(sum<t&&j<n) {
sum=p[++j].v-p[i].v;
if(minn>abs(sum-k)) {
minn=abs(sum-k);
ans=sum;
ansl=p[i].id;
ansr=p[j].id;
}
}
if(sum<t)break;
}
if(ansl>ansr)swap(ansl,ansr);
cout<<ans<<" "<<ansl+1<<" "<<ansr<<'\n';
}
例题5:gcd>1 的最大区间
- 题目描述: n 个数,求 g c d > 1 gcd>1 gcd>1 的区间的最大长度为多少。
- 问题分析: 很显然符合单调性,即区间越小越容易满足条件,区间越大越不容易满足条件。那么就用模板3,st表维护区间gcd即可。时间复杂度O(nlogn)
int main() {
int j=1,ans=0;
for(int i=1; i<=n; i++) {
while(j<=n&&query(i,j)>1)j++;
if(j>n)break;
ans=max(ans,j-i);
}
}
例题6:比 mid 大的数超过 k 个的区间个数
- 题目描述: n 个数 a i a_i ai ,求比 mid 大的数超过 k 个的区间个数
- 问题分析: 显然区间越大,比 mid 大的数超过 k 个的条件更容易满足。对于左边界 i ,当边界尺取到 j 不满足题意时,左边界在 i , 右边界在 [ j , n ] [j,n] [j,n] 的所有区间都满足条件了。统计即可。
int main(){
int j=0,sum=0.ans=0;
for(int i=1;i<=n;i++){
while(sum<k&&j<n){
j++;
if(a[j]>mid)sum++;
}
if(sum<k)break;
ans+=n-j+1;
}
}
例题7:排序形成尺取
- 问题分析: 给定 n 个人的成绩 a i , b i a_i,b_i ai,bi,表示第 i i i 个人可能考到 a i a_i ai,也可能考到 b i b_i bi 。给定 p p p,定义及格:假设 n n n 个人中最高分为 x x x,则比 x × p % x\times p\% x×p% 高的人均及格。求 n 个人中最多有多少个人及格。
- 问题分析: 将 n 个人的两个成绩,放在一起,从小到大排序。枚举右端点表示最高分,尺取左端点推进到第一个 > = x × p % >=x\times p\% >=x×p% 的位置。则之间的人均有可能及格。判断一下合法性,再更新一下答案即可。合法性判断:最高分一定要比所有人最低分的最大值大。
struct node{
int v,id;
}p[N];
int cmp(node x,node y){
return x.v<y.v;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>p[++cnt].v;
p[cnt].id=2*i-2;
cin>>p[++cnt].v;
p[cnt].id=2*i-1;
int t=min(p[cnt-1].v,p[cnt].v);
down=max(down,t);
}
sort(p+1,p+cnt+1,cmp);
int i=1;//0,1 2n-2,2n-1
for(int j=1;j<=cnt;j++){
if(vis[p[j].id^1]==0)sum++;
vis[p[j].id]=1;
while(p[i].v<p[j].v*1.0*p/100&&i<j){
if(vis[p[i].id^1]==0)sum--;
vis[p[i].id]=0;
i++;
}
if(p[j].v<down)continue;
else ans=max(ans,sum);
}
cout<<ans;
}
双指针从左右端点一起往内移动
例题1:在升序排列的整数数组中,找到两个数和为 x x x
- 题目描述: 给定 n 个数,其已经按照升序排序完成。给定 x ,请你在数组中找到两个数满足相加之和等于 x 。
- 问题分析: 双指针 i , j i,j i,j 最开始分别指向 1 , n 1,n 1,n。
- 若 a i + a j > x a_i+a_j>x ai+aj>x,则 j − − j-- j−−
- 若 a i + a j < x a_i+a_j<x ai+aj<x,则 i + + i++ i++
- 若 a i = a j a_i=a_j ai=aj,则已找到