一、尺取法

尺取法:顾名思义,像尺子一样取一段,借用挑战书上面的话说,尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。之所以需要掌握这个技巧,是因为尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的时候,所以尺取法是一种高效的枚举区间的方法,一般用于求取有一定限制的区间个数或最短的区间等等。当然任何技巧都存在其不足的地方,有些情况下尺取法不可行,无法得出正确答案。

使用尺取法时应清楚以下四点:

1、什么情况下能使用尺取法?
2、何时推进区间的端点?
3、如何推进区间的端点?
4、何时结束区间的枚举?

尺取法通常适用于选取区间有一定规律,或者说所选取的区间有一定的变化趋势的情况,通俗地说,在对所选取区间进行判断之后,我们可以明确如何进一步有方向地推进区间端点以求解满足条件的区间,如果已经判断了目前所选取的区间,但却无法确定所要求解的区间如何进一步得到根据其端点得到,那么尺取法便是不可行的。首先,明确题目所需要求解的量之后,区间左右端点一般从最整个数组的起点开始,之后判断区间是否符合条件在根据实际情况变化区间的端点求解答案。

POJ 3061

给定长度为n的数列整数a0,a1,…an-1以及证书S。求出总和不小于S的连续子序列的长度的最小值。如果解不存在在,则输出0.

已知:

10<n<10^5
0<ai≤10^4
S<10^8
sampleinput
n= 10
S= 15
a= {5,1,3,5,10,7,4,9,2,8}
sampleoutput
2(5 + 10)
我们设以as开始总和最初大于S时的连续子序列as+...+at−1,这时
as+1+...+at−2<as+...+at−2<S
所以从as+1开始总和最初超过S的连续子序列如果是as+1+...+at′−1的话,则必然有t≤t′。
用下面的图来解释比较清晰:
NOIP复赛复习(十七)尺取法与折半枚举-少儿编程教育网

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#define sf scanf
#define pf printf
using name space std;
const int Maxn = 100010;
int T,n,s;
int sum[Maxn];
int main()
{
int a;
sf("%d",&T);
while(T--)
{
int tail = -1,head = -1;
sf("%d%d",&n,&s);
for(int i = 0;i < n;i ++)
{
sf("%d",&a);
if(i == 0) sum[i] = a;
else sum[i] = sum[i - 1] + a;
if(tail == -1 and sum[i] >= s)
tail = i;
}
if(tail == -1)
{
pf("0\n");
continue;
}
int Min = n;
while(head < tail)
{
if(sum[tail] - sum[head + 1] >=s)
{
head ++;
Min = min(Min,tail - head);
}
else if(tail < n - 1) tail++;
else
break;
}
pf("%d\n",Min);
}
return 0;
}

POJ 3320

题意:一本书有P页,每一页都一个知识点,求去最少的连续页数覆盖所有的知识点。

分析:和上面的题一样的思路,如果一个区间的子区间满足条件,那么在区间推进到该处时,右端点会固定,左端点会向右移动到其子区间,且其子区间会是更短的,只是需要存储所选取的区间的知识点的数量,那么使用map进行映射以快速判断是否所选取的页数是否覆盖了所有的知识点。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<set>
#include<map>
#define MAX 1000010
#define LL long long
#define INF 0x3f3f3f3f
using name space std;
int a[MAX];
map<int, int> cnt;
set<int> t;
int p, ans = INF, st, en, sum;
int main()
{
scanf("%d", &p);
for (int i = 0; i < p; i++)scanf("%d", a+i), t.insert(a[i]);
int num = t.size();
while (1){
while (en<p &&sum<num)
if (cnt[a[en++]]++ == 0)sum++;
if (sum < num) break;
ans = min(ans, en-st);
if (--cnt[a[st++]] == 0) sum--;
}
printf("%d\n", ans);
return 0;
}

POJ 2566

题意:给定一个数组和一个值t,求一个子区间使得其和的绝对值与t的差值最小,如果存在多个,任意解都可行。

