为了更好的阅读体验,欢迎阅读原文:

[思维提升|干货All in]6种算法解决LeetCode困难题:滑动窗口最大值 (eriktse.com)

最近在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<int> dq;
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
int n = nums.size();
for(int i = 1;i <= n; ++ i)a[i] = nums[i - 1];
for(int i = 1;i <= n; ++ i)
{
int x = a[i];
while(!dq.empty() and dq.front() < i - k + 1)dq.pop_front();
while(!dq.empty() and x >= 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)查询。

我们定义dp[i][j]表示

表示区间的最值,在这道题里我们认为是最大值(维护最小值同理)。看一下能不能开下,大概是maxn * 20的大小,可以开下。

通过定义不难发现,dp[i][0] = a[i],因为此时区间长度为1,那么最值就是元素a[i]本身。当j增大时,我们有转移方程(具体的原因可简单自行推导,以后的文章中也会有讲解):

dp[i][j] = max(dp[i][j - 1], dp[i + 2^(j-1)][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<<k) + 1][k]);
}
vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
memset(st, 0xcf, sizeof st);
vector<int> res;
int n = nums.size();
for(int i = 1;i <= n; ++ i)a[i] = nums[i - 1];
for(int i = 1;i <= n; ++ i)st[i][0] = a[i];
for(int k = 1;k <= 20; ++ k)
{
for(int i = 1;i <= n and i + (1 << (k - 1)) <= n; ++ i)
{
st[i][k] = max(st[i][k - 1], st[i + (1 << (k - 1))][k - 1]);
}
} for(int i = 1;i + k - 1 <= n; ++ i)res.push_back(queMax(i, i + k - 1)); return res;
}
};

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 <= n)
{
t[k] = x;
for(int i = 1;i < lowbit(k); i <<= 1)t[k] = max(t[k], t[k - i]);
k += lowbit(k);
}
} int queMax(int l, int r)
{
int res = a[r];//单点查询
while(l <= r)
{
for(;r - lowbit(r) >= l; r -= lowbit(r))res = max(res, t[r]);
res = max(res, a[r --]);
}
return res;
} vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
n = nums.size();
for(int i = 1;i <= n; ++ i)a[i] = nums[i - 1];
vector<int> res;
for(int i = 1;i <= n; ++ i)
{
update(i, a[i]);
if(i >= k)res.push_back(queMax(i - k + 1, i));
} return res;
}
};

4、多重集合法

这种方法就十分简单粗暴了,就是维护一个不断移动的multiset,简直是暴力之王。

class Solution {
public: vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
multiset<int> st;
vector<int> res;
for(int i = 0;i < k; ++ i)st.insert(nums[i]);
res.push_back(*st.rbegin());
for(int i = k;i < nums.size(); ++ i)
{
st.erase(st.find(nums[i - k]));
st.insert(nums[i]);
res.push_back(*st.rbegin());
} return res;
}
};

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<int> st; void Add(int k)//把a[k]加入到区间内
{
st.insert(a[k]);
} void Del(int k)
{
st.erase(st.find(a[k]));
} vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
n = nums.size();
for(int i = 1;i <= n; ++ i)a[i] = nums[i - 1];
int siz = sqrt(n);
for(int i = 1;i <= n; ++ i)pos[i] = i / siz;
int m = n - k + 1;
for(int i = 1;i <= m; ++ i)q[i].l = i, q[i].r = i + k - 1, q[i].id = i; sort(q + 1, q + 1 + m, [this](const Q &u, const Q &v)
{
return pos[u.l] == pos[v.l] ? u.r < v.r : pos[u.l] < pos[v.l];
});
int L = 1, R = 0;//当前区间
for(int i = 1;i <= m; ++ i)
{
//[L, R] -> [l, r]
int l = q[i].l, r = q[i].r, id = q[i].id;
while(L < l)Del(L ++);
while(L > l)Add(-- L);
while(R > r)Del(R --);
while(R < r)Add(++ R);
ans[id] = *st.rbegin();
}
vector<int> res;
for(int i = 1;i <= m; ++ i)res.push_back(ans[i]);
return res;
}
};

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] < a[v];
}
};
priority_queue<int, vector<int>, cmp> pq;
vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
int n = nums.size();
for(int i = 1;i <= n; ++ i)a[i] = nums[i - 1]; vector<int> res;
for(int i = 1;i <= n; ++ i)
{
pq.push(i);
while(!pq.empty() and pq.top() < i - k + 1)pq.pop();
if(i >= k)res.push_back(a[pq.top()]);
} return res;
}
};

