18 Sep 2016

关于线段树

众所周知,线段树是一个神奇的数据结构,它的神奇之处在于有时你以为你已经是在暴力了,然而其实你的做法却非常的优(一般在$O(n\log n) -O(n \log n\log n) $之间),orz jiry_2在16年集训队论文里主要论述了这些玄学,我照着论文口胡两句(持续施工)


HDU5306 Gorgeous Sequence

题目大意:

给出一个长度为$n$的数组$A$,接下来有$m$次操作,操作有如下$3$种:

$1.$给出$l,r,x$,对所有的$i \in [l,r]$,把$A_i$变为$\min(A_i,x)$

$2.$给出$l,r$,求$\sum_{i=l}^rA_i$

$3.$给出$l,r$,求$\max_{i=l}^r A_i$


题解:

主要论述一种路人皆知的做法以及复杂度的证明。

对于每个线段树节点,维护一个对应区间的最大值以及最大值的个数以及次大值的个数。

然后在更新的时候,如果$x$大于等于区间最大值$fi$,则不会对区间造成任何影响。

如果$x\in(se,fi]$,这个更新很方便吧,直接把最大值给换成$x$然后维护$cnt_{fi}$即可。

如果$x<se$,暴力向下更新。

若要说这玩意的复杂度是$O((n+m) \log n)$的,想必大家都目(zao)瞪(jiu)口(zhi)呆(dao)了。

我说一个比较通俗的证明:

可以把最大值视为标记,如果一个节点的最大值和他爹的最大值相同,那么这个点就没有标记值。

下面是一个例子:(左边是最大值,右边是标记值)

线段树

这个标记具有一些特别的性质:每个点的真实权值=从这个点往上遇到的第一个有标记的节点的标记值。以及你维护的节点的$se$,就是子节点标记的最大值。

显然第一遍build完线段树,标记共有$n$个

然后我们看更新:

if $x\geq fi$,不会有任何影响。

else if $x \in (se,fi]$,更新了$fi$就退出了,也不会有啥事。

else 激动人心的暴力更新来了。

这个暴力更新的本质,正如jiry_2在论文中所说,是为了满足标记随深度递减的性质,在自述中回收掉值大于它的标记。

观察这个标记回收,在回收的过程中,至少减少了一个标记

又一个标记至多存在$O(\log n)$个,那么回收一个标记的复杂度为$O(\log n)$,回收$n$个的复杂度为$O(n \log n)个$

怎么回事,那么我这样的复杂度不是变成了$O((n+m)\log n)$了,怎么和吉利的$O(m \log n)$不一样。。

算了反正同阶,不管他。。

那么复杂度就得证了。

代码里用了一个我也不晓得是啥的读入方式(大概是一口气把东西读进来然后需要读东西的时候在数组里读)。。不用就会T


Code

