吉司机线段树小记

势能线段树的一种,相信如果理解了势能线段树的基本思想那这东西也不难理解了。

问题的引入——区间取 \(\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\)

posted @ 2021-06-25 12:55  tzc_wk  阅读(502)  评论(1)    收藏  举报