尺取法:顾名思义,像尺子一样取一段,借用挑战书上面的话说,尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。之所以需要掌握这个技巧,是因为尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的
时候,所以尺取法是一种高效的枚举区间的方法,一般用于求取有一定限制的区间个数或最短的区间等等。当然任何技巧都存在其不足的地方,有些情况下尺取法不可行,无法得出正确答案。
使用尺取法时应清楚以下四点:
1、 什么情况下能使用尺取法? 2、何时推进区间的端点? 3、如何推进区间的端点? 3、何时结束区间的枚举?
尺取法通常适用于选取区间有一定规律,或者说所选取的区间有一定的变化趋势的情况,通俗地说,在对所选取区间进行判断之后,我们可以明确如何进一步有方向地推进区间端点以求解满足条件的区间,如果已经判断了目前所选取的区间,但却无法确定所要求解的区间如何进一步
得到根据其端点得到,那么尺取法便是不可行的。首先,明确题目所需要求解的量之后,区间左右端点一般从最整个数组的起点开始,之后判断区间是否符合条件在根据实际情况变化区间的端点求解答案。
简单来说,尺取法就是对一个可变化的连续区间里面搞东西;
下面给出列子来说明一切好吧;
(1)POJ3061
题意:为了准备考试,Jessica开始读一本很厚的课本。要想通过考试,必须把课本中所有的知识点都掌握。这本书共有P(1<=P<=10^6)页,第i页恰好有一个知识点Ai(每个知识点都有一个整数编号)。全书中同一个知识点可能会被多次提到,所以她希望通过阅读其中连续的一些页把所有的知识点都覆盖到。给定每页写到的知识点,请求出要阅读的最少页数。
分析:尺取法。我们可以先把种类数算出来(利用set集合),然后,假设在区间[s,t]已经覆盖了所有的知识点,我们可以从s开始,把s取走后,那么页s上的知识点出现次数就要减一,如果此时这个知识点的出现次数为0了,那么,在同一个知识点出现之前,不停地将区间末尾t向后推进即可。
Sample Input
5
1 8 8 8 1
Sample Output
2
2
3
AC代码:
#include<iostream> #include<cstdio> #include<cstring> #include<set> #include<map> #include<algorithm> using namespace std; const int MAXN = 1000000 + 1000; int a[MAXN]; int p; int main() { while (scanf("%d", &p) != EOF) { for (int i = 0; i < p; i++) scanf("%d", &a[i]); set<int>all;///去掉重复的 for (int i = 0; i < p; i++) all.insert(a[i]); int n = all.size(); //利用尺取法来求解 int s = 0, t = 0, num = 0; map<int, int>count; int ret = p; for (;;) { while (t < p && num < n) { if (count[a[t++]]++ == 0) { num++; } } if (num < n) break; ret = min(ret, t - s); if (--count[a[s++]] == 0) num--; } printf("%d ", ret); } }
(2)POJ3320
Sample Input
2 10 15 5 1 3 5 10 7 4 9 2 8 5 11 1 2 3 4 5
Sample Output
2 3
题意:给定长度为n的数列整数A0,A1,A2……An-1以及整数S。求出总和不小于S的连续子序列的长度的最小值。如果解不存在,则输出0。
分析:尺取法。只要sum<S,则一直往后加Ai;当sum>=S时,一直去掉前面已加的项。
AC代码:
#include<stdio.h> #include<algorithm> using namespace std; int a[101000]; int main() { int t,n,s,sum,p,st; scanf("%d",&p); while(p--) { scanf("%d%d",&n,&s); for(int i=0 ; i<n ;i++) { scanf("%d",&a[i]); } int res=n+1; sum=st=t=0; for( ; ; ) { while(t<n&&sum<s) { sum+=a[t++]; } if(sum<s) break; res=min(res,t-st); sum-=a[st++]; } if(res>n) res=0; printf("%d ",res); } }
(3)POJ2566
题目大意】
给出一个整数列,求一段子序列之和最接近所给出的t。输出该段子序列之和及左右端点。
【思路】
……前缀和比较神奇的想法。一般来说,我们必须要保证数列单调性,才能使用尺取法。
预处理出前i个数的前缀和,和编号i一起放入pair中,然而根据前缀和大小进行排序。由于abs(sum[i]-sum[j])=abs(sum[j]-sum[i]),可以忽视数列前缀和的前后关系。此时,sum[r]-sum[l]有单调性。
因此我们可以先比较当前sum[r]-sum[l]与t的差,并更新答案。
如果当前sum[r]-sum[l]<t,说明和还可以更大,r++。
同理,如果sum[r]-sum[l]>t,说明和还可以更小,l++。
如果sum[r]-sum[l]=t,必定是最小答案。
【注意点】
由于序列不能为空,即l<>r,如果l=r则r++。
我们更新答案的时候左右区间端点为乱序,输出的时候调整一下。
就OK了!
AC代码:
#include<stdio.h> #include<algorithm> #include<cmath> using namespace std; #define N 110000 #define INF 0x3f3f3f3f struct no { int sum; int id; }a[N]; bool cmp(no a,no b) { return a.sum<b.sum; } int main() { int n,k,x,t; while(scanf("%d%d",&n,&k)!=EOF) { if(n==0&&k==0) break; a[0].sum=0;a[0].id=0; for(int i=1 ; i<=n ; i++) { scanf("%d",&x); a[i].sum=a[i-1].sum+x; a[i].id=i; } sort(a,a+n+1,cmp); while(k--) { scanf("%d",&t); int en=1,st=0,mix=INF,men,mst,msum; while(en<=n&&st<=n) { int temp=abs(a[en].sum-a[st].sum); if(abs(temp-t)<mix) { mix=abs(temp-t); msum=temp; mst=a[st].id; men=a[en].id; } if(temp<t) en++; else if(temp>t) st++; else break; if(en==st) en++; } if(men<mst) swap(men,mst); printf("%d %d %d ",msum,mst+1,men); } } return 0; }
(4)POJ2100
题意:
分解整数,将一个整数分解成多个数的平方和;
理解:
开始我以为可以暴力枚举,然后发现n是1e14次方;
过不了,然后翻书发现是尺取法;
就用尺取法写了;
但是报了我一个超空间...
随后算了下空间...果然用了1e7个longlong的字节;
然后想啊....
最后受大神影响,发现可以不用数组...
AC代码:
#include<stdio.h> #define ll long long #define MAX 2000 ll num[MAX],left[MAX],right[MAX]; int main() { ll n; scanf("%lld",&n); ll ans=0; ll st=1,en=0,cnt=0; ll sum=0; while(1) { while(sum<n) { en++; sum+=en*en; } if(en*en>n) break; if(sum==n) { num[cnt]=en-st+1; left[cnt]=st; right[cnt]=en; cnt++; } sum-=(st*st); st++; } printf("%d ",cnt); for(ll i=0 ; i<cnt ;i++) { printf("%d ",num[i]); for(ll j=left[i];j<right[i];++j) printf("%d ",j); printf("%d ",right[i]); } }