分块相关题目

题单

UPD:题单里的题 n=m

数列分块入门一

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

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

时间复杂度 O(mn),可以通过。

代码

数列分块入门二

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

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

可惜这样是错的。

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

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

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

时间复杂度 O(mnlogn),可以通过。

代码

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

然后根号平衡随便算算可知块长取 nlogn 最优,实际测试取 135150 最优,运行时间 390 ms 左右。

块长取常数!!!

代码

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

数列分块入门三

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

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

时间复杂度 O(mnlogn),可以通过。

代码

数列分块入门四

考虑维护每个块的和。

对于修改操作,整块打标记,散块暴力修改;对于查询操作,整块加上 区间和+区间长度×标记,散块暴力,但是也要加上标记。

时间复杂度 O(mn),可以通过此题。

代码

数列分块入门五

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

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

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

时间复杂度 O(mn),可以通过此题。

代码

数列分块入门六

块状链表板子。

我们考虑维护这么一个数据结构:内部是一个不定长的块,并设置一个最大长度 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));
}

六、分裂

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

首先特判一下 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×LEN,那么就一直将其末尾分裂为长度为 LEN 的块,直至其满足条件为止;如果当前块不为最尾部的块,且 x 块和 Next(x) 块的大小之和都小于 LEN,就合并这两块;最后,将末尾被删光的空块删除。

一次重构的最坏复杂度是 O(n) 的,但是其平均操作次数远小于 O(n),可以近似认为是 O(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 个元素前面插入等同于在第 x1 个元素后面插入,所以只讨论在后面插入的情况。

首先将第 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 @   Southern_Dynasty  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
点击右上角即可分享
微信分享提示