算法竞赛刷题笔记 iv

本文详细介绍了单调队列这一数据结构在算法竞赛中的应用,通过分析多个具体的题目,如POJ 2823 sliding window、CH1201 最大子序和等,展示了如何利用单调队列实现线性时间复杂度的解决方案。文章对比了单调队列与ST算法的差异,并探讨了单调队列在处理滑动窗口最大值等问题上的优势。
摘要由CSDN通过智能技术生成

单调队列的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 ji<=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(1ijnsjsi)
固定右边的下标 j j j ( j − m , j ] (j-m,j] (jm,j]的最大,相当于一个窗口里求某个值,不同的窗口对应不同的值,如何从 ( j − m , j ] (j-m,j] (jm,j]的信息来求 ( j − m + 1 , j + 1 ] (j-m+1,j+1] (jm+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] (im,i]的所有值的和为 s [ i ] − s [ i − m ] s[i] - s[i-m] s[i]s[im] i-m并没有超界
用时
128 ms
占用内存
1408 KiB
代码长度
659 B

Luogu P1725 琪露诺在这里插入图片描述

这个题到达对岸的条件是下一步的位置编号大于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(nl),f(nl1),...,f(nr))+第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(nl),f(nl1),...,f(nr)) 就像一个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] [ir,il]
第二、有些格子是永远到不了的,比如 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(nr+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<djpipj
那么 ( 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<djpi<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中

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值