分块相关题目

题单

UPD:题单里的题 \(n=m\)

数列分块入门一

看到区间修改 \(+\) 单点查询,考虑差分。

考虑分块维护差分数组。对于修改操作,就对 \(l\) 位置 \(+k\)\(r+1\) 位置 \(-k\);对于查询操作,查询 \([1,x]\) 的和即可。

时间复杂度 \(O(m\sqrt{n})\),可以通过。

代码

数列分块入门二

如果直接查找,那么复杂度是线性的,因此我们考虑让原序列有序。

一个简单的思路;一开始将每个块排序,然后修改整块打标记,散块暴力修改。对于查询操作,散块暴力统计,整块直接二分。

可惜这样是错的。

原因很简单,排序会破坏原数组的顺序,这样散块的结果就不对了。

考虑设辅助数组 \(b\),初始为 \(a\) 并将其排序。修改操作整块直接打标记,散块对 \(a\) 修改再拍到 \(b\) 上面重新排序。对于查询操作,整块二分,散块暴力查找 \(a\)

注意整块二分的时候要先减去该块的标记再对 \(b\) 进行二分。

时间复杂度 \(O(m\sqrt{n}\log{\sqrt{n}})\),可以通过。

代码

更优的做法:对每个块排序,记录每个元素原来的位置。然后对零散块的修改就可以直接归并,砍掉一只 \(\log\)

然后根号平衡随便算算可知块长取 \(\sqrt{\frac{n}{\log n}}\) 最优,实际测试取 \(135\sim 150\) 最优,运行时间 \(390\) ms 左右。

块长取常数!!!

代码

类似的题:P2801 教主的魔法。只需要改改数据范围,开 long long 即可。

数列分块入门三

仍然可以像上个题一样二分查找。

但是还有一种思路:对每个块维护一个 multiset。修改时,整块打标记,散块在 multiset 中删除原数 \(\to\) 原数 \(+c\to\) 插入新数;对于查询,整块二分查找前驱(lower_bound-1),散块暴力。

时间复杂度 \(O(m\sqrt{n}\log{\sqrt{n}})\),可以通过。

代码

数列分块入门四

考虑维护每个块的和。

对于修改操作,整块打标记,散块暴力修改;对于查询操作,整块加上 \(\text{区间和}+\text{区间长度}\times\text{标记}\),散块暴力,但是也要加上标记。

时间复杂度 \(O(m\sqrt{n})\),可以通过此题。

代码

数列分块入门五

乍一看好像不好维护,考虑挖掘一下开方的性质。

实际上,对于 \(2^{31}-1\) 来说,只需要经过五次开方就会 变成 \(1\),之后便不会变化。因此可以考虑维护每个块被开方的次数 \(cnt_x\),修改时整块如果 \(cnt\ge5\) 就直接退出,否则暴力修改,散块直接暴力。这样时间复杂度是对的,因为每个数至多被修改 \(5\) 次。

对于查询操作,只需要在修改的同时顺便维护下区间和即可。

时间复杂度 \(O(m\sqrt{n})\),可以通过此题。

代码

数列分块入门六

块状链表板子。

我们考虑维护这么一个数据结构:内部是一个不定长的块,并设置一个最大长度 \(LEN\);块与块直接用链表连起来。对于这道题来说就是一个 list<vector<int> >

接下来考虑实现如下操作:

零、定义

list<vector<int> > List;
typedef list<vector<int> >::iterator IT;

一、初始化

由于这个题一开始有 \(n\) 个元素,因此直接读入到一个 vector 中并插入块状链表即可。

vector<int>a;
for(int i=1;i<=n;++i){
	int x=read();
	a.emplace_back(x);
}
List.insert(List.end(),a);

关于 std::listinsert 函数请自行 BDFS。

二、查找某个位置所在块

在对某个位置的元素进行操作时,往往需要求出它处于哪个块中,并更新它为在其所在块中的位置,便于在块中进行访问。

做法:枚举每一个块计算 size 即可。