#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<string.h>
using namespace std;
#define bug(x) cout<<#x<<"="<<x<<" "
#define debug(x) cout<<#x<<"="<<x<<endl
const int M=1e6+5;
typedef long long ll;
char *ch,buf[40*1024000+5];
inline void rd(int &a){
	for(++ch;*ch<=32;++ch);
	for(a=0;47<*ch;++ch)a=a*10+(*ch^48);
}
int n,m,A[M];
inline void Min(int &a,int b){
	if(a>b)a=b;
}
struct Segment_Tree{
	struct node{
		int flag,fi,se,cnt_fi;
		ll sum;
	}tree[M<<2];
	inline void up(int &p){
		tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum;
		bool f=tree[p<<1].fi<tree[p<<1|1].fi;
		tree[p].fi=tree[p<<1|f].fi;
		tree[p].cnt_fi=tree[p<<1|f].cnt_fi;
		if(tree[p].fi==tree[p<<1|f^1].fi)tree[p].cnt_fi+=tree[p<<1|f^1].cnt_fi;
		if(tree[p].fi!=tree[p<<1|f^1].fi)tree[p].se=max(tree[p<<1|f].se,tree[p<<1|f^1].fi);
		else tree[p].se=max(tree[p<<1].se,tree[p<<1|1].se);
	}
	inline void down(int &p){
		for(int i=0;i<2;++i)
			if(tree[p].fi<tree[p<<1|i].fi){
				tree[p<<1|i].sum-=(ll)(tree[p<<1|i].fi-tree[p].fi)*tree[p<<1|i].cnt_fi;
				tree[p<<1|i].fi=tree[p].fi;
			}
	}
	inline void build(int l=1,int r=n,int p=1){
		tree[p]=(node){0,0,0,0,0};
		if(l==r){
			tree[p].sum=tree[p].fi=A[l];
			tree[p].cnt_fi=1;
			return;
		}
		int mid=l+r>>1;
		build(l,mid,p<<1);
		build(mid+1,r,p<<1|1);
		up(p);
	}
	inline void update(int l,int r,int x,int L=1,int R=n,int p=1){
		down(p);
		if(l==L&r==R){
			if(tree[p].fi<=x)return;
			if(tree[p].se<=x){
				tree[p].sum-=(ll)(tree[p].fi-x)*tree[p].cnt_fi;
				tree[p].fi=x;return;
			}
		}
		int mid=L+R>>1;
		if(r<=mid)update(l,r,x,L,mid,p<<1);
		else if(l>mid)update(l,r,x,mid+1,R,p<<1|1);
		else update(l,mid,x,L,mid,p<<1),update(mid+1,r,x,mid+1,R,p<<1|1);
		up(p);
	}
	inline ll query_sum(int l,int r,int L=1,int R=n,int p=1){
		if(l==L&r==R)return tree[p].sum;
		down(p);
		int mid=L+R>>1;
		if(r<=mid)return query_sum(l,r,L,mid,p<<1);
		if(l>mid)return query_sum(l,r,mid+1,R,p<<1|1);
		return query_sum(l,mid,L,mid,p<<1)+query_sum(mid+1,r,mid+1,R,p<<1|1);
	}
	inline int query_mx(int l,int r,int L=1,int R=n,int p=1){
		if(l==L&r==R)return tree[p].fi;
		down(p);
		int mid=L+R>>1;
		if(r<=mid)return query_mx(l,r,L,mid,p<<1);
		if(l>mid)return query_mx(l,r,mid+1,R,p<<1|1);
		return max(query_mx(l,mid,L,mid,p<<1),query_mx(mid+1,r,mid+1,R,p<<1|1));
	}
}sgt;
inline void gao(){
	rd(n),rd(m);
	for(int i=1;i<=n;++i)
		rd(A[i]);
	sgt.build();
	for(int opt,l,r,x;m--;){
		rd(opt),rd(l),rd(r);
		switch(opt){
			case(0):rd(x);sgt.update(l,r,x);break;
			case(1):printf("%d\n",sgt.query_mx(l,r));break;
			case(2):printf("%lld\n",sgt.query_sum(l,r));break;
		}
	}
}
int main(){
	ch=buf-1;
	fread(buf,1,1000*35*1024,stdin);
	int _;
	for(rd(_);_--;)gao();
	return 0;
}


Picks loves segment tree

题目大意:

在上一题的基础上加入区间加减。

题解:

看了jiry_2的证明我不明觉厉。。

看了半天智商不够的我只领悟了一个地方:哦,原来复杂度变成了$O(m\log n \log n)$

看不懂所以只好自己瞎比扯一下皮。

证明:

同样的我们沿用上题的标记机制。

如果有了区间加减会发生什么呢?

无非是本来标记已经被删除了的点的标记又冒出来了

但是这个区间是同加同减的。

换而言之,对于线段树上一个节点对于的区间,标记只会增加$1$

一个区间至多被分成$\log n$个区间

也就是说在一次区间加减的过程中,标记至多增加$\log n$个。

那么暴力回收的复杂度就是$O(n \log n \log n)$了

Code

没地方交的题所以没有代码。


AcrossTheSky loves segment tree

题目大意:

给出一个长度为$n$的数列$A$,以及$m$次操作,操作有如下3种:

$1.$对于所有$i \in [l,r]$,将$A_i$变成$\min(A_i,x)$ $2.$对于所有$i \in [l,r]$,将$A_i$变成$\max(A_i,x)$ $3.$求$\sum_{i=l}^r A_i$

题解:

这次打两个标记。

复杂度证明:

同样的,加入了区间取最大值会改变标记的个数。

但是同样的,这里的相对性由于已经有了最大值的限制,只会变小不会变大。

也就是说一个节点对应的区间的标记值不会增加

那么一个区间取$\max$至多被划分成$\log n$个区间,单次至多增加$O(\log n)$个标记。

暴力回收的复杂度为$O(n\log n \log n)$,不会退化

Code

没地方交的题同样没代码。。


以上是论文里最简单(虽然我啥也看不懂)的部分,先在此小结。


Tags:
Stats:
comments


Share:


Comments: