题目地址:
https://www.acwing.com/problem/content/949/
很久很久以前,DOS3.x的程序员们开始对EDLIN感到厌倦。于是,人们开始纷纷改用自己写的文本编辑器⋯⋯多年之后,出于偶然的机会,小明找到了当时的一个编辑软件。进行了一些简单的测试后,小明惊奇地发现:那个软件每秒能够进行上万次编辑操作(当然,你不能手工进行这样的测试)!于是,小明废寝忘食地想做一个同样的东西出来。你能帮助他吗?为了明确目标,小明对“文本编辑器”做了一个抽象的定义:
文本:由
0
0
0个或多个ASCII码在闭区间
[
32
,
126
]
[32,126]
[32,126]内的字符构成的序列。
光标:在一段文本中用于指示位置的标记,可以位于文本首部,文本尾部或文本的某两个字符之间。
文本编辑器:由一段文本和该文本中的一个光标组成的,支持如下操作的数据结构。如果这段文本为空,我们就说这个文本编辑器是空的。
比如一个空的文本编辑器依次执行操作INSERT(13, “Balanced tree”), MOVE(2),DELETE(5),NEXT(),INSERT(7, “editor”),MOVE(0),GET(16)后,会输出 Bad editor tree。
你的任务是:
建立一个空的文本编辑器。
从输入文件中读入一些操作并执行。
对所有执行过的GET操作,将指定的内容写入输出文件。
输入格式:
输入文件的第一行是指令条数
t
t
t,以下是需要执行的
t
t
t个操作。
其中:
为了使输入文件便于阅读,Insert操作的字符串中可能会插入一些回车符,请忽略掉它们(如果难以理解这句话,可以参照样例)。
除了回车符之外,输入文件的所有字符的 ASCII 码都在闭区间
[
32
,
126
]
[32,126]
[32,126]内。且行尾没有空格。
这里我们有如下假定:
MOVE操作不超过
50000
50000
50000个,INSERT和DELETE操作的总个数不超过
4000
4000
4000,PREV和NEXT操作的总个数不超过
200000
200000
200000。
所有 INSERT 插入的字符数之和不超过
2
M
2M
2M(
1
M
=
1024
×
1024
1M=1024×1024
1M=1024×1024字节),正确的输出文件长度不超过
3
M
3M
3M字节。
DELETE操作和GET操作执行时光标后必然有足够的字符。MOVE、PREV、NEXT操作必然不会试图把光标移动到非法位置。
输入文件没有错误。
输出格式:
输出文件的每行依次对应输入文件中每条Get指令的输出。
可以用块状链表来做。块状链表也是用的分块的思想,不同于普通分块的是,块状链表将每个块用双向链表的方式进行连接,从而可以实现快速的插入和删除整块的操作。与普通分块相同,每块的大小大概是
O
(
n
)
O(\sqrt n)
O(n),其中
n
n
n为文本总长度。我们估计好每个整块的大小之后,可以先开一个哨兵节点(这个节点视为第
0
0
0个分块),存一个字符>
,然后用两个变量
x
x
x和
y
y
y记录当前光标在哪个分块的第几个字符后面。每个分块节点里要存该分块存的字符串,该分块的上一个和下一个节点的下标,还有该分块存的字符串长度。考虑每个操作:
1、MOVE
k
k
k。从第
1
1
1个分块开始向后数
k
k
k个字符,如果
k
k
k大于当前块的字符串长度,则略过整块;否则光标就应该停在当前分块中;
2、INSERT
n
n
n。如果光标不在其所在分块的最后一个字符处,则需要分裂操作。将光标所在节点分裂为两个节点,使得光标恰好位于分裂出的第一个节点的最后字符的位置,这样插入
n
n
n个字符就可以通过直接new出若干个新节点,然后整块整块的处理。
3、DELETE
n
n
n。先看光标所在分块,如果这个分块内有
n
n
n个字符可以删,则直接删,操作完成;否则先删掉该分块的剩余字符,然后向后遍历分块,如果还需要删的字符数多于分块拥有的字符数,则直接删整块,直到删到最后一个分块为止。
4、GET
n
n
n。与DELETE类似。
前移和后移都比较简单。
此外需要注意,需要适时的做合并操作,以防每个分块的大小过小,这样操作时间有可能退化为线性。合并的方式是直接遍历所有分块,如果发现其与后一个分块能合并(即这两个分块的字符数量之和小于分块大小的最大值),则合并。这样保证了每个分块的字符数量在平方根级别,同时也保证了分块总数也在平方根级别。
不要每次都new出节点,这样浪费空间,而是要将删掉的节点回收起来,需要节点的时候直接去回收站找。
代码如下:
#include <iostream>
#include <cstring>
using namespace std;
// N是每个分块的最大字符数,M是分块数量
const int N = 2000, M = 2010;
int n, x, y;
struct Node {
char s[N + 1];
int c, l, r;
} p[M];
char str[2000010];
// 节点回收站
int q[M], tt;
// 将光标移到第k个字符处
void move(int k) {
x = p[0].r;
// 整块可以跳着移动
while (k > p[x].c) k -= p[x].c, x = p[x].r;
y = k;
}
// 在x节点后面插入u节点
void add(int x, int u) {
p[u].r = p[x].r, p[p[u].r].l = u;
p[x].r = u, p[u].l = x;
}
// 删除u节点
void del(int u) {
p[p[u].l].r = p[u].r;
p[p[u].r].l = p[u].l;
p[u].l = p[u].r = p[u].c = 0;
// u节点被删除了,放入回收站以供将来使用
q[tt++] = u;
}
// 插入str[1:k]
void insert(int k) {
// 如果光标不在其所在分块的最后一个字符,则将其所在分块分裂
if (y < p[x].c) {
int u = q[--tt];
// 将y后面的字符放到新节点里
for (int i = y + 1; i <= p[x].c; i++)
p[u].s[++p[u].c] = p[x].s[i];
// 当前分块的字符数要更新
p[x].c = y;
// 把新节点放在x的后面
add(x, u);
}
int cur = x;
// 每次拿一个新节点,然后做填充,直到填充完k个字符
for (int i = 1; i <= k; ) {
int u = q[--tt];
while (p[u].c < N && i <= k)
p[u].s[++p[u].c] = str[i++];
add(cur, u);
cur = u;
}
}
// 删除光标后k个字符
void remove(int k) {
if (p[x].c - y >= k) {
// 如果光标所在分块还有k个字符可以删,则直接删
for (int i = y + k + 1, j = y + 1; i <= p[x].c; )
p[x].s[j++] = p[x].s[i++];
p[x].c -= k;
} else {
// 否则先删当前分块的光标后面的字符
k -= p[x].c - y;
p[x].c = y;
// 整块整块删
while (p[x].r && k >= p[p[x].r].c) {
int u = p[x].r;
k -= p[u].c;
del(u);
}
// 删剩余字符
int u = p[x].r;
for (int i = 1, j = k + 1; j <= p[u].c; ) p[u].s[i++] = p[u].s[j++];
p[u].c -= k;
}
}
// 输出光标后k个字符
void get(int k) {
if (p[x].c - y >= k)
// 如果当前分块后面刚好够k个字符,则直接输出
for (int i = y + 1; i <= y + k; i++) putchar(p[x].s[i]);
else {
// 否则先输出当前分块的光标后的字符
for (int i = y + 1; i <= p[x].c; i++) putchar(p[x].s[i]);
k -= p[x].c - y;
int cur = x;
// 整块整块输出
while (p[cur].r && k >= p[p[x].r].c) {
int u = p[cur].r;
for (int i = 1; i <= p[u].c; i++) putchar(p[u].s[i]);
k -= p[u].c;
cur = u;
}
// 输出剩余字符
int u = p[cur].r;
for (int i = 1; i <= k; i++) putchar(p[u].s[i]);
}
// 输出一个换行符
puts("");
}
void prev() {
if (y > 1) y--;
else {
x = p[x].l;
y = p[x].c;
}
}
void next() {
if (y < p[x].c) y++;
else {
x = p[x].r;
y = 1;
}
}
void merge() {
// 扫描所有节点
for (int i = p[0].r; i; i = p[i].r) {
// 如果当前节点和后面的一个节点可以合并,则合并到当前节点中去,否则遍历下一个节点
while (p[i].r && p[i].c + p[p[i].r].c <= N) {
int r = p[i].r;
for (int j = p[i].c + 1, k = 1; k <= p[r].c; )
p[i].s[j++] = p[r].s[k++];
// 注意,如果光标所在的节点被合并到前面去了,则要更新光标位置
if (x == r) x = i, y += p[i].c;
p[i].c += p[r].c;
del(r);
}
}
}
int main() {
for (int i = 1; i < M; i++) q[tt++] = i;
scanf("%d", &n);
// 插入一个哨兵
str[1] = '>';
insert(1);
move(1);
char op[10];
while (n--) {
int a;
scanf("%s", op);
if (!strcmp(op, "Move")) {
scanf("%d", &a);
move(a + 1);
} else if (!strcmp(op, "Insert")) {
scanf("%d", &a);
int i = 1, k = a;
while (a) {
str[i] = getchar();
if (32 <= str[i] && str[i] <= 126) i++, a--;
}
insert(k);
merge();
} else if (!strcmp(op, "Delete")) {
scanf("%d", &a);
remove(a);
merge();
} else if (!strcmp(op, "Get")) {
scanf("%d", &a);
get(a);
} else if (!strcmp(op, "Prev")) prev();
else next();
}
}
移动操作时间复杂度 O ( k ) O(\sqrt k) O(k),插入删除和输出操作时间 O ( n ) O(\sqrt n) O(n),前移后移时间 O ( 1 ) O(1) O(1),空间 O ( n ) O(n) O(n)。