0%

线段树维护单调栈

有一个序列,序列中的每一个元素有两个值$h_i$与$val_i$。你需要回答若干次询问,每一次询问针对于一个区间$[l,r]$,设$l\le p_1<p_2\cdots p_k\le r$,其中$p_k$是$[l,r]$中$h_i$最大的位置,$p_{k-1}$是$[l,p_k-1]$中$h_i$最大的位置……你需要回答出这些位置的$val_i$的某些可以区间合并的信息(和/最大最小值……)。

可以对序列中元素的$h_i,val_i$进行修改。


序列中一个区间的$p_1,p_2\cdots p_k$(我们暂且称这个东西为单调栈吧),和整个序列的单调栈,相比起来唯一的区别,就是这个区间的单调栈中最左边的若干个元素(甚至有可能是所有的元素),可能在原序列中由于在这个区间的左边有更大的元素而被弹掉了。

设$query(l,r,p)$表示对于区间$[l,r]$,我们在它的单调栈左边加入$p$之后,会得到的单调栈信息。设$mxh(l,r)$表示$[l,r]$中$h_i$的最大值。

1)假设$mxh(l,mid)<p$,那么我们需要的信息是$query(mid+1,r,p)$。
2)否则,我们需要的信息是$query(l,mid,p)+query(mid+1,r,mxh(l,mid))$。

注意到这里的$query(mid+1,r,mxh(l,mid))$和$p$并没有任何关系,所以我们在线段树的每个节点上存下来就可以了。这样对线段树上的一个完整区间(线段树上有一个节点恰好表示这个区间)查询的复杂度是$T(n)=T(n/2)+O(1)=O(\log n)$的。

然而我们最终要查询的区间不一定是一个完整的区间。它在线段树上会被划分成$O(\log n)$的完整的区间。我们按照从左到右的顺序依次处理这些区间。对于第一个区间我们的$p$取$0$(比所有的$h_i$都要小的值),对于第$i$个区间,我们取第$i-1$个区间我们取的$p$和第$i-1$个区间的$maxh$两者中的较大值。这样查出来的信息的和就是整个区间的单调栈的信息。查询每个完整区间的复杂度是$O(\log n)$,所以查询的总复杂度是$O(log^2 n)$。

对于单点的修改,直接暴力改就可以了。然后我们需要重新计算$O(\log n)$个区间的$query(mid+1,r,mxh(l,mid))$。因此,修改的复杂度是$O(\log ^2 n)$的。


模板与例题

jzoj5402 God knows

有一个$1$到$n$的排列$p_1,p_2\cdots p_n$。平面内有$2n$个点,它们(像二分图一样)排成两排。第一排的点的坐标是$(1,0),(2,0)\cdots (n,0)$,第二排的点的坐标是$(1,1),(2,1),\cdots (n,1)$。第一排的第$i$个点和第二排的第$p_i$个点之间有一条线段。你可以花费$c_i$的代价删除第$i$个点与第$p_i$个点之间的连线,并且删除所有与这条线有交的线段。问最少需要多少的代价,才能够把所有的线段全部删掉。$n\le 10^5$

Solution:

考虑一种$O(n^2)$的$dp$:设$f[i]$表示前$i$条线全部被删除,并且我们选择了第$i$条线的最小代价,那么有

那么,我们对于$i$,我们可行的转移点$j$一定满足对于所有的$p_k\in (p_j,p_i)$,$k$一定都小于$j$。这是一个区间$[1,p_i]$的单调栈,可以用线段树维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define ll long long
using namespace std;
template <class T>
inline void rd(T &x)
{
x=0; char c=getchar(); int f=1;
while(!isdigit(c)){if(c=='-')f=-1; c=getchar();}
while(isdigit(c)) x=x*10-'0'+c,c=getchar(); x*=f;
}
const int N=2e5+10,inf=2e9+1;
#define ls c<<1
#define rs c<<1|1
int mx[N<<2],mi[N<<2];
int query(int l,int r,int c,int p) {
if(l==r) { return mx[c]>p?mi[c]:inf; } int mid=l+r>>1;
if(p<mx[rs]) return min(mi[c],query(mid+1,r,rs,p));
else return query(l,mid,ls,p);
}
void push_up(int c,int l,int r) {
mx[c]=max(mx[ls],mx[rs]); int mid=l+r>>1;
mi[c]=query(l,mid,ls,mx[rs]);
}
int ql,qr,qv,qt,qans;
void update(int l,int r,int c,int p) {
if(l==r) { mx[c]=qv,mi[c]=qt; return; } int mid=l+r>>1;
if(p<=mid) update(l,mid,ls,p); else update(mid+1,r,rs,p); push_up(c,l,r);
}
void Query(int l,int r,int c,int &p) {
if(ql<=l&&qr>=r) {
qans=min(qans,query(l,r,c,p));
p=max(p,mx[c]); return;
}
int mid=l+r>>1;
if(qr>mid) Query(mid+1,r,rs,p);
if(ql<=mid) Query(l,mid,ls,p);
}
int a[N],c[N],f[N],n;
void Update(int x) {
qv=x,qt=f[x];
update(0,n,1,a[x]);
}
int main() {
freopen("knows.in","r",stdin);
freopen("knows.out","w",stdout);
rd(n);
for(int i=1;i<=n;++i) rd(a[i]);
for(int i=1;i<=n;++i) rd(c[i]);
memset(mi,0x3f,sizeof(mi)); // ?!
memset(mx,-1,sizeof(mx));
c[n+1]=0,a[n+1]=n+1;
Update(0);
for(int i=1;i<=n+1;++i) {
ql=0,qr=a[i]-1,qans=inf;
int tmp=-1; Query(0,n,1,tmp);
f[i]=qans+c[i];
if(i==n+1) break;
Update(i);
}
printf("%d\n",f[n+1]);
return 0;
}

练习题:
HNOI2018 转盘
排序二叉树