分治技巧在高级数据结构中的应用——线段树分治(二)
从一道神题说起
4137: [FJOI2015]火星商店问题
Time Limit: 20 Sec
Memory Limit: 256 MB
Submit: 210
Solved: 98
[ Submit][ Status][ Discuss]
Description
火星上的一条商业街里按照商店的编号1,2 ,…,n ,依次排列着n个商店。商店里出售的琳琅满目的商品中,每种商品都用一个非负整数val来标价。每个商店每天都有可能进一些新商品,其标价可能与已有商品相同。
火星人在这条商业街购物时,通常会逛这条商业街某一段路上的所有商店,譬如说商店编号在区间[L,R]中的商店,从中挑选1件自己最喜欢的商品。每个火星人对商品的喜好标准各不相同。通常每个火星人都有一个自己的喜好密码x。对每种标价为val的商品,喜好密码为x的火星人对这种商品的喜好程度与val异或x的值成正比。也就是说,val xor x的值越大,他就越喜欢该商品。每个火星人的购物卡在所有商店中只能购买最近d天内(含当天)进货的商品。另外,每个商店都有一种特殊商品不受进货日期限制,每位火星人在任何时刻都可以选择该特殊商品。每个商店中每种商品都能保证供应,不存在商品缺货的问题。
对于给定的按时间顺序排列的事件,计算每个购物的火星人的在本次购物活动中最喜欢的商品,即输出val xor x的最大值。这里所说的按时间顺序排列的事件是指以下2种事件:
事件0,用三个整数0,s,v,表示编号为s的商店在当日新进一种标价为v 的商品。
事件1,用5个整数1,L,R,x,d,表示一位火星人当日在编号为L到R的商店购买d天内的商品,该火星人的喜好密码为x。
Input
第1行中给出2个正整数n,m,分别表示商店总数和事件总数。
第2行中有n个整数,第i个整数表示商店i的特殊商品标价。
接下来的m行,每行表示1个事件。每天的事件按照先事件0,后事件1的顺序排列。
Output
将计算出的每个事件1的val xor x的最大值依次输出。
Sample Input
4 6
1 2 3 4
1 1 4 1 0
0 1 4
0 1 3
1 1 1 1 0
1 1 1 1 1
1 1 2 1 2
Sample Output
5
0
2
5
HINT
n, m <= 100000
数据中,价格不大于
100000
Source
一眼看出是裸的线段树套可持久化Trie树对不对?
好像不是很可以接受
看上去是一道神烦的题目,其实还是可以接受的。
首先,对于特殊的商品,就是计算区间L~R用val亦或的最大值。
那么有一种神奇的做法叫做可持久化Trie树,博客看这里:戳我戳我戳我戳我!!!!
那么对于待添加的商品,多了一维时间,所以如果是纯粹的树套树,只能套一个时间线段树,然后每次查询某个时间区间的最大值,而最大值又要用可持久化Trie树来维护。。。。。
细思恐极。。。
好像某大佬空间卡过了。。%%%
但今天,介绍的是一种神奇的分治方法,可以愉快地解决这个问题。就是线段树分治
什么是线段树分治?
显然,对于每个询问——如果我们模拟某个线段树的处理过程——会被分成logn个区间。而对于某个线段树上的区间,每种值都只会被处理一次,然后分成某些个线段树上的区间后,们直接返回存储在线段树上的答案即可。
而线段树分治,其实就是要模拟这个过程。
神奇的模拟操作
我们考虑,每个线段树区间都只会被处理一次,然后每个询问到这个区间后我们直接返回处理后的答案就好了。
显然,我们是先处理,再询问。
那想这道题,先处理要把所有答案存储下来,空间太大做不了。
因此我们改变处理的顺序。先询问,再处理。
大体的思路已经出来了:先把每个询问像线段树一样分成若干log个线段树的区间。对于每个线段树到达的区间,我们按某种分治顺序处理这个区间。得到答案后直接更新询问的答案。然后将处理用的数据清除,继续下一个区间。初步想法已经出来了。
从想法到算法。
第一步,对于当前区间[L,R]进行处理。
第二步,枚举所有可能属于当前区间的询问,如果该询问包含本区间,用本区间处理后的答案更新这个询问的答案。
第三步,还原处理数据。
第四步,分治,取mid=L+R>>1,如果某个询问和[L,mid]有交集,那么把这些询问放到询问队列中递归解决左区间。然后再把和[mid+1,R]有交集的询问入队列,递归解决右区间即可。
伪代码
void Seg_Dived(int ml, int mr, int tl, int tr, int at) { //ml,mr表示修改操作区间,tl,tr表示二分的时间区间
dt = 0; int mid = tl + tr >> 1; //二分时间区间
for(int i = 1;i <= at; ++i) //添加可询问区间进入队列
if(q[id[i]].st <= tl && tr <= q[id[i]].ed)
d[++dt] = id[i];
work(ml, mr); //解决询问
int lt = 0, rt = 0;
for(int i = ml;i <= mr; ++i) { //分治修改区间
if(mod[i].tim <= mid)
tmpL[lt++] = mod[i];
else tmpR[rt++] = mod[i];
}
for(int i = 0;i < lt; ++i) mod[i + ml] = tmpL[i];
for(int i = 0; i < rt; ++i) mod[i + ml + lt] = tmpR[i];
if(tl == tr) return;
int idt = 0;
for(int i = 1;i <= at; ++i) { //把有关[L,mid]区间的询问加入队列
if(q[id[i]].st <= tl && tr <= q[id[i]].ed) continue;
if(q[id[i]].st <= mid) swap(id[i], id[++idt]);
}
Seg_Dived(ml, ml + lt - 1, tl, mid, idt); //分治左区间
idt = 0;
for(int i = 1;i <= at; ++i) { //把有关[mid + 1,R]区间的询问加入队列
if(q[id[i]].st <= tl && tr <= q[id[i]].ed) continue;
if(q[id[i]].ed > mid) swap(id[i], id[++idt]);
}
Seg_Dived(ml + lt, mr, mid + 1, tr, idt); //分治右区间
}
然后就差不多了。
回到题目
反正就是把线段树那层用线段树分治来模拟。可持久化Trie树直接把树根清零就可以完美解决空间问题,代码复杂度、 空间复杂度和时间复杂度都有可观之处。但是代码还是神烦。
代码一波~
#include
#include
#include
#include
#include
#include
最后的小总结
线段树分治其实就是模拟线段树的一个东西,因为是分治,所以要可以划分子问题。因为模拟线段树,所以他可以代替线段树解决树套树包含单点修改和区间询问的问题。