21.双指针(尺取)

双指针(尺取)

通过两个(或多个)移动的标记,高效地探索或处理数据结构中的连续部分,以简化问题并加快求解速度,通常称为双指针、尺取或滑动窗口等。

例:单词背诵

给一个单词表以及一个文章,要在文章里找包含单词表中单词最多的前提下最短的一段。

进一步解释:在文章中找到起点第 \(i\) 个单词,终点第 \(j\) 个单词,包含单词表中单词数量最多的\([i,j]\) 区间可能不唯一,找最短的那个区间。

输入 \(n\) 然后 \(n\) 个单词的单词表,接着输入 \(m\) 然后 \(m\) 个单词的文章。

输入

1
2
3
4
5
6
7
8
9
10
3
hot
dog
milk
5
hot
dog
dog
milk
hot

输出

1
2
3
3

这道题有“包含最多”和“长度最短”两个要求,而“包含最多”的要求是优先的,这说明要不放弃任何一个包含的单词。

将起点 \(j\) 和终点 \(i\) 设置为双指针, \(i\) 先走。

  1. \(i\) 走一步:
    • 文章第 \(i\) 个单词是否是单词表里的单词
    • 目前的 \([j,i]\) 区间包含了这个单词几次
  2. \(j\) (可能)走多步:
    • 文章第 \(j\) 个单词如果不是单词表里的, \(j\) 向前走一步
    • 文章第 \(j\) 个单词是单词表里的,且目前 \([j,i]\) 区间包含不止一个, \(j\) 向前走一步
    • 文章第 \(j\) 个单词是单词表里的,但目前 \([j,i]\) 区间里只有 1 个,\(j\) 停下,不再尝试往前走
  3. 现在 \([j,i]\) 包含单词数是否增多,\([j,i]\)长度(i-j+1)是否更短,更新答案
  4. 重复执行 1) ~ 3)

关于是否包含、包含几个的问题,我们学过hash和字典树都可以很容易处理了,当然如果你已熟练掌握,那么现在可以用STL的mapunorderd_map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<cstdio>
#include<algorithm>
#include<map>
#include<string>
const int maxn = 1e3 + 10;
const int maxm = 1e5 + 10;

char a[maxn][21], b[maxm][21];
int n, m;
int main() {
scanf("%d", &n);
std::map<std::string, int> mp;
for(int i = 0; i < n; i ++) {
scanf("%s", a[i]);
mp[a[i]] = 0;
}
scanf("%d", &m);
for(int i = 0; i < m; i ++) {
scanf("%s", b[i]);
}
int tmp_cnt = 0, ans_cnt = 0, ans_len = m;
for(int i = 0, j = 0; i < m; i ++) {
if(mp.count(b[i])) { // map的count查询是否包含
mp[b[i]] ++; // 包含则计数+1
if(mp[b[i]] == 1) { // 首次计入
tmp_cnt ++;
}
}
while(j <= i) {
if(mp.count(b[j])) { // b[j] 是否在单词表中
if(mp[b[j]] > 1) { // [j, i] 是否有不止一个
mp[b[j]] --; // j要向前走,统计值减一
} else {
break; // [j,i] 只包含一个b[j],不能丢掉它,结束本次循环
}
}
j ++; // j 向前走
}
if(tmp_cnt >= ans_cnt) { // 总结当前 [j,i] 区间对答案的更新
if(tmp_cnt > ans_cnt || i - j + 1 < ans_len) {
ans_len = i - j + 1;
}
ans_cnt = tmp_cnt;
}
}
printf("%d\n%d\n", ans_cnt, ans_len);

return 0;
}

例:挑选钻石

给定 \(N\) 颗钻石的大小和一个整数 \(K\)\(N \leq 50,000\), \(0 \leq K \leq 1,000,000,000\)),你的任务是确定可以在两个展示柜中展示的最大钻石数量。如果两颗钻石的大小之差超过 \(K\),则它们不能放在同一个展示柜中;若大小之差恰好为 \(K\),则可以放在同一个展示柜中。

分析:如果把 \(N\) 个数排序,那么答案就是找两个不重叠的子串,每个子串最大值与最小值差不超过\(K\),两个子串长度之和最大的方案。

在排序后,先考虑找一个这样的子串的最大方案怎么找:双指针保持 \(i\)\(j\) 的价值之差不超过 \(K\),统计最长的一段。

考虑找两个不重叠的:如果对任意划分界限,界限左边找一段最长的,界限右边也找一段最长的,最优方案一定在某一个界限时得到。

需要快速知道确定界限时,左边和右边各自的最优解。

  • 从左往右双指针一遍:记录每个位置作为界限,左边的最优解
  • 从右往左双指针一遍:记录每个位置作为界限,右边的最优解
  • 枚举界限,把左右最优解加起来,找全局最优
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<map>
#include<string>
const int maxn = 5e4 + 10;
int n, k, a[maxn], l_max[maxn], r_max[maxn];
int main() {
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i ++) {
scanf("%d", &a[i]);
}
std::sort(a, a + n);
memset(l_max, 0, sizeof(l_max));
memset(r_max, 0, sizeof(r_max));
for(int i = 0, j = 0; i < n; i ++) { // 双指针较快的 i
for(;j < i && a[i] - a[j] > k; j ++); // 双指针跟随的 j,保持在刚刚好不超过 K 的最远位置
// 以 i 为界限的左边最优解,应当是 “以 i 为结尾的新方案” 和 “之前的方案” 的最优的那个
l_max[i] = std::max(i > 0 ? l_max[i - 1] : 0, i - j + 1);
}
// 从右往左同理,处理好坐标方向和边界
for(int i = n - 1, j = n - 1; i >= 0; i --) {
for(;j > i && a[j] - a[i] > k; j --);
r_max[i] = std::max(i < n - 1 ? r_max[i + 1] : 0, j - i + 1);
}
// 在每个位置作为界限尝试
int ans = 0;
for(int i = 0; i < n; i ++) {
ans = std::max(ans, l_max[i] + r_max[i + 1]);
}
printf("%d\n", ans);
return 0;
}

界限往右的预处理也可以省略:如果已经得到了任意界限左边的最优解(不一定是以界限为结尾的解),从界限出发往右找最远位置,与界限左边的最优解合起来也就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<map>
#include<string>
const int maxn = 5e4 + 10;
int n, k, a[maxn];
int main() {
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i ++) {
scanf("%d", &a[i]);
}
std::sort(a, a + n);
int ans = 0, l_max = 0;
for(int i = 0, j = 0, r = 0; i < n; i ++) {
for(;j < i && a[i] - a[j] > k; j ++); // [j,i]
l_max = std::max(l_max, i - j + 1);
for(; r < n && i < n - 1 && a[r] - a[i + 1] <= k; r ++); // [i+1, r-1]
ans = std::max(ans, r - i - 1 + l_max);
}
printf("%d\n", ans);
return 0;
}