Day1 单调数据结构

提高1/2 单调数据结构


基本

单调数据结构,顾名思义具有单调性,比如说递增/递减。单调栈和单调队列秉持着如果一个人比你小,还比你强,那你就没用了的原则。

单调栈

以上午的模板题为例:

给出项数为 nnn 的整数数列 a1…na_{1\dots n}a1n,定义函数 f(i)f(i)f(i) 代表数列中第 iii 个元素之后第一个大于 aia_iai 的元素的下标,即 f(i)=min⁡j (i<j≤n,aj>ai)f(i)=\min{j}\ (i<j\le n,a_j>a_i)f(i)=minj (i<jn,aj>ai)。若不存在,则 f(i)=0f(i)=0f(i)=0

开一个单调栈,内部值单调递减;如果新进来一个值 xxx,当前栈顶 top<xtop<xtop<x,则说明 xxx 就是 toptoptop 后第一个比自己大的元素,并将其弹出;否则 toptoptop 早就被计算答案后弹出了。重复这个过程直至栈空或 top≥xtop \ge xtopx 后将 xxx​ 压入栈中。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e6 + 5;
int n,top,ans[maxn]; pair<int,int> st[maxn];
int main() {
    scanf("%d",&n);
    for (int i = 1,x;i <= n;i ++) {
        scanf("%d",&x);
        while (top && st[top].first < x)
            ans[st[top].second] = i, top --;
        st[++ top] = {x,i};
    }
    for (int i = 1;i <= n;i ++)
        printf("%d\n",ans[i]);
    return 0;
}

单调队列

对于单调队列相对复杂一些。仍以课上例题(简化后)为例:

给定一个长度为 nnn 的数组 aaa,求其中每个长度为 kkk 的区间中的最大值。

开一个单调队列,内部值单调递减;假设当前区间为 [l,l+k−1][l,l + k - 1][l,l+k1],则这个队列中队头小于 lll 的元素对现在和以后的答案都不会产生影响,将它们出队;对于上一个区间 [l−1,l+k−2][l-1,l+k-2][l1,l+k2] 而言多的元素为 al+k−1a_{l+k-1}al+k1,如果队尾 tail<al+k−1tail<a_{l+k-1}tail<al+k1,因为 tailtailtaill+k−1l+k-1l+k1 更加靠前,而值却不如后者大,则 tailtailtail 就不会再影响后面的答案了,将 tailtailtail 出队。重复这个过程直至队空或 tail<al+k−1tail<a_{l+k-1}tail<al+k1 后将 al+k−1a_{l+k-1}al+k1 入队。此时 [l,l+k−1][l,l+k-1][l,l+k1] 中的最大值即为队头 headheadhead(单调递减)。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e6 + 5;
int a[maxn],n,k,head = 1,tail = 0,q[maxn];
int main() {
    scanf("%d%d",&n,&k);
    for (int i = 1,x;i <= n;i ++) 
        scanf("%d",&a[i]);
    for (int i = 1;i <= n;i ++) {
        while (head <= tail && q[head] < i - k + 1) head ++;
        while (head <= tail && a[q[tail]] < a[i]) tail --;
        q[++ tail] = a[i];
        if (i >= k) printf("%d ",q[head]);
    }
    return 0;
}

上午

【模板】单调栈

如上所述。

动物园的等待

很显然如果 xxxyyy 高,且在 yyy 的后面,则 xxx 后的人都无法看到 yyy 。所以考虑维护一个单调栈,栈中人的高度单调递减。每次进来一个人,其身高为 hhh ,如果栈顶 top<htop<htop<h ,那么只有这两个人是互相看得到的,而后面的人都会因为 hhh 挡住而看不到 toptoptop,于是答案加一并将 toptoptop 弹出;重复操作直到栈空或栈顶小于 hhh ,此时如果栈不空,则这个人和 toptoptop 是可以互相看到的,答案也加一。然后将 hhh 压入栈中。

但细节部分还要考虑一种特殊情况:身高相等。身高相等的人互相都能看得到,而这些人可以对答案产生人数总和的贡献。所以可以把一群相邻的身高相等的一群人看作一个整体。令一群人共有 sizsizsiz 个人,如果他们被出栈了,则可以对答案产生 sizsizsiz 的贡献;而如果一个人被压入栈中后发现,栈中前一个元素是与自己身高相同的一群人(有 sizsizsiz 个人),则能对答案产生 sizsizsiz 的贡献。

可是如果发现自己前面是一群人但身高更高,则只能产生 111 的贡献(因为两人中间的任何人都不能更高,也就是自己只能看到这群人中离自己最近的那个人)。

雨点

O(nlog⁡n)O(n\log n)O(nlogn) 做法

二分花盆长度 WWW ,然后把雨点按照 xxx 坐标排序,则 check 的部分即为求长度为 WWW 的区间中最大值与最小值之差,并判断是否大于等于 DDD 即可。用单调队列滑动窗口判最大 & 最小直接解决。

O(n)O(n)O(n) 做法

