【LuoguP4008】【BZOJ1507】【NOI2003】文本编辑器 Editor
题目链接1: https://www.luogu.org/problemnew/show/P4008
题目链接2: https://www.lydsy.com/JudgeOnline/problem.php?id=1507
前言
这题放在任务计划里已经有些时日了,又拿出来看了看题面。原本是练习Splay用的,但因为不够熟练和懒得缘故,一直不想动手写。看了题解,发现了一个思路简单,代码较为暴力的数据结构——“块状链表”。
然后就愉快的做(抄)完了,首先感谢@HenryHuang的题解,他的题解使我学习块状链表得到了很大帮助。
我才不会告诉你很久以前我以为这是STL中string的练手题。
分析
首先这道题有一个光标,只需用一个变量就可以维护光标。
然后就只剩下Insert、Delete、Get操作了,一般我们都会想到平衡树(Splay),但是难理解且代码复杂。
首先我们都听说过甚至用过链表,STL中的list就是使用链表实现。这种数据结构只能$ O(1) $获得首节点和尾结点,需要逐个遍历才能获得中间的节点。
不过链表也有优点,由于它的结构为一条链,每个节点维护了前节点指针和后节点指针,因此向中间插入一个数的复杂度为$ O(1) $。
然而对于这道题,使用链表并不可行,数据范围太大。
还有一个最简单最常见的数据结构,名叫数组。这个数据结构支持$ O(1) $访问。STL中有封装好的类数组数据结构vector,其中有封装好的函数insert用来插入操作。
对于数组,插入操作只需要2次memcpy函数,一次赋值操作就能完成,但是复杂度是极高的。每次插入的理论最大复杂度是 $ O(\frac{n}{2}) $
那么这题就不能投机取巧了吗?万一把数组和链表通过一定方式结合起来,复杂度不就会均衡了吗?
众所周知,优化暴力的方法有很多,其中分块(根号)算法就非常热门。那么我们让数组的大小$ \sqrt{n} $,然后将这些数组通过链表链接起来,那复杂度不就均衡了吗。
这种数据结构就是我使用的——块状链表。首先是定义一个结构体,用来存储某一节点的信息。我们只需要维护一个指向下一元素指针即可。
题目中说明了最大数据范围:
所有 INSERT 插入的字符数之和不超过 2M(1M=1024*1024 字节) ,正确的输出文件长度不超过 3M 字节。
所以我设置每个块的大小为1300,其实块大小在一定范围内即可。在这范围内,块大小和效率只有微小的关系。
1 | const int sqrtn= 1300; |
内存池
为了节点数组循环利用,学习原题解的做法,使用内存池。注意,程序开始时需要初始化内存池。
1 | int blocks[sqrtn << 2], bp, cur; |
肯定会有人问,块状链表怎么维护呢?这是最核心的部分。
Split
当某一个块较大时我们需要分裂成两个块。在某些操作(插入、删除等)时,我们可以先从光标位置分裂这个块,然后就可以很方便的对后面的内容进行操作。
1 | // x 代表块编号,p代表分裂的位置(相对于块x) |
Add
上面的split函数中用到了add,这个函数是用来在某一块后面插入一个块。但add函数用作插入,那样就不能维护块状链表的性质了。
add函数与链表的插入类似,只不过赋值变成了memcpy函数。
1 | // x 代表块编号 |
Merge
如果两个相邻的块都很小,那么维护时就要需要合并。我选择将靠后的块合并到靠前的块中,这样比较简单。
1 | // x 代表块的编号 |
何时维护
其实我学习的时候,一开始也对维护有些不解。虽然知道了如何维护,但没有具体的思路,不知道什么时候需要维护。
根据前人的题解,维护只需要部分维护,即在插入和删除操作之后,维护相关的块。只要保证相邻两块大小之和大于$ \sqrt{n} $,每块大小不超过$ \sqrt{n} $,并且不考虑当块较大时的分裂操作,可以保证块的数量控制在 $ [\sqrt{n},\sqrt{2n}] $ 范围内。
这里再次感谢@HenryHuang的题解所给予的帮助。
对于本题
本题中有插入和删除操作,两者类似。先得到光标当前所属的块,将此块在光标位置分裂。
对于插入,N 代表要插入的字符串长度。我们在分裂的位置先插入$ \frac{N}{\sqrt{n}} $个长度为$ \sqrt{n} $的块,剩下不足$ \sqrt{n} $长度的再独自插入,因为思路简单就不做解释。然后判断大小合并分裂位置前的块$ x $与$ x $的next块,合并刚插入的最后一个块$ y $与$ y $的next块。
对于删除,N 代表要删除的长度。在分裂的位置后逐个删除回收即可,如果最后一个块的长度大于剩余删除数,就再次分裂最后一个块,然后删除。
这部分代码将最后给出,先给出遍历块查找当前光标位置的代码。
1 | // x 为要找的位置, |
对于Get操作更简单,不需要分裂操作,我为了方便直接在函数中输出了。推荐学习者自行构思,有助于对块状链表的理解。
提示,此题读入较大,建议使用极速IO优化。
代码
1 | #include <iostream> |