单调队列的part i
POJ 2823 sliding window
我的想法: 先只考虑最大值,如果滑动窗口的大小为1,那么每个数都是窗口的最大值。如果滑动窗口为2,如何通过上一个窗口来获得当前的窗口的信息,如(a b) c–>a (b c) 当前窗口为(b c),假设上一个窗口的最大值为b,那么我们可以用上上个窗口的最大值来作为信息,如果b不是最大值,那么我们必须要用的b,窗口为2看不出有什么信息加速的功能。当窗口为3 (a b c) d —> a (b c d) 如果最大值在b或者c 那么将(a b c)的最大值与d进行比较就能得到(b c d)的最大值 如果a是最大值,那么我们需要得到b c的最大值,而不需要比较b c,那么我们就可以减少比较从而达到加速效果。想到的方法一 是全局记录最大的两个数,当最大值离开窗口范围,那么第二大的值就补上,问题是第三大的值在哪里找?方法一的失败 得到了记录的数据结构应不是定长的。
数据结构有的特点是1、能找到最大值,并且2、能找到超出范围的值,而且3、能尽快的找到下一个最大的值,CASE A:如果是堆,1,3都能满足,但是2满足的需要在每个数那里记录序号,当序号超界直接弹出,找到下个最大值的时间需要O(logn),插入一个新元素O(logn)。CASE B:如果是单调数组,1,3都能满足,2满足也是需要记录序号,找到下一个最大值需要O(1),如果插入一个新元素也只需要O(1)的时间,那么这个解就是线性,也是最优解,如果每次插入后排序,那么不能O(1),因为要有序,所以剩下的情况只有将数组中一部分值永久删除来达到排序效果,现在就考虑是否队列中有一部分值是当元素插入后永远用不到的,只有两种可能,一种是大于插入元素,一种小于插入元素,大于肯定不能删,那么就考虑小于的,对于序号i<j 但num[j] > num[i] 那么在j之后i永远用不到,这个永远用不到的性质成立! 代码走起:
11592K 5625MS
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
using namespace std;
const int N = 1000007;
int p[N],q[N],num[N];
int main()
{
int n,k,ql=1,qr=0,pl=1,pr=0;
vector<int> ans_min,ans_max;
scanf("%d%d",&n,&k);
for(int i = 1;i <= n;i++){
scanf("%d",&num[i]);
while(ql<=qr && q[ql] <= i-k) ql++;//弹出超过范围的最大值
while(ql<=qr && num[q[qr]] <= num[i]) qr--;//保持单调队列进行插入,若a_i < a_i+1 那么在包含i+1的窗口,a_i永远用不上
q[++qr] = i;//把索引i 入队
while(pl<=pr && p[pl] <= i-k) pl++;
while(pl<=pr && num[p[pr]] >= num[i]) pr--;
p[++pr] = i;
if(i>=k){//当索引到k时,第一个窗口出现,每次将队首元素加入相应vector中
ans_min.push_back(num[p[pl]]);
ans_max.push_back(num[q[ql]]);
}
}
for(int i=0;i<ans_min.size();i++)
printf("%d ",ans_min[i]);
puts("");
for(int i=0;i<ans_min.size();i++)
printf("%d ",ans_max[i]);
puts("");
return 0;
}
发现了一个神奇的事,把p[N],q[N],num[N]放入到main中的话,程序会爆掉,放到全局变量位置就没事
队列q[ql] q[ql+1] … q[qr] 满足 num[q[ql]] > num[q[qr]] 且 q[ql] < q[qr]
队列p[pl] p[pl+1] … p[pr] 满足 num[p[pl]] < num[p[pr]] 且 p[pl] < p[pr]
队列q的队头是q[ql] 记录的是最大值 队列q的队头是p[pl] 记录的是最小值
无论是对于p还是q
第一步是 将过期了的最大值弹出(即索引在(i-m,i]之外的值)
第二步是 删除队列相应永远不会用到得部分
第三步是 入队
时间复杂度为O(n) 因为每个元素最多入队两次 出队两次
光盘里给的代码 8028K 5563MS
另外对于ST算法与这个算法的区别,ST算法是求数列在下标L–R中最大值,ST算法是一个在线算法,时间复杂度为O(nlogn) 单调队列算法不同的是单调队列是遍历 不需要把值存起来,而ST算法需要保存数组的信息
所以单调队列可以到达O(n)时间复杂度
CH1201 最大子序和
用前缀和数组,题目求得就是 满足
j
−
i
<
=
m
j - i<=m
j−i<=m的
i
i
i和
j
j
j使
m
a
x
(
∑
1
≤
i
≤
j
≤
n
s
j
−
s
i
)
max(\sum_{\newline{1\le i\le j\le n}} s_{j} - s_{i})
max(∑1≤i≤j≤nsj−si)
固定右边的下标
j
j
j 求
(
j
−
m
,
j
]
(j-m,j]
(j−m,j]的最大,相当于一个窗口里求某个值,不同的窗口对应不同的值,如何从
(
j
−
m
,
j
]
(j-m,j]
(j−m,j]的信息来求
(
j
−
m
+
1
,
j
+
1
]
(j-m+1,j+1]
(j−m+1,j+1]对应的值,每个窗口都是最右边的值s[j]减去窗口里面的最小值,这就相当于求最小值,那么这里就可以用上一题的方法,代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
using namespace std;
const int N = 300007;
int s[N],q[N];
int main()
{
int n,m,l=1,r=1,ans = INT_MIN;
scanf("%d%d",&n,&m);
s[0] = 0,q[0] = 0;
for(int i=1;i<=n;i++){
scanf("%d",&s[i]);
s[i] += s[i-1];
}
for(int i=1;i<=n;i++){
while(l<=r && q[l] < i-m) l++;
ans = max(ans,s[i] - s[q[l]]);
while(l<=r && s[q[r]]>=s[i]) r--;
q[++r] = i;
}
printf("%d\n",ans);
return 0;
}
注意这里是q[l] < i - m 而上面一题是q[ql] <= i - k,这里不取等号的原因在于
(
i
−
m
,
i
]
(i-m,i]
(i−m,i]的所有值的和为
s
[
i
]
−
s
[
i
−
m
]
s[i] - s[i-m]
s[i]−s[i−m] i-m并没有超界
用时
128 ms
占用内存
1408 KiB
代码长度
659 B
Luogu P1725 琪露诺![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b5e8a7055e1d70e696993b0ceb2a796b.png)
这个题到达对岸的条件是下一步的位置编号大于N,也就是说不管怎么跳 一定能跳到对岸
这个题属于状态类型的 在位置n的某个状态 是取决于位置n前面的一些位置的状态
令
f
(
n
)
f(n)
f(n)为踩到的第n个格子时 能获得的最大冰冻指数,那么
f
(
n
)
=
m
a
x
(
f
(
n
−
l
)
,
f
(
n
−
l
−
1
)
,
.
.
.
,
f
(
n
−
r
)
)
+
f(n) = max(f(n-l),f(n-l-1),...,f(n-r)) +
f(n)=max(f(n−l),f(n−l−1),...,f(n−r))+第n个格子的冰冻指数
如果直接打表时间复杂度就为O(n(r-l))
注意到这个
m
a
x
(
f
(
n
−
l
)
,
f
(
n
−
l
−
1
)
,
.
.
.
,
f
(
n
−
r
)
)
max(f(n-l),f(n-l-1),...,f(n-r))
max(f(n−l),f(n−l−1),...,f(n−r)) 就像一个sliding window
有几个问题:
第一、更新到
f
[
i
]
f[i]
f[i]的时候
f
[
i
]
f[i]
f[i]并不能直接入队,因为生成
f
[
i
+
1
]
f[i+1]
f[i+1]的过程
f
[
i
]
f[i]
f[i]是不参加的,即位置为i的时候在队列里的只有
[
i
−
r
,
i
−
l
]
[i-r,i-l]
[i−r,i−l]。
第二、有些格子是永远到不了的,比如
L
=
4
L=4
L=4
R
=
5
R=5
R=5时
f
[
1
]
f
[
6
]
f[1] f[6]
f[1]f[6]是永远到不了的!
对于第二个问题 我的想法是对于到不了的位置用INT_MIN来表示
对于第一个问题就是每次到i的时候用i-l进队
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
using namespace std;
const int N = 200007;
int a[N],q[N],f[N];
int main()
{
int n,l,r,head = 1,tail = 0;
scanf("%d%d%d",&n,&l,&r);
for(int i=0;i<=n;i++){
scanf("%d",&a[i]);
if(i < l){f[i] = i==0 ? 0 : INT_MIN;continue;}
while(head <= tail && q[head] < i - r) head++;
while(head <= tail && f[q[tail]] <= f[i-l]) tail--;
q[++tail] = i-l;
f[i] = f[q[head]] + a[i];
}
while(head <= tail && q[head] < n+1-r) head++;//弹出队列中不能一步到达n+1位置的成员
for(int i=n+1;i<=n+l;i++){
while(head <= tail && f[q[tail]] <= f[i-l]) tail--;//更新队列,能一步跳到[n+1,n+l]范围的
q[++tail] = i-l;
}
printf("%d\n",f[q[head]]);
return 0;
}
while和for的那个代码实际上就是求的 f ( n − r + 1 ) . . . f ( n ) f(n-r+1)...f(n) f(n−r+1)...f(n)的最大值 不需要用我这种写法,直接按下面这样就OK了
int maxn=INT_MIN;
for(int i=n-r+1;i<=n;i++)
maxn=max(maxn,dp[i]);
Luogu P3088 Crowded Cows
做到这里感觉到单调队列一个比较明显的标志 那就是窗口!
注意这个题里面 一个牛觉得crowded是当它左边和右边 都有2倍高度的牛时
一开始不从单调队列开始分析,暴力来解决这道题,就是遍历每头牛,每次遍历的时候,拿当前牛与其它牛进行高度比较,时间复杂度就是
O
(
n
2
)
O(n^2)
O(n2)
减少时间复杂度的思想就是用过去得到的信息来加速,对于basic case 即第一头牛,我如何知道第一头牛是是不是拥挤,我必须遍历所有的牛,因为最后一头牛可能会使第一头牛感到拥挤,那么我第一次遍历到第二头牛的时候 我是不知道第一头牛的情况,这个时候是加不了速的
我就想先按x(i)进行排序,从右到左扫一遍 得出每头牛的左边d范围内是否有高度为其2倍的牛,再从右边扫一遍,得出每头牛的右边d范围内是否有高度为其2倍的牛。无论是从左到右还是从右到左
其核心是求窗口大小为d的窗口里面的最大值是否为当前值的两倍!! 这就是个sliding window
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
using namespace std;
const int N = 50004;
typedef pair<int,int> P;
P a[N];
int q[N],flag[N];
int comp(const void* a,const void* b){
return ((P*)a)->first > ((P*)b)->first;
}
int main()
{
int n,d,x,h,l=1,r=0,cnt=0;
scanf("%d%d",&n,&d);
memset(flag,0,n*sizeof(int));
for(int i=0;i<n;i++){
scanf("%d%d",&x,&h);
a[i].first = x;
a[i].second = h;
}
qsort(a,n,sizeof(P),comp);
for(int i=0;i<n;i++){
while(l<=r && a[q[l]].first < a[i].first - d) l++;
if(l <= r && a[q[l]].second >= 2*a[i].second){flag[i]++;if(flag[i]==2)cnt++;}
while(l<=r && a[q[r]].second <= a[i].second) r--;
q[++r] = i;
}
l = 1,r = 0;
for(int i=n-1;i>=0;i--){
while(l<=r && a[q[l]].first > a[i].first + d) l++;
if(l <= r && a[q[l]].second >= 2*a[i].second){flag[i]++;if(flag[i]==2)cnt++;}
while(l<=r && a[q[r]].second <= a[i].second) r--;
q[++r] = i;
}
printf("%d\n",cnt);
return 0;
}
注意qsort里面的需要的函数comp 如果comp返回值大于0 那么将第2个值放到第1个值的前面,所以comp中
return ((P*)a)->first > ((P*)b)->first;
return ((P*)a)->first - ((P*)b)->first;//这两个语句是等价的
flag是为每头牛的一个标记,当flag为2 说明这个牛左右两边都有身高为其两倍的牛,当flag为1时说明左边或右边有身高为其两倍的牛,qsort后面两个for循环分别是从左到右和从右到左遍历,一开始都是弹出超界的队首,然后进行判断队首的牛身高是否大于2倍的当前牛的身高,最后将这头牛插到队列的正确位置。
Luogu P1714 切蛋糕
这道题就是最大子序和的那道,咱们跳过
Luogu P2949 Work Scheduling
一开始看这道题,觉得这道题有点奇怪,这不就是求每个时刻对应的最大值,然后把这些最大值加起来吗?
想了一下 可能是D_i的范围有点大,会超时,要用N来解决,我的想法就是按D_i进行排序 然后输出每个D_i中最大的P_i 时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 我的代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
using namespace std;
const int N = 1000004;
typedef long long ll;
typedef pair<ll,ll> P;
P a[N];
int comp(const void* a,const void* b){
return ((P*)a)->first > ((P*)b)->first;
}
int main()
{
int n;
ll ans = 0,mx = 0,cur = -1;
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d%d",&a[i].first,&a[i].second);
qsort(a,n,sizeof(P),comp);
for(int i=0;i<n;i++){
if(a[i].first != cur){
cur = a[i].first;
ans += mx;
mx = a[i].second;
}
else
mx = max(mx,a[i].second);
}
ans += mx;
printf("%lld\n",ans);
return 0;
}
通过这个题 我发现了有的代码是通过读取文件的形式来读输入,像这道题的输入有10000行 如果用粘贴复制的方式来输入这些数据会很花时间
错误原因:
当工作是 2 3 和 2 4的时候 我的算法是只做2 4的这个 但实际上2 3和2 4都能做!!
这个我搜了半天,没找到和我错的一样的错误分析,后来终于自己发现了错因!!
如果这题强行按单调队列的思路来走,由之前总结出的单调队列的明显特征:固定的窗口,但是这道题没有这个特征
通过分析序列的特点来看是否能起到加速作用,对于任意的
(
d
i
,
p
i
)
和
(
d
j
,
p
j
)
若
d
i
<
d
j
且
p
i
≥
p
j
(d_i,p_i)和(d_j,p_j) 若d_i<d_j 且 p_i \ge p_j
(di,pi)和(dj,pj)若di<dj且pi≥pj
那么
(
d
i
,
p
i
)
(d_i,p_i)
(di,pi)肯定在
(
d
j
,
p
j
)
(d_j,p_j)
(dj,pj)前面使用,若
d
i
<
d
j
且
p
i
<
p
j
d_i<d_j 且 p_i < p_j
di<dj且pi<pj 那么
(
d
i
,
p
i
)
(d_i,p_i)
(di,pi)在
(
d
j
,
p
j
)
(d_j,p_j)
(dj,pj)的后面的情况一定是
(
d
i
,
p
i
)
和
(
d
j
,
p
j
)
(d_i,p_i)和(d_j,p_j)
(di,pi)和(dj,pj)不能同时使用,什么时候不能同时使用,就是小于
d
j
d_j
dj的
(
d
i
,
p
i
)
(d_i,p_i)
(di,pi)的个数大于
d
j
d_j
dj 当存在这样的(d_j,p_j)不能同时使用时,应该踢掉前面所有中最小p值的
(
d
i
,
p
i
)
(d_i,p_i)
(di,pi)
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
using namespace std;
const int N = 1000004;
typedef long long ll;
typedef pair<ll,ll> P;
P a[N];
int comp(const void* a,const void* b){
return (((P*)a)->first > ((P*)b)->first) || ( ((P*)a)->first == ((P*)b)->first && ((P*)a)->second > ((P*)b)->second);
}
int main()
{
int n,time = 1;
ll ans = 0;
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%lld%lld",&a[i].first,&a[i].second);
qsort(a,n,sizeof(P),comp);
priority_queue<int,vector<int>,greater<int> > pq;
for(int i=0;i<n;i++){
if(time<=a[i].first){
ans+=a[i].second;
pq.emplace(a[i].second);
time++;
}
else if(pq.top() < a[i].second){
ans += a[i].second - pq.top();
pq.pop();
pq.emplace(a[i].second);
}
}
printf("%lld\n",ans);
return 0;
}
洛谷里面 不支持emplace函数 要将emplace改成push
先对a排序
用pq来存time时间内所选择的所有工作,当小于
d
j
d_j
dj的
(
d
i
,
p
i
)
(d_i,p_i)
(di,pi)的个数大于
d
j
d_j
dj 即a[i].first大于time的时候 就看已选择工作中是否存在价值比a[i].second低的 低的就将其替换,若不是这种情况,即time <= a[i].first 那么直接将a[i].second 加入到pq中