尺取法
部分内容来源于以下博文 侵删
http://blog.csdn.net/consciousman/article/details/52348439
据说思想来源于尺蠖的运动
https://baike.baidu.com/item/%E5%B0%BA%E8%A0%96/3663106?fr=aladdin//虫类百科,慎点
算法流程
尺取法:顾名思义,像尺子一样取一段,借用挑战书上面的话说,尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。之所以需要掌握这个技巧,是因为尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的
时候,所以尺取法是一种高效的枚举区间的方法,一般用于求取有一定限制的区间个数或最短的区间等等。当然任何技巧都存在其不足的地方,有些情况下尺取法不可行,无法得出正确答案。
使用尺取法时应清楚以下四点:
1、 什么情况下能使用尺取法? 2、何时推进区间的端点? 3、如何推进区间的端点? 3、何时结束区间的枚举?
1.尺取法通常适用于选取区间有一定规律,或者说所选取的区间有一定的变化趋势的情况,通俗地说,在对所选取区间进行判断之后,我们可以明确如何进一步有方向地推进区间端点以求解满足条件的区间,
如果已经判断了目前所选取的区间,但却无法确定所要求解的区间如何进一步得到根据其端点得到,那么尺取法便是不可行的。
2.3.首先,明确题目所需要求解的量之后,区间左右端点一般从最整个数组的起点开始,之后判断区间是否符合条件在根据实际情况变化区间的端点求解答案。
4.当右端点已经到达数组末尾,且左端点已经达到最优时,结束区间的枚举
根据区间的特征交替推进左右端点求解问题,其高效的原因在于避免了大量的无效枚举,其区间枚举都是根据区间特征有方向的枚举,如果胡乱使用尺取法的话会使得枚举量减少,因而很大可能会错误,所以关键的一步是进行问题的分析!
eg1
题目
http://poj.org/problem?id=3320
一本书有P页,每一页都一个知识点,求去最少的连续页数覆盖所有的知识点。
题解
首先,为了避免知识点编号过大,用map或者二分+离散化的方式重新编号
如果一个区间的子区间满足条件,那么在区间推进到该处时,右端点会固定,左端点会向右移动到其子区间,且其子区间会是更短的
整个区间是可行的,
左右指针lr初始化为1
当前区间若可行,则需在保证可行的前提下,缩小区间范围,将l指针右移
当前区间若不可行,则需要扩大区间范围,将r指针右移
当r已到末尾且l已经最优时,结束枚举
代码
#include<iostream>
#include<cstdio>
#include<map>
using namespace std;
const int N=1005000;
int a[N],cnt[N];
map<int,int> bo;
int l,r,ans,n,p,sum,x;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) {
scanf("%d",&x);
if(!bo[x]) bo[x]=++p;
a[i]=bo[x];
}
ans=n;
l=1,r=1;
cnt[a[1]]++;
sum++;
while(l<=n){
while(sum<p&&r<n){
r++;
if(!cnt[a[r]]){
sum++;
}
cnt[a[r]]++;
}
if(r==n&&sum<p) break;
while(sum>=p&&l<=r){
ans=min(ans,r-l+1);
cnt[a[l]]--;
if(!cnt[a[l]]) sum--;
l++;
}
}
printf("%d",ans);
return 0;
}
eg2
题目
http://poj.org/problem?id=3061
给定一个序列,找出最短的子序列长度,使得其和大于或等于S。
题解
首先,序列都是正数,如果一个区间其和大于等于S了,那么不需要在向后推进右端点了,因为其和也肯定大于等于S但长度更长,所以,当区间和小于S时右端点向右移动,和大于等于S时,左端点向右移动以进一步找到最短的区间,如果右端点移动到区间末尾其和还不大于等于S,结束区间的枚举。
一开始没有处理整个序列相加也无法满足条件输出0的情况,WA了
题解说要处理前缀和,实际上不必要,在移动指针时顺便维护即可
代码
#include<iostream>
#include<cstdio>
#define min(a,b) ((a)<(b)?(a):(b))
using namespace std;
const int N=100000+500;
const int inf=1e9;
long long sum,ans,n,s,t;
long long a[N];
void work(){
int l=1,r=1;
sum=a[1];
while(l<=n){
while(sum<s&&r<n){
sum+=a[++r];
}
if(sum<s) return ;
while(sum>=s&&l<=r){
sum-=a[l++];
}
ans=min(ans,r-l+2);//此时[l-1,r]是满足条件的最小区间
}
return ;
}
int main(){
scanf("%lld",&t);
while(t--){
scanf("%lld%lld",&n,&s);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
ans=inf;
work();
if(ans==inf) ans=0;
printf("%lld\n",ans);
}
return 0;
}
eg3
http://poj.org/problem?id=2566
给定一个数组和一个值t,求一个子区间使得其和的绝对值与t的差值最小,如果存在多个,任意解都可行。
不会做,抄题解
http://www.cnblogs.com/smilesundream/p/5129758.html
原数组有正有负,区间的左移右移没有单调性,需要进行转化,找到单调性,满足尺取法
但是原数组不能改变,考虑做原数组的前缀和,将前缀和数组按照升序排序,在前缀和数组上做尺取,要把< 0,0 >也加入排序,以便包含区间1~x
尺取的值是左右区间的差,
如果这个差大于t,则左移
小于t,右移
注意区间长度不能为1,因为原题中要求区间长度不能为0,
前缀和区间长度为1的话,即原数组中长度为0
一开始给minn赋值为1e9,太小,【后来给int型的minn赋成2647483647,搞成了负数这种事我才不会告诉你qwq】,WA了好几次。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=100000+100;
int ll,rr,minn,ans,T,a;
int n,k;
struct node{
int id;
long long s;
}sum[N];
bool cmp(node a,node b){
return a.s<b.s;
}
void work(int t){
int l=0,r=1;
ll=0,rr=0,minn=2147483640;
while(l<=n&&r<=n&&minn!=0){
int tmp=sum[r].s-sum[l].s;
if(abs(tmp-t)<minn){
minn=abs(tmp-t);
ll=sum[l].id;
rr=sum[r].id;
ans=tmp;
}
if(tmp>t) l++;
else if(tmp<t) r++;
else break;
if(l==r) r++;
}
if(ll>rr) swap(ll,rr);
printf("%d %d %d\n",ans,ll+1,rr);
return ;
}
int main(){
while(scanf("%d%d",&n,&k)!=EOF){
if(n==0&&k==0) return 0;
sum[0].id=0,sum[0].s=0;
for(int i=1;i<=n;i++){
scanf("%d",&a);
sum[i].s=sum[i-1].s+a;
sum[i].id=i;
}
sort(sum,sum+n+1,cmp);
for(int i=1;i<=k;i++){
scanf("%d",&T);
work(T);
}
}
return 0;
}
eg4
poj2739&poj2100