单调队列


算法笔记:单调队列与滑动窗口进阶

单调队列的核心在于“如果在你之后入队的人比你强(或者更符合条件),那你就永远没机会了”。通过维护一个双端队列(deque),存储原数组的下标,并保持队列内部对应元素值的单调性,可以实现 $O(n)$ 复杂度下的区间最值查询。

级别一:定长滑动窗口(基础模板)

对应文件P_1886_模板_单调队列_滑动窗口.cpp

  • 🏷️ 题面抽象:给定一个长度为 $n$ 的数组和一个固定大小为 $k$ 的滑动窗口。窗口从左向右滑动,求每次滑动时窗口内的最大值和最小值。

  • 💡 核心思路

  • 维护单调性:以求最小值为例,维护一个单调递增队列。遍历时,若队尾元素 $\ge$ 当前元素,则不断弹出队尾,然后将当前元素的下标入队。

  • 剔除过期元素:若队头下标 $< i - k + 1$,说明队头元素已经离开窗口,从队头弹出。

  • 记录答案:当遍历索引 $i \ge k$ 时,队头元素对应的就是当前窗口的最值。

  • ⚠️ 细节提醒:队列中务必存下标而不是具体的值,否则无法判断元素是否已经滑出窗口边界。

点击查看代码
void solve()
{
	int n, k;
	cin >> n >> k;
	deque<int> q; // 存的下标
	vi a(n + 1);
	for (int i = 1; i <= n; i++)
		cin >> a[i];

	deque<int> mnq;
	for (int i = 1; i <= n; ++i)
	{

		while (mnq.size() && a[mnq.back()] > a[i])
		{
			mnq.pop_back();
		}
		mnq.push_back(i);
		if (!mnq.empty() && mnq.front() < i - k + 1)
			mnq.pop_front();
		int front = a[mnq.front()];
		if (i >= k)
			cout << front << ' ';
	}
	cout << endl;

	for (int i = 1; i <= n; ++i)
	{

		while (q.size() && a[q.back()] < a[i])
		{
			q.pop_back();
		}

		q.push_back(i);
		if (!q.empty() && q.front() < i - k + 1) // 注意判断 q 不为空
			q.pop_front();
		int front = a[q.front()];
		if (i >= k)
			cout << front << ' ';
	}
	cout << endl;
}


级别二:定长滑动窗口 + 前缀和转化

对应文件U_192528_最大子序和.cpp

  • 🏷️ 题面抽象:在长度为 $n$ 的序列中,找出一个长度不超过 $m$ 的连续子序列,使其元素总和最大。

  • 💡 核心思路

  • 区间和问题优先转化为前缀和:子序列 $[j, i]$ 的和为 $S_i - S_{j-1}$。

  • 要使 $S_i - S_{j-1}$ 最大,对于固定的右端点 $i$,我们需要在合法的窗口范围 $[i-m, i-1]$ 内,找到一个最小的 $S_{j-1}$

  • 这本质上退化成了级别一的问题:用单调递增队列维护前缀和数组在长度为 $m$ 的滑动窗口内的最小值。

  • ⚠️ 细节提醒

  • 入队时机:当遍历到 $i$ 时,准备入队的是 $i-1$ 的状态。

  • 越界判断:合法区间是 $[i-m, i-1]$,因此过期的队头条件是下标 $< i-m$。

点击查看代码
// 输入一个长度为 n 的整数序列,
// 从中找出一段长度不超过 m 的连续子序列,使得子序列中所有数的和
void solve() {
    int n, m;
    cin >> n >> m;
    vi a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        a[i] += a[i - 1];
    }
    // 对每个 i 找到 (i - m) 到 (i - 1) 这个滑动窗口的最小值
    int mx = -1e18;
    deque<int> q; // 单调递增序列
    for (int i = 1; i <= n; i++) {

        while (!q.empty() && a[q.back()] > a[i - 1]) {
            q.pop_back();
        }
        q.push_back(i - 1);

        if (!q.empty() && q.front() < i - m) {
            q.pop_front();
        }
        int sj = a[q.front()];
        int si = a[i];
        // cerr << 1 << endl;
        mx = max(si - sj, mx);
    }
    cout << mx << endl;
}


级别三:动态窗口求“最长”满足条件的区间

