【一更】2015/4/5
线段树是『维护区间』的数据结构,它能高效计算一个关于区间的函数 『F([a, b])』,计算方法是对区间进行『分治』。
题目
_________
1.『RMQ』传送门
_________
假设整个货架上从左到右摆放了N种商品,并且依次标号为1到N,每次小Hi都给出一段区间[L, R],小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量。但是在这个过程中,可能会因为其他人的各种行为,对某些位置上的商品的重量产生改变(如更换了其他种类的商品)。
『分析』
*这道题要求维护的函数是区间最小值MIN([a, b]),分治策略显然min([a, b])=min(min([a, mid]), min([mid, b]))。
*点修改。
struct node{ int l, r, mi; int mid(){ return (l+r)>>1; } }T[MAX_N<<2]; void unite(int id){ node &now=T[id], &lch=T[id<<1], &rch=T[id<<1|1]; now.mi=min(lch.mi, rch.mi); } void build(int id, int l, int r){ node &now=T[id]; now.l=l, now.r=r; if(l==r){ scanf("%d", &now.mi); return; } int nid=(l+r)>>1; build(id<<1, l, mid); build(id<<1|1, mid+1, r); unite(id); } void insert(int id, int pos, int v){//点更新 node &now=T[id]; if(l==r){ now.mi=v; return; } if(pos<=mow.mid){ insert(id<<1, pos, v); } else{ insert(id<<1|1, pos, v); } unite(id); } int query(int id, int l, int r){ node &now=T[id]; if(l>=now.l&&r<=now.r){ return now.mi; } int mid=now.mid(); int res=INT_MAX; if(l<=mid){ res=min(res, query(id<<1, l, r)); } if(r>mid){ res=min(res, query(id<<1|1, l, r)); } return res; }
_______________
2.『区间修改』传送门
_______________
假设货架上从左到右摆放了N种商品,并且依次标号为1到N,其中标号为i的商品的价格为Pi。小Hi的每次操作分为两种可能,第一种是修改价格——小Hi给出一段区间[L, R]和一个新的价格NewP,所有标号在这段区间中的商品的价格都变成NewP。第二种操作是询问——小Hi给出一段区间[L, R],而小Ho要做的便是计算出所有标号在这段区间中的商品的总价格,然后告诉小Hi。
『分析』
*这道题要维护的函数是区间和SUM([a, b]), 分治策略显然SUM([a,b])=sum([a,mid])+sum([mid,b])。
*区间修改,用到『lazy-tag』。
代码有两种写法
struct node{ int l, r, sum, tag; int mid(){ return (l+r)>>1; } int size(){return r-l+1;} }T[MAX_N<<2]; void unite(int id){ node &now=T[id], &lch=T[id<<1], &rch=T[id<<1|1]; now.sum=lch.sum+rch.sum; } void retag(int id){ node &now=T[id], &lch=T[id<<1], &rch=T[id<<1|1]; lch.tag=rch.tag=now.tag; lch.sum=lch.tag*lch.size(); rch.sum=rch.tag*rch.size(); now.tag=0; } void build(int id, int l, int r){ node &now=T[id]; now.l=l, now.r=r; if(l==r){ scanf("%d", now.sum); return; } int mid=(l+r)>>1; build(id<<1, l, mid); build(id<<1|1, mid+1, r); utite(id); } void insert(int id, int l, int r, int v){ node &now=T[id]; if(l<=now.l && now.r<=r){ now.sum=(now.r-now.l+1)*v; now.tag=v; return; } if(now.tag){ retag(id); } int mid=now.mid(); if(l<=mid){ insert(id<<1, l, mid); } if(r>mid){ insert(id<<1|1, mid+1, r); } unite(id); } int query(int id, int l, int r){ node &now=T[id]; if(l<=now.l&&now.r<=r){ return now.sum; } int mid=now.mid(), res=0; if(l<=mid){ res+=query(id<<1, l, r); } if(r>mid){ res+=query(id<<1|1, l, r); } return res; }
这种写法同时维护了tag与sum,这样做可加速query。
struct node{ int l, r, tag; int mid(){ return (l+r)>>1; } int size(){return r-l+1;} }T[MAX_N<<2]; void unite(int id){ node &now=T[id], &lch=T[id<<1], &rch=T[id<<1|1]; now.sum=lch.sum+rch.sum; } void retag(int id){ node &now=T[id], &lch=T[id<<1], &rch=T[id<<1|1]; lch.tag=rch.tag=now.tag; now.tag=0; } void build(int id, int l, int r){ node &now=T[id]; now.l=l, now.r=r; if(l==r){ scanf("%d", now.tag); return; } int mid=(l+r)>>1; build(id<<1, l, mid); build(id<<1|1, mid+1, r); } void insert(int id, int l, int r, int v){ node &now=T[id]; if(l<=now.l && now.r<=r){ now.tag=v; return; } if(now.tag){ retag(id); } int mid=now.mid(); if(l<=mid){ insert(id<<1, l, mid); } if(r>mid){ insert(id<<1|1, mid+1, r); } } int query(int id, int l, int r){ node &now=T[id]; if(l<=now.l&&now.r<=r&&now.tag){ return now.tag*now.size(); } int mid=now.mid(), res=0; if(l<=mid){ res+=query(id<<1, l, r); } if(r>mid){ res+=query(id<<1|1, l, r); } return res; }
这种写法只维护tag。
__________________
3.『离线算法』传送门
___________________
小Hi和小Ho所在的学校举办社团文化节,各大社团都在宣传栏上贴起了海报,但是贴来贴去,有些海报就会被其他社团的海报所遮挡住。看到这个场景,小Hi便产生了这样的一个疑问——最后到底能有几张海报还能被看见呢?
于是小Ho肩负起了解决这个问题的责任:因为宣传栏和海报的高度都是一样的,所以宣传栏可以被视作长度为L的一段区间,且有N张海报按照顺序依次贴在了宣传栏上,其中第i张海报贴住的范围可以用一段区间[a_i, b_i]表示,其中a_i, b_i均为属于[0, L]的整数,而一张海报能被看到当且仅当存在长度大于0的一部分没有被后来贴的海报所遮挡住。那么问题就来了:究竟有几张海报能被看到呢?
『分析』
#这道题要维护的是一个区间内可见海报的数量,但海报数量不适合分治来求。考虑下面的分治策略,
F([a, b])=F([a, mid])+F([mid, b])-([a, mid]右端海报==[mid, b]左端海报)
按这个策略,节点还要维护两个额外信息『左端海报的ID』,『右端海报的ID』,但这样做是错误的。
容易看出,按上面定义的函数,子节点的函数值与父节点的函数值是【不简并的】(这里我引用物理上的词语来表示父子节点的函数值可否【简单合并】)
#正确的做法是采用离线算法(off-line algorithms),维护节点(区间)的状态:
#节点被一幅图完全覆盖(也可能是完全空白)。
#节点被多幅图覆盖(空白也算)。
所有修改完成后,从根节点DFS,统计答案。这个策略有点像离线LCA算法,先处理完所有修改再处理所有查询。
#区间更新。使用lazy-tag。
#对区间进行离散化。离散化就是建立一个映射。
『类似的题目』
『POJ 2528 Mayors' Posters』
『ZOJ 1610 Count the Colors』
这道题的写法和上一题的第二种写法相同,只维护一个tag
————————————————————————————
4.『多个lazy-tag』
————————————————————————————
小Hi和小Ho都是游戏迷,“模拟都市”是他们非常喜欢的一个游戏,在这个游戏里面他们可以化身上帝模式,买卖房产。
在这个游戏里,会不断的发生如下两种事件:一种是房屋自发的涨价或者降价,而另一种是政府有关部门针对房价的硬性调控。房价的变化自然影响到小Hi和小Ho的决策,所以他们希望能够知道任意时刻某个街道中所有房屋的房价总和是多少——但是很不幸的,游戏本身并不提供这样的计算。不过这难不倒小Hi和小Ho,他们将这个问题抽象了一下,成为了这样的问题:
小Hi和小Ho所关注的街道的长度为N米,从一端开始每隔1米就有一栋房屋,依次编号为0..N,在游戏的最开始,每栋房屋都有一个初始价格,其中编号为i的房屋的初始价格为p_i,之后共计发生了M次事件,所有的事件都是对于编号连续的一些房屋发生的,其中第i次事件如果是房屋自发的涨价或者降价,则被描述为三元组(L_i, R_i, D_i),表示编号在[L_i, R_i]范围内的房屋的价格的增量(即正数为涨价,负数为降价)为D_i;如果是政府有关部门针对房价的硬性调控,则被描述为三元组(L_i, R_i, V_i),表示编号在[L_i, R_i]范围内的房屋的价格全部变为V_i。而小Hi和小Ho希望知道的是——每次事件发生之后,这个街道中所有房屋的房价总和是多少。
『分析』
区间函数及分治策略显然。要点是lasy-tag。
两个lazy-tag分别为『set』和『add』
set能覆盖add,add不能覆盖set,所以set的级别要高于add,需优先下传。
——————————————————————————————
5.『复杂分治』
——————————————————————————————
现在有一个有n个元素的数组a1, a2, ..., an。
记f(i, j) = ai * ai+1 * ... * aj。
初始时,a1 = a2 = ... = an = 0,每次我会修改一个ai的值,你需要实时反馈给我 ∑1 <= i <= j <= n f(i, j)的值 mod 10007。
输入
第一行包含两个数n(1<=n<=100000)和q(1<=q<=500000)。
接下来q行,每行包含两个数i, x,代表我把ai的值改为了x。
这道题的区间函数显然,而且不可采用离线算法,所以要想办法找出分治策略。
L|______________________|R
L |__________|M|__________|R
考虑如何分治
将所求区间函数记为F(L,R),那么累加式中的各项可分为三类
(1) i<=j<=mid
(2) mid<i<=j
(3) i<mid<j
前两中情况可分治到子节点,考虑第三中情况,思考的方向是通过维护节点的某些额外信息使第三种情况同样能利用子节点的信息求解。
不难发现,如果再维护
【起点在左端点的所有连乘积的和】
【终点在右端点的所有连乘积的和】
【从起点到终点的连乘积】
就能处理第三种情况。
这种『跨区间』的分治策略是比较常用的,类似的有利用『merge sort』求逆序对数的分治策略。
——————————————————————————————
6. 『树转数组』
——————————————————————————————
『数转数组』有两种方式,都是通过DFS遍历。
(1)
对一棵有根树进行DFS遍历并记录下每个节点的首次和末次(回溯)访问顺序将得到一个数组【这个数组只是概念上的,其含义是将树结构转化为线性结构】,可把树的结构用数组表示,这个数组包含了树的全部结构信息。DFS过程中记录下的每个节点的初末访问顺序即该节点在数组中的前后两个位置pos_1[], pos_2[]。容易看出对于某节点a,a的全部子节点均位于pos_1[a]与pos_2[a]之间。
#这种记录方式得到的数组长度为2*|V|,每个节点都被记录两次。
(2)
从树的根节点开始进行深度优先搜索,每次经过某一个点——无论是从它的父亲节点进入这个点,还是从它的儿子节点返回这个点,都按顺序记录下来。
#这种记录方式得到的数组长度为2*|E|+1, 每条边贡献两次记录,外加进入根节点时的一次记录。
【注意】这两种转化方式是有区别的,不能混淆。按第一种方式每个节点都被记录了两次,按第二种方式,叶子节点只记录一次而非叶子节点被记录多于 一次。【除根节点外每个节点被记录的次数为其『度数』,根节点被记录的次数为其『度数』+1】
6.1 【LCA在线算法(LCA Online Algorithm)】
不妨将LCA问题中的树称作谱系树(Family Tree),简称FT。按方式(2)遍历FT,将其转为数组,数组里存的是对应节点的深度,遍历的同时建立从访问顺序到节点的映射,
order[],以及从节点到末次访问顺序的映射last[]。
将a,b两节点的LCA记作LCA(a,b)。那么不难发现,LCA(a,b)就是a,b两点连通路径上的【折点】,而这个【折点】就是last[a]到last[b]中『深度最小』的那个位置对应的节点。这样就把LCA转化成RMQ了。
6.2 【HDU 3947 Assign the Task】
题目大意:parent[]表的形式给出一棵有根树,给出两中操作
T (x y) 将以x为根子树中所有节点的值置为y。
Q (x) 询问节点x的值。
『分析』按方式(1)将【树】转成【数组】,建【线段树】,区间修改,lasy-tag,点询问。
【注意】这道题的询问不能直接根据记录的pos数组在叶子节点找,那样要求每次修改都推到叶子。应该利用【lazy-tag】,【top-down】地找。
【类题】『Apple Tree』
————————————————————————————————————
7、『扫描线』求矩形面积并
————————————————————————————————————
【第二更 2015/4/6】
扫描线是一根假想的线。扫面线求矩形面积并的原理好懂。而实现上我的思路至今还不清晰,这里总结几个关于扫描线的重要observation。这将有助于理解实现。
(1)扫描线是一根从区间最左端到最右端的线,它始终充满整个区间。
----------------------------------------------
|_____________________________________________|
(2)扫描是一个【查询】过程并不是一个【修改】过程。
(3)线段树维护的是扫描所需要的【Active Edge Table】即每次扫描时整段区间内那些区域是有效的。
_______________
________________________
_________
__________
_____________
|---------------------------------------| X-axis(X-Bucket)
(4)线段树节点无需存储高度信息,只要维护区间是否active。因为每次查询时X-Bucket内的所有active edge的高度都上次扫描线的高度。