我在做leetcode—239. 滑动窗口最大值这道题时,看到最优解都用到了单调队列这一数据结构。查遍好多博文之后觉得还是不好理解,直到看到了Pecco大佬的这一篇帖子,写的非常不错,形象易懂,在此推荐该篇文章。
文章原文:算法学习笔记(66): 单调队列
“如果一个选手比你小还比你强,你就可以退役了。”——单调队列的原理
好久没写笔记了,先补一个简单的。单调队列是一种主要用于解决滑动窗口类问题的数据结构,即,在长度为 [公式] 的序列中,求每个长度为 [公式] 的区间的区间最值。它的时间复杂度是 [公式] ,在这个问题中比 [公式] 的ST表和线段树要优。
单调队列的基本思想是,维护一个双向队列(deque),遍历序列,仅当一个元素可能成为某个区间最值时才保留它。
形象地打个比方,上面的序列可以看成学校里各个年级XCPC选手,数字越大代表能力越强。每个选手只能在大学四年间参赛,毕业了就没有机会了。那么,每一年的王牌选手都在哪个年级呢?
一开始的时候,大三大四的学长都比较菜,大二的最强,而大一的等大二的毕业后还有机会上位,所以队列里有两个数。
一年过去了,原本大一的成为大二,却发现新进校的新生非常强,自己再也没有机会成为最大值了,所以弹出队列。
又过了一年,新入校的新生尽管能力只有1,但理论上只要后面的人比他还菜,还是可能成为区间最大值的,所以入队。
终于,原本的王牌毕业了,后面的人以为熬出头了,谁知道这时一个巨佬级别的新生进入了集训队,这下其他所有人都没机会了。
(这只是比方,现实中各位选手的实力是会增长的,不符合这个模型ovo)
总之,观察就会发现,我们维护的这个队列总是单调递减的。如果维护区间最小值,那么维护的队列就是单调递增的。这就是为什么叫单调队列。
代码也很简洁:
deque<int> Q; // 存储的是编号
for (int i = 0; i < n; ++i)
{
if (!Q.empty() && i - Q.front() >= m) // 毕业
Q.pop_front();
while (!Q.empty() && V[Q.back()] < V[i]) // 比新生弱的当场退役(求区间最小值把这里改成>即可)
Q.pop_back();
Q.push_back(i); // 新生入队
if (i >= m - 1)
cout << V[Q.front()] << " ";
}
题目描述
有一个ab的整数组成的矩阵,现请你从中找出一个nn的正方形区域,使得该区域所有数中的最大值和最小值的差最小。
输入格式
第一行为3个整数,分别表示a,b,n的值 第二行至第a+1行每行为b个非负整数,表示矩阵中相应位置上的数。每行相邻两数之间用一空格分隔。
输出格式
仅一个整数,为ab矩阵中所有“nn正方形区域中的最大整数和最小整数的差值”的最小值。 这个题可以用单调队列先后处理行和列。
矩阵中的每一行可以看作长度为 [公式] 的序列,我们用单调队列求出其中所有长度为 [公式] 的区间的最值。对每行都如此操作,即可求出每个 [公式] 的长方形区域中的最大、最小整数。
这些最值又构成一个矩阵。我们再把这个新矩阵的每一列看作一个序列,求其中所有长度为 [公式] 的区间最值。这样,可以求出原矩阵每个 [公式] 的正方形区域中的最大、最小整数。
这时我们得到两个 [公式] 的矩阵,包含了所有 [公式] 正方形区域的最小、最大值,遍历一遍求最小差即可。
题目描述
给定一行n个非负整数a[1]…a[n]。现在你可以选择其中若干个数,但不能有超过k个连续的数字被选择。你的任务是使得选出的数字的和最大。
输入格式
第一行两个整数n,k 以下n行,每行一个整数表示a[i]。
输出格式
输出一个值表示答案。 这种题是很典型的单调队列优化DP。
我们把问题转化为删除若干个数,且删除的数间隔不超过k,求删除数的最小值。设dp[i]表示在删除第i个数的情况下, 前i个数中删除数的最小和。那么很容易想到转移方程:
![[公式]](https://i-blog.csdnimg.cn/blog_migrate/5b3421e826ed3f1a35359e73aab0c798.png)
这是因为,如果要删除某个数,除非它是前k+1个数之一,否则在它之前的k+1个数中,至少要删除一个。最后的答案在最后k+1个数里找最小值,然后用总和去减即可,因为最后k+1个数中至少有一个是要删除的。
这个朴素方法是O(nm)的,为了优化它,我们可以使用单调队列。注意到,我们不断地在求dp的区间最小值,而且区间长度是固定的m+1,这正好符合滑动窗口的模型。只不过,我们需要动态地进行整个过程,即,在维护单调队列的过程中求出dp。
附一份参考代码:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main()
{
ios::sync_with_stdio(false);
ll n, m, sum = 0;
cin >> n >> m;
vector<ll> A(n + 1), dp(n + 1);
for (int i = 1; i <= n; ++i)
cin >> A[i], sum += A[i];
deque<int> Q;
for (int i = 1; i <= n; ++i)
{
if (i > m + 1)
dp[i] = dp[Q.front()] + A[i];
else
dp[i] = A[i];
if (!Q.empty() && i - Q.front() >= m + 1)
Q.pop_front();
while (!Q.empty() && dp[Q.back()] > dp[i])
Q.pop_back();
Q.push_back(i);
}
cout << sum - *min_element(dp.end() - m - 1, dp.end()) << endl;
return 0;
}
794

被折叠的 条评论
为什么被折叠?



