文章目录
- 前言
- 算法描述
- 例题
- [poj 3061 Sequence](http://poj.org/problem?id=3061)
- [洛谷 p1638 逛画展](https://www.luogu.org/problemnew/show/P1638)
- [UVA 11572 Unique Snowflakes](https://vjudge.net/problem/UVA-11572)
- [Atcoder 4142 Xor Sum 2](https://abc098.contest.atcoder.jp/tasks/arc098_b?lang=en)
- [洛谷 p1102 A-B数对](https://www.luogu.org/problemnew/show/P1102)
- [Codeforces 1042D Petya and Array](http://codeforces.com/problemset/problem/1042/D)
- [Codeforces 47E Cannon](http://codeforces.com/problemset/problem/47/E)
- [Codeforces 939E Maximize!](http://codeforces.com/problemset/problem/939/E)
前言
前面的米娜桑把提高组,省选的算法讲了一遍又一遍,向我这种蒟蒻,该听不懂的还是听不懂.
所以我写了这篇博客来介绍一下尺取法,即使它只是一个普及组的简单算法也非常有意思.
update:此文为洛谷日报第73期. 链接:尺取法小结
算法描述
Codeforces中显示它的算法名称叫做"two pointers".
直译成中文的话叫双指针法.
怎么说呢…做到提高组之后,很多oier仅仅是觉得好像有这么一个两个指针从左到右扫一遍的算法存在,却不知道它的名字.(其实是因为大佬们根本没把它当个算法)
这个算法不是很难,却很有意思.
尺取法是一种比较基础的算法,一般用来解决具有单调性的区间问题.
当然,说到单调性,大家都会想到二分.
尺取法能做的题,有很大的概率也可以用二分解决,不过尺取和二分的复杂度在不同的题目中往往是不同的.
所以尺取法的题大概也是找到可二分性然后优化之.
我想不到类比尺取法的实际问题,所以我只能给了很多例题.
大家如果有的话可以告诉我,我将非常感谢.
维护两个指针
l
,
r
l,r
l,r,每次确定区间的左端点,让
r
r
r不断向右移动,直到满足条件停下,维护一下答案,直到
r
>
n
r>n
r>n或者其它情况(视题目而定).
而实际中很多算法都要用到尺取法来辅助或者优化计算,尤其是分治算法.
例题
poj 3061 Sequence
给出一个序列,求区间和大于或者等于S的最短区间长度.
先想暴力.枚举每一个区间起点
l
l
l,往右去找位置
r
r
r,直到
[
l
,
r
]
[l,r]
[l,r]的区间和大于或者等于
r
r
r为止.
这时我们注意到一个性质.给出的所有数都是正整数,即它们的前缀和是单调递增的.
因此可以运用二分优化暴力.
求出数列的前缀和
s
u
m
sum
sum,枚举
l
l
l,二分找出
s
u
m
[
r
]
−
s
u
m
[
l
−
1
]
sum[r]-sum[l-1]
sum[r]−sum[l−1]区间和刚好大于等于
S
S
S的最小
r
r
r.
能不能把时间复杂度再优化一下呢?答案当然是可以.
我们定义三个变量
l
,
r
,
n
o
w
l,r,now
l,r,now,表示现在取到的区间的左右两个端点,和
[
l
,
r
]
[l,r]
[l,r]的区间和.
每一次当
n
o
w
>
=
s
now>=s
now>=s,我们就让
r
r
r停止向右移动.
所以当
l
l
l不断增加的时候,
r
r
r端点不可能会往左移动,因此对于每一个
l
l
l,
r
r
r也是单调的.
证明如下:当
[
l
,
r
]
[l,r]
[l,r]的区间和小于
S
S
S时,
[
l
,
r
−
1
]
[l,r-1]
[l,r−1]就不可能大于或者等于
S
S
S,因为
a
[
r
]
a[r]
a[r]是个正数.
[
l
+
1
,
r
−
1
]
[l+1,r-1]
[l+1,r−1]就更不可能,所以当
l
l
l向右移动的时候,
r
r
r不可能向左移动.
代码就不难写出了.
for (int l=1,r=0,now=0;l<=n;){
while (now<S&&r<n) now+=a[++r]; //表示先把r+1,再执行now+=a[r].注意要使r<=n.
/*当now<S的时候,我们不断地向右延伸区间,直到now>=S*/
if (now<S) break;
/*这时候l已经过大了,r到n也满足不了条件,这时要退出循环,否则下面的ans会被错误更新.*/
ans=min(ans,r-l+1); // [l,r]区间的长度
now-=a[l++]; // 这里就是先执行now-=a[l],然后再执行l+=1.
}
洛谷 p1638 逛画展
求刚好拥有所有m种数字的最短区间.
这一次我们会发现随着
l
l
l的增加,
r
r
r也不可能会往左移动.
也就是说如果
[
l
,
r
]
[l,r]
[l,r]刚好有
m
m
m种画的时候
[
l
,
r
−
1
]
[l,r-1]
[l,r−1]不可能有
m
m
m种画,
[
l
+
1
,
r
−
1
]
[l+1,r-1]
[l+1,r−1]就更不可能.
开一个
c
n
t
cnt
cnt数组,存储
[
l
,
r
]
[l,r]
[l,r]区间内
1
→
m
1\to m
1→m每一种画出现了多少幅.
用
n
o
w
now
now存储
[
l
,
r
]
[l,r]
[l,r]区间内有多少种画.
那么我们可以这样维护.
for (int l=1,r=0,now=0;l<=n;){
while (now<m&&r<n){
r++;
if (!cnt[a[r]]) now++; // 如果a[r]还没有,这个区间就多了一种画.
cnt[a[r]]++;
}// 大括号内的东西简写为 now+=!cnt[a[++r]]++;
if (now<m) break;
ans=min(ans,r-l+1);
now-=!--cnt[a[l++]];
}
UVA 11572 Unique Snowflakes
求没有重复数字的最长区间.
暴力的话是对于每一个位置
l
l
l找在它右边离它最远不出现重复数字的位置
f
(
l
)
f(l)
f(l).
f
(
l
)
f(l)
f(l)单调不降,所以还是开
c
n
t
cnt
cnt数组维护区间
[
l
,
r
]
[l,r]
[l,r]内每一个数出现的次数,有一个数次数大于
1
1
1就把
l
l
l向右推,否则一路把
r
r
r向右开过去.
你说数据范围太大不能开数组?
m
a
p
map
map去重!
这样的话推动
[
l
,
r
]
[l,r]
[l,r]移动的复杂度就是
O
(
l
o
g
)
O(log)
O(log)的了.
map<int,int> cnt;
int main(){
int ans=1,l=1,r=2;
cnt.clear();
cnt[a[l]]++,cnt[a[r]]++;
for (;r<=n;){
while (cnt[a[r]]>1) cnt[a[l++]]--;
ans=max(ans,r>n?r-l:r-l+1);
cnt[a[++r]]++;
}
cout<<ans<<endl;
}
Atcoder 4142 Xor Sum 2
求区间[l,r]的对数,使得A[l] xor A[l+1] xor ... xor A[r] = A[l] + A[l+1] + ... +A[r].
当时我在ABC第98场的现场打这场比赛.一开始我没有头绪.
突然我在第30分钟的时候发现了性质用尺取法通过了此题.
注意到a xor b <= a + b
,并且等号只有在a and b = 0
的时候才能够取到.
那么
[
l
,
r
]
[l,r]
[l,r]所有数的异或值等于所有数的和,必须要每一个二进制位上该区间所有数加起来最多只有一个
1
1
1才行.
对于每一个
l
l
l算出
f
(
l
)
f(l)
f(l)为从第
l
l
l个数过去使得
[
l
,
r
]
[l,r]
[l,r]满足上面条件最远能够延伸到的位置
r
r
r.
我去看了题解,题解中说显然
f
(
l
)
f(l)
f(l)单调不降,所以可以用尺取法做.
虽然听起来非常牵强,但是确实显然如此.我试着证明一下.
由于
[
l
,
f
(
l
)
]
[l,f(l)]
[l,f(l)]区间是符合题目条件的,
[
l
+
1
,
f
(
l
)
]
[l+1,f(l)]
[l+1,f(l)]就更加符合条件(有几位上又少了几个
1
1
1),因此
f
(
l
+
1
)
f(l+1)
f(l+1)不可能比
f
(
l
)
f(l)
f(l)还小.
用
n
o
w
now
now表示当前
[
l
,
r
]
[l,r]
[l,r]区间的和.
每一次我们延伸
r
r
r的时候,判断
n
o
w
x
o
r
a
[
r
]
now\ xor \ a[r]
now xor a[r]和
n
o
w
+
a
[
r
]
now+a[r]
now+a[r]是否相等,如果相等就继续将
r
r
r向右移动,否则停止.
对于这一个
l
l
l,答案的个数就是
r
−
l
r-l
r−l,因为
l
→
r
−
1
l\to r-1
l→r−1中任意取一个位置
j
j
j,
[
l
,
j
]
[l,j]
[l,j]都是符合条件的.
/*
可以看到上面的条件是r<=n.
这个代码其实是枚举了r,尺取了l的位置,所以每一次将r++以后更新答案.
*/
for (int l=1,r=1,now=0;r<=n;){
for (;!(now&a[r])&&r<=n;){
/*now & a[r] = 0即now ^ a[r] = now + a[r]*/
now|=a[r++];
ans+=r-l;
}
now^=a[l++];
}
/*如果用上面说的方法代码是这样的.*/
for (int l=1,r=1,now=0;l<=n;){
for (;r<=n&&(now^a[r])==now+a[r];) now^=a[r++];
ans+=r-l,now^=a[l++];
}
洛谷 p1102 A-B数对
给出一串数以及一个数字c,要求计算出所有a-b=c的数对的个数.(不同位置的数字一样的数对算不同的数对)
又是一个二分尺取均可做的题目.
将
a
−
b
=
c
a-b=c
a−b=c转化成
a
=
b
+
c
a=b+c
a=b+c,枚举
b
b
b,判断数组中有多少个
b
+
c
b+c
b+c.
首先把序列排序.维护两个下标
r
1
,
r
2
r1,r2
r1,r2.
对于每一个枚举的
a
[
i
]
a[i]
a[i],
r
2
−
r
1
r2-r1
r2−r1就是等于
a
[
i
]
+
c
a[i]+c
a[i]+c的数字的总个数.
自己思考一下,排完序的时候所有
a
[
i
]
+
c
a[i]+c
a[i]+c都在一起,
r
1
r1
r1指在这一坨
a
[
i
]
+
c
a[i]+c
a[i]+c最左端的位置,
r
2
r2
r2指在最右端
+
1
+1
+1的位置.
如果是二分的话,
r1=lower_bound(a+1,a+n+1,a[i]+c)-a;
r2=upper_bound(a+1,a+n+1,a[i]+c)-a;
用尺取可以代替二分处理这两个位置.
while (a[i]+c>a[r1]) r1++;
while (a[i]+c>=a[r2]) r2++;
可以自己感性理解一下.
接下来的题目是和其它算法的结合,散发了尺取法不一般的魅力.
Codeforces 1042D Petya and Array
给一个序列,问有多少区间的和小于t.
昨天刚打的比赛,看起来很模板的一题,比赛前期AC量一直超过C题.
这题我已经写过博客,请大家看我的博客链接.
Codeforces 1042D Petya and Array 题解 by Fuko_Ibuki
Codeforces 47E Cannon
感谢翻译.
https://www.luogu.org/problemnew/show/CF47E
非常有意思的一道E题,没想到用到的算法都是普及组算法.
这一题的尺取法比起标算奇怪二分来不知道高到哪里去了.
我非常推荐大家去写一下这题.
Codeforces 47E Cannon 题解 by Fuko_Ibuki
Codeforces 939E Maximize!
支持加入一个不小于之前所有数的数,询问整个集合中某个子集最大值减平均值的最大值.
我们思考一下性质.按顺序加入的数单调不降,符合尺取法的要求.
具体证明可以参考当场的题解,但我觉得里面的说明有一点点问题.
它写的内容是
f
(
i
+
1
)
−
f
(
i
)
≤
0
f(i+1)-f(i)\leq 0
f(i+1)−f(i)≤0,所以
f
(
i
)
f(i)
f(i)单调不降.
它的证明是正确的,不过结论应该是单调不升,其它没什么问题.
思考询问的内容,我们可以发现,要求的格式是最大值减平均值,显然最大的数字一定要选,而剩下的数字需要尽量小.
因此要求的集合就是最大的数字和若干个较小的数字.
稍微思考一下可以发现随着加入集合数字个数的增加,能够选的较小的数字的数量是不降的.我们可以利用这个单调性尺取较小数字的个数.
故主程序如下.
typedef long long ll;
ll ans; // ans表示前p-1个数的和.
int p=1,len=0; // p表示目前取到的小数的个数,len表示一共数字的个数.
for (int n=read();n--;){
int op=read(),x=read();
switch(op){
case 1: a[++len]=x; break;
case 2:
for (;p<=len&&1ll*a[p]*p<ans+a[len];++p) ans+=a[p];
/*把a[len]-(ans+a[len])/p反过来转化成乘法,避免精度误差.*/
printf("%.15lf\n",a[len]-1.0*(ans+a[len])/p);
break;
}
}
结合各例题,我们可以发现,尺取法就是在对于枚举每一个
l
l
l的时候,另一个坐标
r
r
r维护的答案也是单调的时候可以使用,能够均摊枚举的时间从而把时间复杂度降到
O
(
n
)
O(n)
O(n).
当你发现所求的问题存在类似的单调性的时候,不妨思考一下尺取法.只是有些时候尺取法推动
l
,
r
l,r
l,r两个指针的复杂度达到了
O
(
n
)
O(n)
O(n),这种时候便要另当别论了.
那么我就讲到这里.谢谢大家了.