吉司机线段树小记
势能线段树的一种,相信如果理解了势能线段树的基本思想那这东西也不难理解了。
问题的引入——区间取 \(\min\) 区间求和
你需要维护一个序列,支持以下两种操作:
- 区间取 \(\min\)
- 区间求和
在刚学线段树的时候我们就知道,这东西一般的线段树+懒标记是解决不了的,因为在下放标记的时候无法直接计算此次修改对和的贡献。
怎么办呢?这下就要请出我们的吉司机线段树了。
吉司机线段树大概就是对线段树上每个节点维护一个最大值 \(mx\)(注意是最大值,可以简单记忆为维护的值与操作相反)和严格次大值 \(smx\)(注意,必须是严格次大值,否则会出错),以及最大值个数 \(c\),如果不存在严格次大值可以设为 \(-\infty\)。
吉司机线段树的过程如下:
-
假设我们要对于区间 \([l,r]\) 中的数对 \(x\) 取 \(\min\),我们首先将 \([l,r]\) 拆分成线段树上若干个区间 \([l_i,r_i]\)
-
对于每个 \([l_i,r_i]\):
-
如果它的最大值 \(\le x\),那么显然此次操作没有任何效果,直接
return
即可。 -
如果 \(x\) 小于最大值 \(mx\),但大于(注意,这里必须是严格大于,否则的话也会出问题)严格次大值 \(smx\),那么显然有且只有 \(c\) 个最大值会变为 \(x\),我们可以简单维护一个标记 \(tg\) 表示这段区间内最大值会增加 \(tg\),然后令 \(tg\leftarrow tg+(x-mx)\) 即可,由于更新完之后最大值依然严格大于次大值,因此最大值个数不会发生变化。
-
如果 \(x\) 小于等于严格次大值 \(smx\),这个就没有什么优美的方法了,直接暴力递归左右子区间即可。
-
吉司机线段树复杂度是 \(n\log n\) 的,证明如下:
- 我们记一个节点的容为这段区间内不同数的个数,那么显然在不断取 \(\min\) 的过程中容只可能越来越小。而如果对于某个区间如果我们对其进行暴力递归,那么原来是最大值和次大值的位置上的数必然会变为同一个值,区间的容建一,而所有区间的长度之和是 \(n\log n\) 级别的,因此所有区间容之和也是 \(n\log n\) 级别的,暴力递归的次数也是 \(n\log n\) 级别的,得证。
代码大致长这样:
void pushup(int k){
s[k].mx=max(s[k<<1].mx,s[k<<1|1].mx);
s[k].sum=s[k<<1].sum+s[k<<1|1].sum;
if(s[k<<1].mx>s[k<<1|1].mx){
s[k].smx=max(s[k<<1|1].mx,s[k<<1].smx);
s[k].c=s[k<<1].c;
} else if(s[k<<1].mx<s[k<<1|1].mx){
s[k].smx=max(s[k<<1].mx,s[k<<1|1].smx);
s[k].c=s[k<<1|1].c;
} else {
s[k].smx=max(s[k<<1].smx,s[k<<1|1].smx);
s[k].c=s[k<<1].c+s[k<<1|1].c;
}
}
void build(int k,int l,int r){
s[k].l=l;s[k].r=r;if(l==r) return s[k].mx=a[l],s[k].smx=INF,s[k].c=1,void();
int mid=l+r>>1;build(k<<1,l,mid);build(k<<1|1,mid+1,r);pushup(k);
}
void pushtag(int k,ll v){s[k].sum+=1ll*v*s[k].c;s[k].tag+=v;s[k].mx+=v;}
void pushdown(int k){
if(s[k].tag){
bool tmp=s[k<<1|1].mx>=s[k<<1].mx;
if(s[k<<1].mx>=s[k<<1|1].mx) pushtag(k<<1,s[k].tag);
if(tmp) pushtag(k<<1|1,s[k].tag);s[k].tag=0;
}
}
void modify(int k,int l,int r,int v){
if(l>r||v>=s[k].mx) return;
if(l<=s[k].l&&s[k].r<=r){
if(v>s[k].smx) return pushtag(k,v-s[k].mx),void();
else{
int mid=(pushdown(k),l+r>>1);
return modify(k<<1,l,mid,v),modify(k<<1|1,mid+1,r,v),pushup(k),void();
}
} int mid=(pushdown(k),s[k].l+s[k].r>>1);
if(r<=mid) modify(k<<1,l,r,v);
else if(l>mid) modify(k<<1|1,l,r,v);
else modify(k<<1,l,mid,v),modify(k<<1|1,mid+1,r,v);
pushup(k);
}
ll query(int k,int l,int r){
if(l<=s[k].l&&s[k].r<=r) return s[k].sum;
int mid=(pushdown(k),s[k].l+s[k].r>>1);
if(r<=mid) return query(k<<1,l,r);
else if(l>mid) return query(k<<1|1,l,r);
else return query(k<<1,l,mid)+query(k<<1|1,mid+1,r);
}
吉司机线段树的一些变种
带区间加的线段树
其实也比较好办,额外加一个懒标记 \(stag\) 表示除了最大值之外的其他数都要增加 \(stag\),区间加 \(v\) 时令对应区间的 \(tg\) 和 \(stag\) 都加 \(v\),区间取 \(\min\) 时只会影响 \(tg\),不会影响 \(stag\) 的值。下推标记就和普通吉司机树一样正常推即可。具体可见 CF1290E 的代码。
据说复杂度是 \(n\log^2n\) 的,但不会证/kk,直接用就得了(
同时取 \(\min/\max\) 的吉司机线段树
再维护一个最小值、次小值、最小值标记即可,注意特判最大值等于最小值的情况。
复杂度依旧 \(n\log n\)。
求一个点被取最小值的次数
额外维护一个标记 \(ctag\) 表示被取最小值的次数,对于上面情况中的第二种(\(smx<x<mx\)),直接令 \(ctag\) 加一即可,下推 \(ctag\) 时就对于左右儿子中最大值等于该区间原来的最大值的区间把 \(ctag\) 传给他们即可。
查询时就一路将 \(ctag\) 推到叶子节点处,输出对应叶子节点的 \(ctag\) 即可。
例题:
1. HDU 5306 Gorgeous Sequence
mol ban tea,没啥好说的,练练熟练度吧
2. CF1290E Cartesian Tree
djq 说他年轻时分不清 Cartesian 和 Catalan
首先关于笛卡尔树子树的大小,有一个结论:记 \(r_i\) 为在 \(i\) 后面第一个大于 \(a_i\) 的位置,\(l_i\) 为在 \(i\) 前面第一个小于 \(a_i\) 的位置,那么以 \(i\) 为根的子树大小为 \(r_i-l_i+1\)。
因此所有子树之和的大小自然就是 \(\sum\limits_{i=1}^nr_i-l_i+1\)
故我们只需求出 \(\sum\limits_{i=1}^nl_i\) 和 \(\sum\limits_{i=1}^nr_i\) 即可。
考虑怎样维护这个东西,当我们加入一个数 \(a_p=x\) 时候,会对 \(r_i\) 产生以下的影响:
- 对于 \(i>p\),\(r_i\leftarrow r_i+1\),因为下标整体向右移了一格
- 对于 \(i<p\),\(r_i\leftarrow\min(r_i,p')\),其中 \(p'\) 为 \(a_p\) 此时在序列中的位置
- 对于 \(i=p\),\(r_i\leftarrow x+1\)
于是我们需要支持区间加、区间取 \(\min\),单点赋值,全局求和,吉司机线段树即可。
至于怎样求 \(\sum l_i\),其实只需把序列反转一下,按照 \(\sum r_i\) 的套路维护即可,最后真正的 \(\sum l_i\) 等于 \(i(i+1)\) 减去求出的 \(\sum r_i\)
时间复杂度 \(n\log^2n\)。
3. CF855F Nagini
讲个笑话,星期三我们机房几个(吊打我的)人组队打 CF 855 的 virtual,而我在学吉司机线段树,然后上网一搜,刚好搜到这个题……
kyl:啊,这场竟然是以哈利波特为主题的
跑题了跑题了。
首先开两个 set
\(ban1,ban2\) 分别维护不存在正数、不存在负数的集合编号的集合,当我们加入一个正数时,我们只需将 \(ban1\) 中编号在 \([l,r)\) 中的数删除,加入一个负数也同理,如果一个数同时不在 \(ban1,ban2\) 中就证明它的权值非零了。
显然,对于一个权值非零的集合,它的权值等于正数的最小值减去负数的最大值,于是考虑建立两棵线段树维护正数最小值和负数最大值,需要支持区间取 \(\min\)(或者区间取 \(\max\)),将某个位置设为合法,查询所有合法的位置上数的和,吉司机线段树一波带走,时间复杂度线性对数。
4. UOJ 515 【UR #19】前进四
这题为什么 UOJ 上这么多差评啊 qwq,感觉除了套路一点其他还好罢/ts
首先将询问离线并建出时间轴,然后从右往左枚举位置,对于每个位置记录每个数出现的时间(显然是一个区间),在线段树上对其取 \(\min\),那么对于一个询问而言,它的答案就是对应询问时间的位置被取 \(\min\) 的次数,直接用求一个点被取最小值的次数的套路即可搞定,时间复杂度 \(n\log n\)。