【ACWing】947. 文本编辑器

题目地址:

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)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值