题单。
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::list
的 insert
函数请自行 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--
去掉即可。
然后这个题需要维护的操作就没了,注意初始化的时候也要重构一次。
代码(output
为 debug
)。