分治技巧在高级数据结构中的应用——线段树分治(二)&&bzoj4137火星商店问题详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lvzelong2014/article/details/78688727

分治技巧在高级数据结构中的应用——线段树分治(二)

从一道神题说起

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<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;
}

最后的小总结

线段树分治其实就是模拟线段树的一个东西,因为是分治,所以要可以划分子问题。因为模拟线段树,所以他可以代替线段树解决树套树包含单点修改和区间询问的问题。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页