尺取法可以将两重循环优化成一重循环,从而把时间复杂度从 O(n^2)降低到O(n)。
把循环指针 ii 、jj 称为「扫描指针」,在尺取法中,这两个指针 ii、jj,有两种扫描方向:
- 反向扫描。ii、jj 方向相反,ii 从头到尾,jj 从尾到头,在中间相会。也可以把反向扫描的 ii、jj 指针称为「左右指针」。
- 同向扫描。ii、jj 方向相同,都从头到尾,但是速度不一样,比如可以让 jj 跑在 ii 前面。也可以把同向扫描的 ii、jj 指针称为「快慢指针」,此时由于 ii 和 jj 速度不同,ii 和 jj 之间在序列上产生了一个大小可变的「滑动窗口」,这是尺取法的优势,有灵活的应用。
- 下面先介绍单向扫描
- 例一· 回文数字判定:(回文:就是从最后一个和第一个元素相比较,直到每个元素都比较过一次之后,都相同的,可以判定为回文) adcda --回文,abcda--不回文;
- 此题用尺取法的反向扫描比较简单,如果单纯暴力,复杂度是n2;
- 示例代码:
- #include<bits/stdc++.h>// 数组是从0开始索引的,所以要减1;
using namespace std;
int main(){
string s;
cin>>s;
int f=1;
int i=0,j=s.length()-1;
while(i<j){
if(s[i]!=s[j]){
f=0;
cout<<"N";
break;
}
i++;
j--;
}
if(f==1) cout<<"Y";
} - https://www.lanqiao.cn/problems/179--蓝桥杯日志统计那题,我最开始用的是暴力,现在用正向扫描做下:
- t,t+d符合正向扫描的滑动窗口,这个思路是:按时间从小到大处理每个帖子,当处理到 T 时刻的贴子时,窗口[T−D,T) 之前的帖子等于已经失效了,对当前窗口的统计无用。
- 复杂度为O(nlogn)
- 定义 i 指针,它是主循环,遍历随时间而流逝的所有帖子。第 17 行是 i 的 for 循环。
- 定义 j 指针,作用是在时刻 i = T,把 [T−D,T) 之前的帖子都置为无效。具体的做法见第 2020 行:用 j遍历 [T-D, T) 之前的帖子,每遍历一个帖子,就把它的点赞数减一。注意这里的最关键之处:j 是跟随 i 的,而不是重新循环。--此题参考罗老师的思路;
- #include<bits/stdc++.h>
using namespace std;
const int N =10005;
int num[N];
int flag[N];
struct post{
int id;
int ts;
}p[N];
int cmp(post x,post y){
return x.ts<y.ts;
}
int main(){
int n,d,k;
cin>>n>>d>>k;
for(int i=1;i<=n;i++)
cin>>p[i].ts>>p[i].id;
sort(p,p+n+1,cmp);
for(int i=1,j=1;i<=n;i++){ // 默认先全部加
num[p[i].id]++;
while(p[i].ts-p[j].ts>=d){// 如果超时就减1
num[p[j].id]--;
j++;
}
if(num[p[i].id]>=k) //大于点赞值就统计当前的位置
flag[p[i].id]=1;
}
for(int i=1;i<=N;i++)
if(flag[i]==1) cout<<i;
return 0;
}
https://www.lanqiao.cn/problems/1374/
锻造兵器,这题也可以用暴力双重循环做,但是可以先排序,然后用正向双指针扫描,
就是扫到作差等于c的区间,然后这个区间上的数都满足,ans=ans+k-j;
#include<bits/stdc++.h>
using namespace std;
const int N =2e5+5;
int a[N];
int main(){
int n,c; cin>>n>>c;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n);
long long ans=0;
for(int i=1,j=1,k=1;i<=n;i++){
while(j<=n&&a[j]-a[i]<c) j++; // 执行这次程序之后,j++,找到起始的区间,445778,找到的是7而不是5
while(k<=n&&a[k]-a[i]<=c) k++;
if(a[j]-a[i]==c&&a[k]-a[i]==c&&k>=2) // k从第二位开始
ans+=k-j;
}
cout<<ans;
return 0;
}