inline IT find(int &pos){
	// 返回 pos 所在块,pos 变为在其所在块的位置 
	for(IT i=List.begin();;++i){
		if(i==List.end()||pos<=(int)i->size())return i;
		pos-=i->size();
	}
}

三、查询某一位置的值

我们假定下标从 \(1\) 开始。

我们首先调用 find 函数求出当前位置所在的块 it 及其在所在块的位置 pos。由于容器的下标是从 \(0\) 开始的,因此调用 it->at(pos-1)

inline int get(int pos){
	// 查询第 pos 个元素的值
	IT it=find(pos);
	return it->at(pos-1);
}

四、后继

查找某个块的后继。

指针 \(++\) 即可。

inline IT Next(IT x){return ++x;}

五、合并

假设要合并 \(x\) 块和 \(Next(x)\) 块。

我们只需要将 \(Next(x)\) 中的所有元素复制到 \(x\) 的最后面,然后删除 \(Next(x)\) 即可。

inline void Merge(IT x){
	// merge x and x+1
	x->insert(x->end(),Next(x)->begin(),Next(x)->end());
	List.erase(Next(x));
}

六、分裂

这次我们要将块 \(x\)\(pos\) 的位置后面分裂,\(pos\) 成为前一段的最后一个元素(\(pos\)\(1\) 开始)。

首先特判一下 \(pos\) 在块尾的情况,此时不需要分裂。

然后把 \(pos\) 那一段插入到 \(Next(x)\) 前面,然后在 \(x\) 中删除这一段即可。

inline void Split(IT x,int pos){
	// 将第 x 块从第 pos 个元素后面分开 
	if(pos==(int)x->size())return;
	List.insert(Next(x),vector<int>(x->begin()+pos,x->end()));
	x->erase(x->begin()+pos,x->end());
}

七、重构

这个是重点。

由于频繁插入后可能会使一个块的大小过大,从而浪费时间,因此需要将这些块重新分裂、合并。

具体操作是,扫描每一个块,如果其大小超过 \(2\times LEN\),那么就一直将其末尾分裂为长度为 \(LEN\) 的块,直至其满足条件为止;如果当前块不为最尾部的块,且 \(x\) 块和 \(Next(x)\) 块的大小之和都小于 \(LEN\),就合并这两块;最后,将末尾被删光的空块删除。

一次重构的最坏复杂度是 \(O(n)\) 的,但是其平均操作次数远小于 \(O(n)\),可以近似认为是 \(O(\sqrt{n})\) 级别。

inline void rebuild(){
	// 重构 
	for(IT i=List.begin();i!=List.end();++i){
		while(i->size()>=(LEN<<1)) Split(i,i->size()-LEN);
		while(Next(i)!=List.end()&&i->size()+Next(i)->size()<=LEN) Merge(i);
		while(Next(i)!=List.end()&&!Next(i)->size()) List.erase(Next(i));
	}
}

八、插入元素

插入分为在前面插入和在后面插入两种。本题为在前面插入,但实际上在第 \(x\) 个元素前面插入等同于在第 \(x-1\) 个元素后面插入,所以只讨论在后面插入的情况。

首先将第 \(x\) 个元素所在块在 \(x\) 所在的位置分裂,然后在 \(Next(x)\) 面前插入待插入元素即可(本题是一个长为 \(1\),元素为插入元素的 vector)。

inline void insert(int pos,int x){
	// 在 pos 前面插入 x 
	pos--;
	IT now=find(pos);
	if(List.size())Split(now,pos);
	List.insert(Next(now),vector<int>(1,x));
	rebuild();
}

这个是在前面插入的,在后面插入把 pos-- 去掉即可。

然后这个题需要维护的操作就没了,注意初始化的时候也要重构一次。

代码outputdebug)。

P4008 [NOI2003] 文本编辑器

posted @ 2024-02-28 15:25  Southern_Dynasty  阅读(17)  评论(0编辑  收藏  举报