[思维提升|干货All in]6种算法解决LeetCode困难题:滑动窗口最大值的更多相关文章

  1. 利用深搜和宽搜两种算法解决TreeView控件加载文件的问题。

    利用TreeView控件加载文件,必须遍历处所有的文件和文件夹. 深搜算法用到了递归. using System; using System.Collections.Generic; using Sy ...

  2. [干货]Kaggle热门 | 用一个框架解决所有机器学习难题

    新智元推荐 来源:LinkedIn 作者:Abhishek Thakur 译者:弗格森 [新智元导读]本文是数据科学家Abhishek Thakur发表的Kaggle热门文章.作者总结了自己参加100 ...

  3. [DeeplearningAI笔记]卷积神经网络3.1-3.5目标定位/特征点检测/目标检测/滑动窗口的卷积神经网络实现/YOLO算法

    4.3目标检测 觉得有用的话,欢迎一起讨论相互学习~Follow Me 3.1目标定位 对象定位localization和目标检测detection 判断图像中的对象是不是汽车--Image clas ...

  4. 算法入门:最大子序列和的四种算法(Java)

    最近再学习算法和数据结构,推荐一本书:Data structures and Algorithm analysis in Java 3rd 以下的四种算法出自本书 四种最大子序列和的算法: 问题描述 ...

  5. Java利用DES/3DES/AES这三种算法分别实现对称加密

    转载地址:http://blog.csdn.net/smartbetter/article/details/54017759 有两句话是这么说的: 1)算法和数据结构就是编程的一个重要部分,你若失掉了 ...

  6. nignx 负载均衡的几种算法介绍

    负载均衡,集群必须要掌握,下面介绍的负载均衡的几种算法.   1 .轮询,即所有的请求被一次分发的服务器上,每台服务器处理请求都相同,适合于计算机硬件相同.   2.加权轮询,高的服务器分发更多的请求 ...

  7. [BS-28] iOS中分页的几种算法

    iOS中分页的几种算法 总记录数:totalRecord 每页最大记录数:maxResult 算法一: totalPage = totalRecord % maxResult == 0 ? total ...

  8. TCP控制拥塞的四种算法:慢开始,拥塞避免,快重传,快恢复

    我们在开始假定: 1:数据是单方向传递,另一个窗口只发送确认. 2:接收方的缓存足够大,因此发送方的大小的大小由网络的拥塞程度来决定. 一:慢开始算法和拥塞避免算法 发送方会维持一个拥塞窗口,刚开始的 ...

  9. 提升PHP性能的21种方法

    提升PHP性能的21种方法. 1.用单引号来包含字符串要比双引号来包含字符串更快一些.因为PHP会在双引号包围的字符串中搜寻变量,单引号则不会.2.如果能将类的方法定义成static,就尽量定义成st ...

  10. 从零开始学C++之STL(四):算法简介、7种算法分类

    一.算法 算法是以函数模板的形式实现的.常用的算法涉及到比较.交换.查找.搜索.复制.修改.移除.反转.排序.合并等等. 算法并非容器类型的成员函数,而是一些全局函数,要与迭代器一起搭配使用. 算法的 ...

随机推荐

  1. ansible使用笔记:长期更新

    ansible使用笔记 ##将/home/system.sh文件拷贝到所有服务器的/home/system.sh ansible all -m copy -a "src=/home/syst ...

  2. OO课程第三阶段(实验和pta试题)总结Blog3

    OO课程第三阶段(实验和pta试题)总结Blog3 前言:学习OOP课程的第三阶段已经结束了,在此进行对于知识点,题量,难度的个人看法. 学习OOP课程的第三阶段已经结束了,较第一次阶段学习难度加大, ...

  3. 看懂插入才算看懂redis跳表

    跳表的数据结构 forward相当于9之于8,12之于8 插入算法流程: 1.初始化两个数组update和rank数组:update表示每一层的前继节点.rank表示update节点到头结点的距离(用 ...

  4. Cortex-M内核指令WFI和WFI

    背景 今天阅读Nordic SDK里的example时发现里面功耗管理使用的power_manage()函数主要就是调用了一个__WFE()函数.查阅资料了解到WFE是一条汇编指令,它可以让CPU内核 ...

  5. oracle导入dmp

    通过impdp导入 1.sqlplus       (连接oracle数据库) 2.输入用户名密码3.create user abc identified by 123456;   (创建用户名为ab ...

  6. input类型为number仍可以输入e,+,-解决方法

    <input type="number" onKeypress="return(/[\d\.]/.test(String.fromCharCode(event.ke ...

  7. Ubuntu与centos之间的文件传输

          Ubuntu与centos之间的文件传输 默认端口:22 传输前确认端口是否开启 1.在Ubuntu上安装scp命令 apt-get install openssh-server 2.将U ...

  8. C/S 架构 和 B/S 架构

    C/S架构的理解: 官方称:clinet-server 客户端需要下载的软件: 今日头条,爱奇艺等  能在手机和浏览器打开的软件 B/S架构的理解: 官方称:web-server 客户端为浏览器的: ...

  9. Manage your references to .Net assemblies Dynamics 365 for Operations VS projects

    (Dynamics 365 for Operations was previously known as the New Dynamics AX) Dynamics 365 for Operation ...

  10. 让VMWARE ESXI 虚拟机开机进入BIOS设置

    VMware vSphere Client 里面选中需要从光驱启动的客户机,单击右键选择 "编辑设置", 在虚拟机属性的窗口里面选择 "选项 "选项卡 ,单击& ...