考虑枚举并固定花盆左端点 LLL,从小到大枚举右端点 RRR,单调队列维护 [L,R][L,R][L,R] 中雨滴 yyy 坐标的最大/小值;如果发现某一时刻将 RRR 加入单调队列中后,最大值减最小值大于等于 DDD 了,即满足了题目的条件,则此时 [L,R][L,R][L,R] 就是一个可能的答案;可此时如果用原来的 LLL 继续把 RRR 向后移,雨滴的最早/最晚落下的时间只会越来越长(不会更优),花盆长度也会越来越长。

假设当前左端点为 LLL,发现一个可行的右端点 RRR,那么对于将 L+1L+1L+1 作为左端点,其右端点只会 >R> R>R(感性理解一下,否则当左端点为 LLL 时直接选这个更靠前的 RRR 作为右端点会更优)。然后时间复杂度就降到了 O(n)O(n)O(n)L,RL,RL,R 只会向右移。

// 核心部分,q 单调递增,q1 单调递减。
for (int L = 1;L <= n && R <= n;L ++) {
    while (l <= r && a[q[l]].x < a[L].x) l ++;
    while (l1 <= r1 && a[q1[l1]].x < a[L].x) l1 ++;
    for (;R <= n && a[q1[l1]].y - a[q[l]].y < d;R ++) {
        while (l <= r && a[q[r]].y >= a[R].y) r --;
        while (l1 <= r1 && a[q1[r1]].y <= a[R].y) r1 --;
        q[++ r] = q1[++ r1] = R;
    }
    if (a[q1[l1]].y - a[q[l]].y >= d)
        ans = min(ans,a[R - 1].x - a[L].x);
}

jxcakak

这题用二维线段树会 TLE70pts

单调队列做法即为:把每一行以每个点为起点,长度为 nnn 的区间中的最大/小值先逐个用单调队列算下来;然后在列上再用单调队列去滑。码量特别大。所以不如二维st表,然后我就这么过了

泽泽在英国

显然如果以 iii 号建筑物的高度 hih_ihi 为矩形的宽,则矩形的长即为 iii 左右两边最近的比 hih_ihi 小的建筑物之间。然后就可以用单调栈维护了。可以两遍单调栈,也可以跑完一次后照着栈中剩余元素算出另一边。

下午

仓鼠窝

如果我们固定完整矩阵的右下角,则能产生贡献的矩阵会受到附近 000 的影响;而如果靠左的 000 比靠右的 000 更高,则这个靠左的 000 就不会成为决策点(当前不影响答案,毕竟矩形不能拐弯)。所以我们固定一个右下角,用单调栈维护比该点所在行高的,会对答案产生影响的 000 的位置。设当前右下角为 (u,v)(u,v)(u,v),一个可用的左上角为 (x,y)(x,y)(x,y),则这个新矩阵对答案产生的贡献即为 (x−u)×(y−v)(x-u)\times(y-v)(xu)×(yv)。当然 (u,v)(u,v)(u,v) 也可以直接继承在栈中自己下面那个点的答案,可以自行画图理解。

消防

~~根据课上的暴论,~~最终的链一定在树的直径上。因为对于树上的任意一点,距离其最远的点一定是直径上的点。所以就可以在直径上把长度 ≤s\le ss 的区间(链)用单调队列枚举。

考虑选定的一个区间(一条链),如何求其他所有点到这条链。我们设直径为 u→vu\to vuv,当前枚举的链为 [L,R][L,R][L,R]

  • 对于直径上的点:显然 uuuvvv (直径的两端)到达 [L,R][L,R][L,R] 的距离是直径上点到 [L,R][L,R][L,R] 的距离中最大的。可以用前缀和去维护这个两端的距离。
  • 对于直径外的点:只有该点到 [L,R][L,R][L,R] 上的某点 xxx 的距离,是所有直径外的点到 xxx 的距离中最大的时,该点才会可能对答案产生影响。可以先算出每个直径上的点距离其最远的点的距离 disdisdis,然后开一个单调队列维护 [L,R][L,R][L,R]disdisdis 的最大值。
  • 本轮答案即为上两种情况取较大值。

所以先跑出直径上的点(两遍 dfs 并记录每个直径点的父亲,后面滑动窗口时方便);然后一遍 bfs 算出每个直径点在不经过其他直径点的情况下,能到的最远的点的距离;最后在直径上滑动窗口,如果 [L,R][L,R][L,R] 中的距离(同样用前缀和)合法,那么就可以计算答案。

小奇探险

