为了更好的阅读体验,欢迎阅读原文:
[[思维提升|干货All in]6种算法解决LeetCode困难题:滑动窗口最大值 (eriktse.com)](https://www.eriktse.com/algorithm/1039.html)
最近在leetcode遇到一道非常经典的题目:239. 滑动窗口最大值 – 力扣(LeetCode)
以前只会看题解用单调队列做,最近研究一下发现是一道很好的题,可以帮助我们提升“维护区间最值”的算法思维。
先介绍一下我解决这题所用的算法及其复杂度:
- 单调队列 O(n)
- st表 O(nlogn)
- 树状数组 O(n(logn)^2)
- 多重集合法 O(nlogn)
- 莫队O (n sqrt{n})
- 优先队列 O(nlogn)
首先确定一点,单调队列是解决这道题最好的办法,但是其他的方法的适用范围更广。
1、单调队列
首先可以参考几篇优秀的文章:
算法学习笔记(66): 单调队列 – 知乎 (zhihu.com)
单调队列 – OI Wiki (oi-wiki.org)
我这里提几点值得注意的地方:
1.单调队列中存放的是下标,而不是元素值
2.单调队列是一个双端队列,尾插前先查队头后查队尾
3.单调队列维护的是元素值的单调性
有了这几点注意,代码就很好写了:
class Solution {
public:
static const int maxn = 1e5 + 9;
int a[maxn];
deque dq;
vector maxSlidingWindow(vector& nums, int k) {
vector res;
int n = nums.size();
for(int i = 1;i = a[dq.back()])dq.pop_back();
dq.push_back(i);
if(i >= k)res.push_back(a[dq.front()]);
}
return res;
}
};
我做题习惯把输入数组存一个array,大家请勿介意。
2.st表
st表是一种基于DP(动态规划)思想的算法,也算是一种数据结构吧。
st表可以静态维护区间的最值,需要用的时间来预处理,后可以O(1)查询。
我们定义dpi表示
表示区间的最值,在这道题里我们认为是最大值(维护最小值同理)。看一下能不能开下,大概是maxn * 20的大小,可以开下。
通过定义不难发现,dpi = a[i],因为此时区间长度为1,那么最值就是元素a[i]本身。当j增大时,我们有转移方程(具体的原因可简单自行推导,以后的文章中也会有讲解):
dpi = max(dpi, dpi + 2^(j-1))
查询十分方便,方法是从两边往中间尽可能多地覆盖。假设要查询的区间是[l,r]
,那么我们可以得到区间长度r – l + 1,现在求一个比长度小且最大的2的幂,然后把左右两块取大即可。
直接看代码:
class Solution {
public:
static const int maxn = 1e5 + 9;
int a[maxn], st[maxn][30];//st[i][j]表示[i, i + 2^j - 1]的最大值
int queMax(int l, int r)
{
int k = log(r - l + 1) / log(2);
return max(st[l][k], st[r - (1 maxSlidingWindow(vector& nums, int k)
{
memset(st, 0xcf, sizeof st);
vector res;
int n = nums.size();
for(int i = 1;i
3、树状数组
看见树状数组可能有小朋友会感到疑惑了哦,树状数组不是维护区间和的吗?怎么还来凑“区间最值”的热闹了。
其实树状数组不仅可以维护区间和,还可以“动态维护区间最值”,但是维护的方法和区间和略有不同。这一次主要看一下代码吧,具体的原理之后再讲。
树状数组节点t[k]维护的是区间[k – lowbit(k) + 1, k]。
树状数组主要突出的优点就是可以动态维护,但是注意在维护区间最值的时候仅可单点修改,不支持区间修改。
class Solution {
public:
static const int maxn = 1e5 + 9;
//树状数组
int a[maxn], t[maxn], n;//t[i] 表示 a[i - lowbit(i) + 1] ~ a[i]的最大值
int lowbit(int x){return x & -x;}
void update(int k, int x)// modify a[k] to x
{
a[k] = x;
while(k = l; r -= lowbit(r))res = max(res, t[r]);
res = max(res, a[r --]);
}
return res;
}
vector maxSlidingWindow(vector& nums, int k)
{
n = nums.size();
for(int i = 1;i res;
for(int i = 1;i = k)res.push_back(queMax(i - k + 1, i));
}
return res;
}
};
4、多重集合法
这种方法就十分简单粗暴了,就是维护一个不断移动的multiset,简直是暴力之王。
class Solution {
public:
vector maxSlidingWindow(vector& nums, int k)
{
multiset st;
vector res;
for(int i = 0;i
5、莫队
莫队需要基于multiset,在这道题里的优势并不明显,因为这题的询问都是有顺序的,但是可以写个莫队练个手。
莫队在处理随机区间查询问题的时候有独特的优势,因为足够暴力,所以维护的东西可以很多很杂,比如区间和,区间最值,区间元素种类数等。
以后我会详细讲莫队的,欢迎大家访问我的个人博客:https://www.eriktse.com
在右上角留下邮箱订阅我的博客,每周更新优质的算法/技术/互联网文章!
class Solution {
public:
static const int maxn = 1e5 + 9;
int a[maxn], pos[maxn], ans[maxn], n;
struct Q
{
int l, r, id;//询问离线
}q[maxn];//outline algorihm
multiset st;
void Add(int k)//把a[k]加入到区间内
{
st.insert(a[k]);
}
void Del(int k)
{
st.erase(st.find(a[k]));
}
vector maxSlidingWindow(vector& nums, int k)
{
n = nums.size();
for(int i = 1;i [l, r]
int l = q[i].l, r = q[i].r, id = q[i].id;
while(L l)Add(-- L);
while(R > r)Del(R --);
while(R res;
for(int i = 1;i
6、优先队列
优先队列可以理解为一个“堆”结构。
我们知道优先队列可以维护最值,但是他只有一个堆顶怎么办呢?
我们只能保证堆顶是最大值但是却无法保证堆顶是在窗口内的呀!
为了解决这个问题,我们在每一次查询堆顶之前,都要对堆顶进行检查,直到堆顶在窗口内才能输出。
注意以下几点:
1.堆里存放的是下标,但是比较函数用值比较。
2.每次取出元素之前堆顶检查,只要堆顶的位置不在窗口内就一直弹出。
上代码!
const int maxn = 1e5 + 9;
int a[maxn];
class Solution {
public:
struct cmp{
bool operator ()(const int &u, const int &v)const
{
return a[u] , cmp> pq;
vector maxSlidingWindow(vector& nums, int k)
{
int n = nums.size();
for(int i = 1;i res;
for(int i = 1;i = k)res.push_back(a[pq.top()]);
}
return res;
}
};
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net