对应文件P_3512_POI_2010_PIL_Pilots.cpp

  • 🏷️ 题面抽象:求最长的连续子段,使得该子段内的“最大值与最小值之差”不超过 $k$。
  • 💡 核心思路
  • 窗口大小不再固定,属于动态滑动窗口(尺取法)
  • 需要两个单调队列同时维护当前窗口内的最大值和最小值。
  • 扩张与收缩:维护左端点 $l$。右端点 $i$ 不断向右扩张并入队;如果此时刻极差 max_val - min_val > k,说明当前窗口不合法,必须将左端点 $l$ 向右收缩(l++),同时弹出所有下标 $< l$ 的队头元素,直到窗口重新合法。
  • 在每次确认窗口合法后,使用 $i - l + 1$ 更新最长长度答案。
点击查看代码
//题意 给定 n,k 和一个长度为 n 的序列,求最长的最大值最小值相差不超过 k 的子段
//求窗口长度的最大值
void solve()
{
	int k, n;
	cin >> k >> n;
	vi a(n + 1);
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	deque<int> mxq;
	deque<int> mnq;
	int ans = -1;
	int l = 1;//要维护左端点
	for (int i = 1; i <= n; i++)
	{

		while (mnq.size() && a[mnq.back()] > a[i])
			mnq.pop_back();
		mnq.push_back(i);
		while (mxq.size() && a[mxq.back()] < a[i])
			mxq.pop_back();
		mxq.push_back(i);
		// 动态区间窗口 要用 while维护
		//必须要写在 i 加入后我们模拟时就是加入之后
		//直接判断的要符合模拟的
		while (mxq.size() && mnq.size() && a[mxq.front()] - a[mnq.front()] > k)
		{
			l++;
			while (mxq.size() && mxq.front() < l)
				mxq.pop_front();
			while (mnq.size() && mnq.front() < l)
				mnq.pop_front();
		}
		ans = max(ans, i - l + 1);
	}
	cout << ans << endl;
}

级别四:动态窗口求“最短”满足条件的区间(二维坐标轴)

对应文件P_2698_USACO_12_MAR_Flowerpot_S.cpp

  • 🏷️ 题面抽象:给定二维平面上的 $n$ 个点 $(x, y)$,求一个最小的宽度(即两点 $x$ 坐标的差值),使得在该宽度范围内的点,其 $y$ 坐标的极差 $\ge d$。

  • 💡 核心思路

  • 降维排序:先将所有点按照 $x$ 坐标升序排序,将问题转化为一维序列上的滑动窗口。

  • 依然使用双单调队列维护窗口内 $y$ 坐标的最大值和最小值。

  • 满足条件即收缩:与级别三逻辑相反。当 $i$ 向右扩张且发现极差 $\ge d$ 时,说明已经找到了一个合法解。此时记录 $x_i - x_l$ 更新最小宽度答案,然后尝试将左端点 $l$ 右移(收缩窗口)来寻找看看有没有更短的合法宽度,并同步清理过期队头。

  • ⚠️ 细节提醒

  • 判断合法并收缩的操作一定要写在 while 循环里,因为左端点 $l$ 可能可以连续向右移动多次且依然满足极差 $\ge d$。

  • 如果没有找到任何合法解,注意根据初始化的边界值(如 2e18)输出 -1

点击查看代码
// 滑动窗口求最值 单调队列
//求窗口长度的最小值
//把判断写在while里面
void solve()
{
	int n, d;
	cin >> n >> d;
	vector<pair<int, int>> a(n + 1);
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i].first >> a[i].second;
	}
	sort(a.begin() + 1, a.end());
	deque<int> mxq; // 存的是下标
	deque<int> mnq;
	int l = 1;
	int ans = 2e18;
	for (int i = 1; i <= n; i++)
	{
		while (mxq.size() && a[i].second < a[mxq.back()].second)
		{
			mxq.pop_back();
		}
		mxq.push_back(i);
		while (mnq.size() && a[i].second > a[mnq.back()].second)
			mnq.pop_back();
		mnq.push_back(i);
		//窗口的len 最小值这样写没问题
		while (mxq.size() && mnq.size() && a[mnq.front()].second - a[mxq.front()].second >= d)
		{
	      int len = abs(a[i].first - a[l].first);
		  ans = min(len, ans);
			l++;
			while (mxq.size() && mxq.front() < l)
				mxq.pop_front();
			while (mnq.size() && mnq.front() < l)
				mnq.pop_front();
		}
	
	}
	if (ans == 2e18)
	{
		cout << -1 << endl;
	}
	else
		cout << ans;
}
posted @ 2026-05-16 18:52  r_123  阅读(16)  评论(0)    收藏  举报