分治技巧在高级数据结构中的应用——线段树分治(二)
从一道神题说起
4137: [FJOI2015]火星商店问题
Time Limit: 20 Sec Memory Limit: 256 MBSubmit: 210 Solved: 98
[ Submit][ Status][ Discuss]
Description
Input
Output
Sample Input
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
0
2
5
HINT
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<iostream>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
#include<cmath>
using namespace std;
const int N = 1e5;
const int T = 5e6;
int read() {
char ch = getchar(); int x = 0, f = 1;
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {x = (x << 1) + (x << 3) - '0' + ch; ch = getchar();}
return x * f;
}
struct ask {int l, r, st, ed, x;}q[N];
struct modify {int shop, tim, val;}mod[N], tmpL[N], tmpR[N];
bool cmp(modify a, modify b) {return a.shop < b.shop;}
int sz, sum[T], ch[T][2], bin[30], num[N], root[N], nt, dt, d[N], ans[N], id[N];
void add(int &cur, int last, int val) {
cur = sz + 1; bool d;
for(int i = 17; ~i; --i) {
sum[++sz] = sum[last] + 1;
d = bin[i] & val;
ch[sz][!d] = ch[last][!d];
ch[sz][d] = sz + 1;
last = ch[last][d];
}
sum[++sz] = sum[last] + 1;
}
int query(int lt, int rt, int val) {
if(lt > rt) return 0;
int ans = 0;
for(int i = 17; ~i; --i) {
bool d = bin[i] & val;
if(sum[ch[rt][!d]] - sum[ch[lt][!d]]) {
ans |= bin[i];
rt = ch[rt][!d];
lt = ch[lt][!d];
}
else {
rt = ch[rt][d];
lt = ch[lt][d];
}
}
return ans;
}
int Lower(int val) {
int l = 1, r = nt, ret = 0;
while(l <= r) {
int mid = l + r >> 1;
if(num[mid] <= val) {
ret = mid;
l = mid + 1;
}
else r = mid - 1;
}
return ret;
}
void work(int ml, int mr) {
sz = 0; nt = 0;
for(int i = ml; i <= mr; ++i) {
++nt;
add(root[nt], root[nt - 1], mod[i].val);
num[nt] = mod[i].shop;
}
for(int i = 1;i <= dt; ++i) {
int l = Lower(q[d[i]].l - 1);
int r = Lower(q[d[i]].r);
ans[d[i]] = max(ans[d[i]], query(root[l], root[r], q[d[i]].x));
}
}
void Seg_Dived(int ml, int mr, int tl, int tr, int at) {
if(ml > mr || at == 0) return;
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) {
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) {
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);
}
int main() {
bin[0] = 1; for(int i = 1;i <= 20; ++i) bin[i] = bin[i - 1] << 1;
int n = read(), m = read(), day = 0, mt = 0, qt = 0, cur = 0;
for(int i = 1;i <= n; ++i) add(root[i], root[i - 1], read());
for(int i = 1;i <= m; ++i) {
int opt = read();
if(!opt) {
++day; mod[++mt].shop = read();
mod[mt].val = read(); mod[mt].tim = day;
}
else {
q[++qt].l = read(); q[qt].r = read();
q[qt].x = read(); int d = read();
q[qt].st = max(day - d, 0) + 1; q[qt].ed = day;
ans[qt] = query(root[q[qt].l - 1], root[q[qt].r], q[qt].x);
}
}
sort(mod + 1, mod + mt + 1, cmp);
for(int i = 1;i <= qt; ++i) id[i] = i;
Seg_Dived(1, mt, 1, day, qt);
for(int i = 1;i <= qt; ++i) printf("%d\n", ans[i]);
return 0;
}
最后的小总结
线段树分治其实就是模拟线段树的一个东西,因为是分治,所以要可以划分子问题。因为模拟线段树,所以他可以代替线段树解决树套树包含单点修改和区间询问的问题。