分析:明显,借用第一题的思路,既然要找到一个子区间使得和最接近t的话,那么不断地找比当前区间的和更大的区间,如果区间和已经大于等于t了,那么不需要在去找更大的区间了,因为其和与t的差值更大,然后区间左端点向右移动推进即可。所以,首先根据计算出所有的区间和,排序之后按照上面的思路求解即可。

#include<cstdio>
#include<algorithm>
#include<cstring>
#define INF 0x3f3f3f3f
#define LL long long
#define MAX 100010
using name space std;
typedef pair<LL, int> p;
LL a[MAX], t, ans, tmp, b;
int n, k, l, u, st, en;
p sum[MAX];
LL myabs(LL x)
{
return x>=0? x:-x;
}
int main()
{
while (scanf("%d %d", &n,&k), n+k){
sum[0] = p(0, 0);
for (int i = 1; i <= n; i++){
scanf("%I64d", a+i);
sum[i] = p(sum[i-1].first+a[i],i);
}
sort(sum, sum+1+n);
while (k--){
scanf("%I64d",&t);
tmp = INF; st = 0, en = 1;
while(en <= n){
b =sum[en].first-sum[st].first;
if(myabs(t-b) < tmp){
tmp = myabs(t-b);
ans = b;
l = sum[st].second; u =sum[en].second;
}
if(b > t) st++;
else if(b < t) en++;
else break;
if(st == en) en++;
}
if (u < l) swap(u, l);
printf("%I64d %d %d\n",ans, l+1, u);
}
}
return 0;
}

总结:尺取法的模型便是这样:根据区间的特征交替推进左右端点求解问题,其高效的原因在于避免了大量的无效枚举,其区间枚举都是根据区间特征有方向的枚举,如果胡乱使用尺取法的话会使得枚举量减少,因而很大可能会错误,所以关键的一步是进行问题的分析!

二、折半枚举

折半枚举不是一般的双向搜索,当问题的规模较大,无法枚举所有元素的组合,但能够枚举一半元素的组合,此时将问题拆成两半后分别枚举,再合并它们的结果这一方法往往非常有效。

POJ 2785

给定各有n个整数的四个数列A,B,C,D。从每个数列中各取一个数,使四个数的和为0.当一个数列中有多个相同数字时,把它们作为不同的数字看待。求出这样的组合的个数。

已知:

1≤n≤4000

|(数字的值)|≤228

sampleinput

n= 6

A= {-45,-41,-36,-36,26,-32}

B= {22,-27,53,30,-38,-54}

C= {42,56,-37,-75,-10,-6}

D= {-16,30,77,-46,62,45}

sampleoutput

5

如果全部枚举,则有n4种可能性。时间复杂度通不过。因此可以进行折半枚举,计算A,B之间的组合,共有n2种情况,同样的,C,D之间也有n2种情况。

在取出A,B组合中的一组组合(a + b)时,为了使和为0,去查找C,D组合中满足a + b+c+d = 0的组合(c+d),这个查找可以用二分查找来实现,因此最后的复杂度是O(n2logn)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<string>
#define sf scanf
#define pf printf
using name space std;
typedef long long LL;
const int Maxn = 4010;
int A[Maxn],B[Maxn],C[Maxn],D[Maxn];
int AB[Maxn * Maxn],CD[Maxn * Maxn];
int n;
LL solved(int val)
{
int l = 0, r = n * n - 1;
LL ans;
while(l <= r)
{
int mid = (l + r) / 2;
if(CD[mid] <= val)
l = mid + 1;
else
r = mid - 1;
}
ans = r;
l = 0, r = n * n - 1;
while(l <= r)
{
int mid = (l + r) / 2;
if(CD[mid] < val)
l = mid + 1;
else
r = mid - 1;
}
ans = ans - r;
return ans;
}
int main()
{
while(~sf("%d",&n))
{
for(int i = 0;i < n;i++)
sf("%d%d%d%d",&A[i],&B[i],&C[i],&D[i]);
int cnt = 0;
for(int i = 0;i < n;i ++)
for(int j = 0;j < n;j ++)
AB[cnt] = A[i] + B[j],CD[cnt++]= C[i] + D[j];
sort(CD,CD + cnt);
LL ans = 0;
for(int i = 0;i < cnt;i ++)
ans += solved(0 - AB[i]);
pf("%lld\n",ans);
}
return 0;
}

