NeFut Logo NeFut
EN 管理员登录

深入解析双指针技巧与滑动窗口应用

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #C++ #Sliding Window

核心逻辑与数学原理

双指针(Two Pointers)并不是一种独立的算法,而是搜索的简化版。它通过利用序列的单调性,将朴素的双重循环 $O(N^2)$ 优化为 $O(N)$。

其本质是在二维状态空间 $(i, j)$ 中,利用边界的单调递增或递减特性,剪除无用的状态分支。如果状态量满足 $f(i, j) \le f(i, j+1)$ 且目标是寻找临界值,则当 $i$ 增加时,$j$ 绝无回溯的必要。指针移动的方向是不可逆的拓扑序。


状态设计与快慢/对撞拓扑演进

1. 对撞指针(Two-Way Convergence)

常用于有序数组求特定和。设左右指针为 $l, r$,初始化为 $1, N$。 若 $A[l] + A[r] > \text{target}$,由于数组单调递增,对于任意 $k > l$,必有 $A[k] + A[r] > \text{target}$。因此状态 $(*, r)$ 已无搜索价值,执行 r--。同理,若小于目标值,则执行 l++

2. 快慢指针与滑动窗口(Sliding Window)

用于维护区间的连续属性。设窗口左界 $l$,右界 $r$。 状态定义:$[l, r]$ 为当前维护的合法/非法区间。

由于 $l$ 和 $r$ 均单调递增,每个元素最多入队一次、出队一次。总时间复杂度为:

$$\sum_{i=1}^N (\Delta l_i + \Delta r_i) = O(N)$$


C++ 标准源码(要求是工业级代码)

以下为在 NOIP 评测环境(Linux, GCC, -O2)下完全合规的滑动窗口模板。求解问题:求包含不超过 $K$ 种不同元素的最长连续子数组长度。

#include <iostream>
#include <vector>
#include <algorithm>

using std::cin;
using std::cout;
using std::vector;
using std::max;

// 约束:N <= 100000, 元素值域在 [0, 100000]
const int MAX_VAL = 100005;
int cnt[MAX_VAL]; // 频次哈希表,避免使用 std::map 导致引入 O(log N) 复杂度

int main() {
    // 强制同步断开,加速 I/O。注意:绝对不能再混用 scanf/printf
    std::ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, k;
    if (!(cin >> n >> k)) return 0;

    vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
    }

    int l = 0;
    int distinct_types = 0;
    int max_len = 0;

    // r 为快指针,l 为慢指针
    for (int r = 0; r < n; ++r) {
        // 移入右界元素
        if (cnt[a[r]] == 0) {
            distinct_types++;
        }
        cnt[a[r]]++;

        // 当窗口状态不合法时,持续收缩左界 l
        while (distinct_types > k) {
            cnt[a[l]]--;
            if (cnt[a[l]] == 0) {
                distinct_types--; // 某种元素完全移出窗口
            }
            l++; // 致命踩坑点:l++ 必须在状态更新逻辑之后,否则会导致数组越界或逻辑错乱
        }

        // 此时 [l, r] 必然是合法窗口,更新全局最优解
        max_len = max(max_len, r - l + 1);
    }

    cout << max_len << "\n";

    return 0;
}

NOIP 实战避坑指南

1. 慢指针越界与死循环陷阱

在最优化滑动窗口中(如寻找满足和 $\ge S$ 的最短子数组),写出 while (sum >= S) { sum -= a[l]; l++; } 时,极易在 $l$ 超过 $r$ 甚至超过 $N$ 时引发段错误(Segmentation Fault)。 修正方案:必须在 while 内部置入边界限定 l <= r。但在上述“求不超过 $K$ 种元素的最长区间”逻辑中,当 $k=0$ 时,distinct_types > 0 会导致 l 一路追赶并超越 r。因此,写任何 while 收缩时,先在线判别 l <= r 是否为逻辑前置条件。

2. 计数器更新与指针自增的先后序

在收缩左界时,新手常写出 cnt[a[l++]]--。若当前 cnt[a[l]] 为 1,该语句先提取 cnt[a[l]] 进行减法,随后 l 自增。看起来正确,但若后续有依赖 a[l] 的其他状态判定(例如需要根据移除后的计数更新 distinct_types),则会因为 l 已经改变而读到错误位置的内存。 修正方案:严禁将 ++/-- 算子与复杂的数组下标存取复合写在同一行。分步编写:先修改计数,再处理联动状态,最后 l++


经典 NOIP/洛谷 真题

1. 洛谷 P1638 逛画展

2. 洛谷 P1102 A-B 数对

原文链接: local://1.1

[h] 返回首页