我是黑洞小白,欢迎来到算法竞赛系列!(先赞后看,互三必回!)
引言
在很多题目中,会有“请找出一个区间,满足…”这样的描述。但对于寻找一个区间,很多人第一反应就是:枚举左右端点。但这样的复杂度是O(n^2)的,太慢。于是,我们需要一个更快的、更适用的算法。它,就是——
尺取法
概念介绍
尺取法(又称双指针,Two Pointers),是一个常用的优化技巧,时间复杂度为O(n^2),用来解决序列的区间问题。尺取法的前提条件是:区间是单调的,和二分法相同,所以很多题目用尺取和二分都行。
代码实现
问题和序列的区间有关,用 i, j 分别表示左右端点的下标(这就是所谓的指针)
暴力枚举
for (int i = 0; i < n; i++) { // 左端点
for (int j = i + 1; j < n; j++) { // 右端点
// 代码
}
}
尺取法
显然,我们需要优化上面的代码。怎么优化?就是说,要去掉不必要的枚举,首先要明确一件事:序列在某一方面是有序的。既然是有序的,大的序列符合了,小的序列就不用再枚举了,直接加上大序列所包含的小序列数量。(先结合例题理解效果更好)
for (int i = 1, j = 0; i <= n; i++) {
while (/*题目条件*/ && j < n) {
j++; // 枚举j
}
if (/*题目条件*/)
ans += (n - j + 1); // 改变答案
//i++带来的变化"
}
算法实战
例题1:数列的部分和
题目描述
题目分析
首先需要枚举 i(左端点),它是单调的,从 1 到 n 。再看 j(右端点),从 i+1 到 n ,并对 a[j] 进行累加(一定是不断变大的,这点很重要),一旦 sum >= k 说明在 j 之后的所有右端点都是满足条件的(j, j+1, j+2 ...... n),在 ans 中加上这些数量,最后 i++ 。为什么 j 不用变?因为 i++ 后当前的总和一定比之前小,保证不会超过 k。(看不懂就多看几遍)
总之,在尺取中,单调性是非常重要的。
题解
#include <bits/stdc++.h>
using namespace std;
int a[100010];
int main() {
int n;
long long k;
cin >> n >> k;
long long sum = 0, ans = 0;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1, j = 0; i <= n; i++) {
while (sum < k && j < n) {
j++;
sum += a[j];
}
if (sum >= k)
ans += (n - j + 1);
sum -= a[i];
}
cout << ans;
return 0;
}
例题2:找相同数对
题目描述
题目分析
如果用尺取法做,那么首先要找到题目中的“单调”。将数组排序,然后就会发现每个数 A ,对应的数 B 一定是一段连续的区间。因为排序之后序列的有序性,我们枚举每个数,他们的左端点和右端点都是单调不降的。啧,单调不就出来了吗?
题解
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, c;
int a[N];
int main () {
cin >> n >> c;
for(int i = 1; i <= n; i++) cin >> a[i];
sort(a+1, a+1+n);
int l = 1, r1 = 1, r2 = 1;
long long ans = 0;
for(l = 1; l <= n; l++) {
while(r1 <= n && a[r1] - a[l] <= c) r1++;
while(r2 <= n && a[r2] - a[l] < c) r2++;
if(a[r2] - a[l] == c && a[r1-1] - a[l] == c && r1 - 1 >= 1)
ans += r1 - r2;
}
cout << ans;
return 0;
}