假设从前往后一次拿了 ttt 个宝箱 w1…tw_{1\dots t}w1t,则最终得到的价值即为:
w1+(w2×k)+(w3×k2)+⋯+(wt×kt−1) w_1+(w_2\times k)+(w_3\times k^2)+\dots+(w_t\times k^{t-1}) w1+(w2×k)+(w3×k2)++(wt×kt1)
此时如果我们从 wtw_twt 从后往前推,设当前总价值为 sumsumsum,则在拿到 wtw_twtsum←wtsum\gets w_tsumwt,但拿到 wt−1w_{t-1}wt1 时,我们就知道了 wtw_twt 之前至少拿了 111 个宝箱,所以 wtw_twt 需要乘上一个 kkk,再加上 wt−1w_{t-1}wt1,即 sum←sum×k+wt−1sum\gets sum\times k+w_{t-1}sumsum×k+wt1。以此类推,当我们每从后往前拿一个宝箱时,sumsumsum 都需要乘上 kkk 再加上该宝箱的价值。

于是我们设 fif_ifi 表示当前从后往前扫到了第 iii 个宝箱时的最大价值,转移方程即为
fi=max⁡{fj×k+ai} (i<j≤i+m) f_i=\max\{f_j\times k+a_i\}\ (i<j\le i+m) fi=max{fj×k+ai} (i<ji+m)
可以发现 k,aik,a_ik,ai 均为定值,可以用单调队列维护出 (i,i+m](i,i+m](i,i+m]fjf_jfj 的最大值。然后就没了。

股票交易

状态即为 fi,jf_{i,j}fi,j 表示第 iii 天持有 jjj 张股票。初始状态即为买多少花多少,不考虑前面的转移(其他买不到的为负无穷)。

转移分为三种情况:

  • 啥都不干,即 fi,j←fi−1,jf_{i,j}\gets f_{i-1,j}fi,jfi1,j
  • 之前交易过,现在买到了 jjj 张:从 (i−w−1)(i - w - 1)(iw1) 天前,且那天持有 kkk 张股票,进行转移,收益为 −(j−k)∗ap-(j - k) * ap(jk)ap,其中 j−as≤k<jj - as \le k < jjask<j。方程即为 fi,j=max(fi−w−1,k−(j−k)∗ap)f_{i,j} = max(f_{i-w-1,k} - (j - k) * ap)fi,j=max(fiw1,k(jk)ap)
  • 之前交易过,现在卖到了 jjj 张:从 (i−w−1)(i - w - 1)(iw1) 天前,且那天持有 kkk 张股票,进行转移,收益为 (k−j)∗bp(k - j) * bp(kj)bp,其中 j<k≤j+bsj < k \le j + bsj<kj+bs。方程即为 fi,j=max(fi−w−1,k+(k−j)∗bp)f_{i,j} = max(f_{i-w-1,k} + (k - j) * bp)fi,j=max(fiw1,k+(kj)bp)

后两种情况方程可以转化为可以单调性优化的形式:

  • fi,j=max⁡(fi−w−1,k+k∗ap)−j∗ap (j−as≤k<j)f_{i,j} = \max(f_{i-w-1,k} + k * ap) - j * ap\ (j - as \le k < j)fi,j=max(fiw1,k+kap)jap (jask<j)
  • fi,j=max⁡(fi−w−1,k+k∗bp)−j∗bp (j+bs≥k>j)f_{i,j} = \max(f_{i-w-1,k} + k * bp) - j * bp\ (j + bs \ge k > j)fi,j=max(fiw1,k+kbp)jbp (j+bsk>j)

2,3 的转移顺序是相反的(状态之间的依赖关系相反)。

国旗计划

其实这题不需要用到单调数据结构

首先破环成链。因为各个区间不包含,所以在按照区间左端点排序后,右端点也是单调的。然后就可以用双指针求出每个区间后接的最优的区间。在知道每个区间的下一个区间后,就可以用倍增思想加速在环上跳的过程。对于 [L,R][L,R][L,R] 这个区间必须选的情况下(已经破环成链),结束条件即为右端点 ≥L+m\ge L+mL+m。答案记得 +2+2+2​。

课后题

排序游戏

因为所有操作的左端点都固定为 111,所以先操作的小区间会被后操作的大区间覆盖。所以我们用一个单调栈把无效操作全部去除,最终栈里剩下从上至下右端点单调递减的操作。没被操作过的部分不变,将原序列 aaa 从小到大排序。我们按照栈中操作从下至上(也就是右端点逐渐变大)进行处理。每次就可以固定前后两个操作之间的值。

// 核心代码
for (int i = st[1].second + 1;i <= n;i ++) ans[i] = a[i]; // 没有产生变化的区间
nowr = st[1].second; // 上一个操作右端点
sort(a + 1,a + st[1].second + 1); 
for (int i = 1;i <= st[1].second;i ++) q[++ r] = a[i];
for (int i = 2;i <= top;i ++) {
    if (st[i - 1].first == 1) { // 这一段是升序,从头取
        for (int j = nowr;j > st[i].second;j --)
            ans[j] = q[r --];
    } else { // 这一段是降序,从尾取
        for (int j = nowr;j > st[i].second;j --)
            ans[j] = q[l ++];
    }
    nowr = st[i].second;
}
for (int i = nowr;i > 0;i --) // 最后剩下的
    ans[i] = q[st[top].first == 1 ? r -- : l ++];

信号传输/魔王降临

开了题解视频了所以偷个懒

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值