POJ 3977

题意:给你一个含n(n<=35)个数的数组,让你在数组中选出一个非空子集,使其元素和的绝对值最小,输出子集元素的个数以及元素和的绝对值,若两个子集元素和相等,输出元素个数小的那个。

思路:如果直接暴力枚举,复杂度O(2^n),n为35时会超时,故可以考虑折半枚举,利用二进制将和以及元素个数存在两个结构体数组中,先预判两个结构体是否满足题意,再将其中一个元素和取相反数后排序,因为总元素和越接近零越好,再二分查找即可,用lower_bound时考虑查找到的下标和他前一个下标,比较元素和以及元素个数,不断更新即可。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cmath>
using name space std;
struct Z{
long long int x;
int y;
bool operator < (const Z& b)const
{
if(x!=b.x)
return x < b.x;
return y<b.y;
}
}a[300005],b[300005];
long long  int c[40];
long long int abs1(long long int x){
if(x<0)
return -x;
return x;
}
int main(){
int n;
while((cin>>n)&&n!=0){
for(int i=0;i<300005;i++)
a[i].x=a[i].y=b[i].x=b[i].y=0;
long long int sum=1e17;
int ans=40;
for(int i=0;i<n;i++)
cin>>c[i];
int n1=n/2;
for(int i=0;i<1<<n1;i++){
for(int j=0;j<n1;j++){
if(i>>j&1&&(i!=0||j!=0)){
a[i-1].x+=c[j];
a[i-1].y++;
}
}
}
int n2=n-n1;
for(int i=0;i<(1<<n2);i++){
for(int j=0;j<n2;j++){
if(i>>j&1&&(i!=0||j!=0)){
b[i-1].x+=c[j+n1];
b[i-1].y++;
}
}
}
for(int i=0;i<(1<<n1)-1;i++){
if(abs1(a[i].x)<sum){
sum=abs1(a[i].x);
ans=a[i].y;
}
elseif(abs1(a[i].x)==sum&&a[i].y<ans){
ans=a[i].y;
sum=abs1(a[i].x);
}
}
for(inti=0;i<(1<<n1)-1;i++)
a[i].x=-a[i].x;
for(int i=0;i<(1<<n2)-1;i++){
if(abs1(b[i].x)<sum){
sum=abs1(b[i].x);
ans=b[i].y;
}
else if(abs1(b[i].x)==sum&&b[i].y<ans){
ans=b[i].y;
sum=abs1(b[i].x);
}
}
sort(a,a+(1<<n1)-1);
sort(b,b+(1<<n2)-1);
for(int i=0;i<(1<<n1)-1;i++){
int t=lower_bound(b,b+(1<<n2)-1,a[i])-b;
if(t>0){
if(abs1(b[t-1].x-a[i].x)<sum){
sum=abs1(b[t-1].x-a[i].x);
ans=b[t-1].y+a[i].y;
}
else if(abs1(b[t-1].x-a[i].x)==sum&&b[t-1].y+a[i].y<ans){
sum=abs1(b[t-1].x-a[i].x);
ans=b[t-1].y+a[i].y;
}
}
if(t<(1<<n2)-1){
if(abs1(b[t].x-a[i].x)<sum){
sum=abs1(b[t].x-a[i].x);
ans=b[t].y+a[i].y;
}
else if(abs1(b[t].x-a[i].x)==sum&&b[t].y+a[i].y<ans){
sum=abs1(b[t].x-a[i].x);
ans=b[t].y+a[i].y;
}
}
}
cout<<sum<<""<<ans<<endl;
}

*