双指针法
引入
双指针顾名思义,就是同时使用两个指针,在序列、链表结构上指向的是位置,在树、图结构中指向的是节点,通过或同向移动,或相向移动来维护、统计信息。 ——OI Wiki
接下来我们来看双指针的几个具体使用方法。
板子
以 POJ3061 为例:
给定长度为 \(n\) 的序列 \(a\),请你找到一个区间,使得这个区间的和大于等于 \(k\) 的条件下数字个数尽可能少。
\(n\leq 10^5\),\(1\leq a_i,k\leq 10^9\)
过程
维护两个指针,左指针 \(l\) 与右指针 \(r\),初始时都指向数组开始(\(l=1\),\(r=1\)),并额外维护区间和 \(sum\)。
如,\(a=\{5,1,4,3,4\}\),\(k=7\)。
-
初始时,\(l\) 指向 \(a_1\)。接着向右延伸 \(r\),显然 \(r\) 指向 \(a_3\) 时满足条件,即当 \(l=1\) 时答案为 \(r-l+1=3\)。
-
接着,我们将 \(l\) 滑到 \(a_2\) 的位置,我们知道 \(r\) 不会小于 \(3\),所以我们继续向右延伸到 \(r=4\),即当 \(l=2\) 时答案为 \(3\)。
-
然后,我们将 \(l\) 滑到 \(a_3\) 的位置,\(r\) 从 \(4\) 开始枚举,发现正好满足条件,即当 \(l=3\) 时答案为 \(2\) 。
-
我们再将 \(l\) 滑到 \(a_4\) 的位置,\(r\) 从 \(4\) 开始枚举,向右延伸到 \(r=5\) 的时候满足条件,即当 \(l=4\) 时答案为 \(2\)。
-
最后 \(l\) 滑到 \(a_5\) 的位置 ,\(r\) 从 \(5\) 开始枚举,发现无法满足条件,此时往右走没有能满足条件的区间,结束。
\(\texttt{code}\)
for(int l=1,r=1.sum=0;l<=n;l++){
while(sum+a[r]<k&&r<=n){
r++;
}
if(r==n+1){
break;
}
ans=min(ans,r-l+1);
}
代码中,虽然 for
循环中嵌套着 while
循环,但是无论是 \(l\) 还是 \(r\),都只是从最左边滑到最右边,所以两个指针的时间复杂度均为 \(\mathcal{O(n)}\),所以双指针的复杂度为 \(\mathcal{O(n)}\)。
总结
给定一个序列 \(a\),请你选取一个区间,使得这个区间在满足某个条件的前提下:
- 求满足条件的区间数量。
- 求满足条件的答案最值。
且这类问题满足:选取出区间 \([l,r]\) 是满足条件的,那么比它更大的一个区间 \([l',r'](l'\leq l,r'\geq r)\) 也满足条件。并且更大的这个区间所求信息答案更差,那么我们就可以用双指针法来解决这个问题。
例题
Problem 1. Luogu P1102
A-B 数对:给出一串正整数数列以及一个正整数 \(C\),要求计算出所有满足 \(A - B = C\) 的数对的个数(不同位置的数字一样的数对算不同的数对)。
\(n\leq 2\times 10^5,1\leq a_i,C\leq 10^9\)
这道题显然可以用二分答案解决,枚举数字 \(B\),二分查找是否存在 \(A\) 使得 \(A-B=C\) 即可,这是因为将这些数字排序后,数列具有单调性,就能以 \(\mathcal{O(\log n)}\) 的时间复杂度查找。
这道题同样可以使用双指针来解决。以样例为例:
\(a={1,1,2,3}\),\(C=1\)。
枚举 \(i\) 表示 \(A\),接下来要在数列中找到 \(B\) 使得 \(B=A-C\)。因为题中数字有可能相同,所以满足条件的 \(B\) 在数列中连续出现。因为 \(a\) 具有单调性,所以维护 \(l,r\),使得 \(l\) 指向第一个大于等于 \(B\) 的数,\(r\) 指向第一个大于 \(B\) 的数。这样,若 \(a_l=B\),则 \([l,r)\) 为最终答案区间,对于 \(i\),数对个数为枚举的 \(r-l\),累加进答案中。
\(\texttt{code}\)
sort(a+1,a+n+1);
for(int l=1,r=1,i=1;i<=n;i++){
while(a[l]<a[i]-c&&l<=n){
l++;
}
while(a[r]<=a[i]-c&&r<=n){
r++;
}
if(a[l]==a[i]-c){
ans=ans+r-l;
}
}
Problem 2. Luogu P1638
逛画展:给定一个正整数数列 \(a\),求一个数对 \((l,r)\),满足所有 \(a_i(l\leq i\leq r)\) 中包含 \(1\sim m\)。
\(1\leq n\le10^6\),\(1 \leq a_i \leq m\le2\times10^3\)。
如果区间 \([l,r]\) 包含 \(m\) 种不同的数,那么包含它的任何区间都包含 \(m\) 种不同的数。同时包含它的大区间答案更差,所以可以使用双指针。维护 \(l,r\) 为符合要求的区间,考虑贪心策略,当 \(l\) 固定时,\(r\) 向右延伸,直到凑齐 \(1\sim m\) 为止,用桶记录 \(1\sim m\) 是否在区间内出现,\(cnt\) 记录不同的 \(a_i\) 的个数。接着,尝试去除 \(l\) 指向的数(即 f[a[l]]--,cnt--
,然后继续向右移动。
\(\texttt{code}\)
for(int l=1,r=0,cnt=0;l<=n;l++){
while(cnt<m&&r<n){
r++;
if(!f[a[r]]){
cnt++;
}
f[a[r]]++;
}
if(cnt<m){
break;
}
f[a[l]]--;
if(f[a[l]]==0){
cnt--;
}
if(minn>r-l+1){
minn=r-l+1;
L=l;
R=r;
}
}
Problem 3. UVa11572
唯一的雪花:\(T\) 组数据,每组测试数据中有 \(n\) 个整数 \(a_i\),数字可能重复。求一个尽可能长的区间,满足区间内所有的数字均不相同。
\(n\leq 10^6\),\(0\leq a_i\leq 10^9\)
显然的双指针。与上题大体相同,开一个桶 \(f\),右端点向右延伸的条件为 \(f_{a_r+1}==0\),区别在于这道题 \(a_i\) 是 \(10^9\) 级别的,显然不能直接开一个桶。这里完全可以使用 STL 里的 \(map\)、\(set\) 或 hash 来解决,但这样的常数无疑是巨大的,这里使用离散化。
离散化的方法是,将大范围的数字映射到一个较小的连续区间内,从而可以使用数组来统计数字的出现次数。如,对于 \(a=\{1,1,10^9,10^9,10^9\}\),我们使用离散化可以将 \(1\to 1\),\(10^9\to 2\),从而实现减少桶的空间开销。具体地,将原始数组复制到一个临时数组中,对临时数组进行排序并去重,得到一个唯一的有序数组,再将原始数组中的每个数字通过二分查找映射到去重后的数组中的位置,从而实现离散化。
离散化后,因为 \(n\leq 10^6\),所以桶的大小不超过 \(n\leq 10^6\),从而可以解决这道题。
\(\texttt{code}\)
void lsh(){
for(int i=1;i<=n;i++){
p[i]=a[i];
}
sort(p+1,p+n+1);
int num=unique(p+1,p+n+1)-p-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(p+1,p+num+1,a[i])-p;
}
}
...
lsh();
for(int l=1,r=0;l<=n;l++){
while(!f[a[r+1]]&&r<=n){
r++;
f[a[r]]++;
}
f[a[l]]--;
ans=max(ans,r>n?r-l:r-l+1);
}
Problem 4. ARC098B
给定数组 \(a\),求满足 \(a_l+a_{l+1}+...+a_r=a_l \oplus a_{l+1}\oplus...\oplus a_r\) 的区间 \([l,r]\) 的数量。
\(1\leq l\leq r\leq n\leq 2\times 10^5\),\(0\leq a_i< 2^{20}\)
异或即不进位的加法,所以这道题换个说法就是你希望找出有多少个区间,他们之间做加法是不会进位的。易得若区间 \([l,r]\) 会产生进位,则区间 \([l',r'](l'\leq l,r'\geq r)\) 也会产生进位,所以可以使用双指针法来解决。于是我们枚举 \(l\),向右延伸 \(r\) 到最右的满足条件的位置,给答案加上 \(r-l+1\),因为区间 \([l,r]\) 不产生进位,则区间 \([l,r'](l\leq r'\leq r)\) 也不产生进位。同理,为什么可以 \(r\) 不变,移动一次 \(l\),因为区间 \([l,r]\) 不产生进位,则区间 \([l',r](l\leq l'\leq r)\) 也不产生进位。
\(\texttt{code}\)
for(int l=1,r=0,sum=0,oplus=0;l<=n;l++){
while(sum+a[r+1]==(oplus^a[r+1])&&r<n){//一定要打小括号!!!!!
r++;
sum=sum+a[r];
oplus=oplus^a[r];
}
ans=ans+r-l+1;
sum=sum-a[l];
oplus=oplus^a[l];
}