{ }codelper
栈P0187

带撤销和重做的记事本

从一个空字符串开始,依次给出 m 条修改它的命令。
命令有以下四种。
'type s' 在字符串末尾接上 s。
'erase k' 从字符串末尾删去 k 个字符。
如果字符串不足 k 个字符,就把已有的字符全部删去。
'undo' 在可以撤销的编辑中,撤销最后做(或最后重做)的那一个。
'redo' 在可以重做的编辑中,重做最后被撤销的那一个。
这里的编辑指 type,以及实际删去了至少一个字符的 erase。
字符串为空时的 erase 什么也不做,不算编辑。
做了一个编辑后,它成为可以撤销的编辑。
被 undo 撤销的编辑不能再撤销,成为可以重做的编辑。
被 redo 重做的编辑不能再重做,又成为可以撤销的编辑。
撤销 erase 时,把当时删去的字符重新接到末尾;重做时,再从末尾删去同样多的字符。
做了新的编辑(type,或删去了字符的 erase)后,之前所有可以重做的编辑都会消失。
undo 和 redo 不是新编辑,所以不会清除可以重做的编辑。
没有可撤销的编辑时的 undo,以及没有可重做的编辑时的 redo,什么也不做。
输出执行完所有命令后的字符串。
如果字符串为空,输出 EMPTY。

输入
第一行给出 m,接下来 m 行每行一条命令。
1 ≤ m ≤ 10,000。
type 的 s 是 1~5 个英文小写字母,erase 的 k 是 1 到 100 之间的整数。
输出
最终的字符串(英文小写字母)或 EMPTY
样例 1 · stdin
7
type hello
erase 2
type p
undo
undo
redo
type y
stdout
hely
样例 2 · stdin
5
type ab
undo
erase 3
redo
redo
stdout
ab
main.py
输入(stdin)

运行时这些行会依次作为 input()/标准输入传入(运行中不会等待输入)。

$ 就绪

请在电脑上打开以编写和运行代码。在手机上可以查看题目和题解。

其他语言怎么写?查看各语言题解 · 8 种语言题解

撤销和重做就是两个栈。
做了编辑就压进撤销栈;undo 取出撤销栈的栈顶,撤销后压进重做栈;redo 反过来。
最后做的最先取回(LIFO),正好是栈。
诀窍是把编辑记成"做了什么":type 记下接上的字符串,erase 要记下实际删去的字符串,撤销时才能接回去(按实际删去的字符数,而不是要求的 k)。
常见陷阱有三个。
① 做了新编辑却不清空重做栈,分叉出去的旧记录会通过 redo 复活。
② 把空字符串上的 erase 记成编辑,什么都没做的命令就会清掉重做栈,还白白耗掉一次 undo。
③ 不把重做的编辑放回撤销栈,这个编辑就再也不能撤销。
各语言处理栈和字符串的方法不同。
Python 用列表 append/pop 和字符列表(text.extend(s)、del text[len(text) - k:]),Java 用 ArrayDeque<Edit>(push/pop)和 record Edit(boolean typed, String s),用 StringBuilder.setLength 截掉末尾。
C# 的 Stack<Edit> 用 TryPop(out var e) 自动跳过空栈,用 StringBuilder.Length -= k 截掉末尾。
C++ 用 std::stack(取出要先 top() 再 pop() 两步)和 std::string::erase,清空是 redo = {}。
C 把编辑放在结构体数组里,两个栈只放编号;字符串用固定缓冲区加长度变量,截掉末尾只要减小长度。
JavaScript、TypeScript 用数组 push/pop(空数组的 pop() 得到 undefined)和 slice,清空是 length = 0。
Craykon 把编辑存成 ["type", s] 这样的列表,栈用 添加 和 按位置移除(数量 - 1),清空用 清空(),截掉末尾用 text.截取(0, text.长度 - k)。

Pythonmain.py · 50 行
import sys lines = sys.stdin.read().split("\n")m = int(lines[0])text = []       # 지금 문자열(글자 목록: 끝에 붙이고 끝에서 지우기가 빠르다)undo = []       # 되돌릴 수 있는 편집들: ("type", 붙인 문자열) 또는 ("erase", 지운 문자열)redo = []       # 되돌렸다가 다시 할 수 있는 편집들  def do(kind, s):    if kind == "type":        text.extend(s)    else:        del text[len(text) - len(s):]  def undo_edit(kind, s):    if kind == "type":        del text[len(text) - len(s):]    else:        text.extend(s)  for line in lines[1:1 + m]:    parts = line.split()    cmd = parts[0]    if cmd == "type":        do("type", parts[1])        undo.append(("type", parts[1]))        redo.clear()  # 새 편집을 하면 다시 하기 목록은 사라진다    elif cmd == "erase":        k = min(int(parts[1]), len(text))        if k == 0:            continue  # 지운 글자가 없으면 편집이 아니다(기록도 안 하고 다시 하기도 그대로)        removed = "".join(text[len(text) - k:])        do("erase", removed)        undo.append(("erase", removed))        redo.clear()    elif cmd == "undo":        if undo:            edit = undo.pop()            undo_edit(*edit)            redo.append(edit)    elif cmd == "redo":        if redo:            edit = redo.pop()            do(*edit)            undo.append(edit) print("".join(text) if text else "EMPTY")
JavaScriptmain.js · 45 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const m = Number(lines[0]);let text = "";const undo = []; // 되돌릴 수 있는 편집들: { kind: "type" | "erase", s: 붙이거나 지운 문자열 }const redo = []; // 되돌렸다가 다시 할 수 있는 편집들 function apply(edit) {  if (edit.kind === "type") text += edit.s;  else text = text.slice(0, text.length - edit.s.length);} function revert(edit) {  if (edit.kind === "type") text = text.slice(0, text.length - edit.s.length);  else text += edit.s;} for (let i = 1; i <= m; i++) {  const [cmd, arg] = lines[i].trim().split(" ");  if (cmd === "type") {    const edit = { kind: "type", s: arg };    apply(edit);    undo.push(edit);    redo.length = 0; // 새 편집을 하면 다시 하기 목록은 사라진다  } else if (cmd === "erase") {    const k = Math.min(Number(arg), text.length);    if (k === 0) continue; // 지운 글자가 없으면 편집이 아니다(기록도 안 하고 다시 하기도 그대로)    const edit = { kind: "erase", s: text.slice(text.length - k) };    apply(edit);    undo.push(edit);    redo.length = 0;  } else if (cmd === "undo") {    const edit = undo.pop();    if (edit !== undefined) {      revert(edit);      redo.push(edit);    }  } else if (cmd === "redo") {    const edit = redo.pop();    if (edit !== undefined) {      apply(edit);      undo.push(edit);    }  }}console.log(text.length > 0 ? text : "EMPTY");
TypeScriptmain.ts · 52 行
import { readFileSync } from "node:fs"; interface Edit {  kind: "type" | "erase";  s: string; // 붙이거나 지운 문자열} const lines: string[] = readFileSync(0, "utf8").split("\n");const m = Number(lines[0]);let text = "";const undo: Edit[] = []; // 되돌릴 수 있는 편집들const redo: Edit[] = []; // 되돌렸다가 다시 할 수 있는 편집들 function apply(edit: Edit): void {  if (edit.kind === "type") text += edit.s;  else text = text.slice(0, text.length - edit.s.length);} function revert(edit: Edit): void {  if (edit.kind === "type") text = text.slice(0, text.length - edit.s.length);  else text += edit.s;} for (let i = 1; i <= m; i++) {  const [cmd, arg] = lines[i].trim().split(" ");  if (cmd === "type") {    const edit: Edit = { kind: "type", s: arg };    apply(edit);    undo.push(edit);    redo.length = 0; // 새 편집을 하면 다시 하기 목록은 사라진다  } else if (cmd === "erase") {    const k = Math.min(Number(arg), text.length);    if (k === 0) continue; // 지운 글자가 없으면 편집이 아니다(기록도 안 하고 다시 하기도 그대로)    const edit: Edit = { kind: "erase", s: text.slice(text.length - k) };    apply(edit);    undo.push(edit);    redo.length = 0;  } else if (cmd === "undo") {    const edit = undo.pop();    if (edit !== undefined) {      revert(edit);      redo.push(edit);    }  } else if (cmd === "redo") {    const edit = redo.pop();    if (edit !== undefined) {      apply(edit);      undo.push(edit);    }  }}console.log(text.length > 0 ? text : "EMPTY");
JavaMain.java · 62 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayDeque; public class Main {    // 편집 하나: 붙였는지(type) 지웠는지(erase)와 그 문자열    record Edit(boolean typed, String s) {}     static StringBuilder text = new StringBuilder();     static void apply(Edit e) {        if (e.typed()) text.append(e.s());        else text.setLength(text.length() - e.s().length());    }     static void revert(Edit e) {        if (e.typed()) text.setLength(text.length() - e.s().length());        else text.append(e.s());    }     public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        int m = Integer.parseInt(in.readLine().trim());        ArrayDeque<Edit> undo = new ArrayDeque<>(); // 되돌릴 수 있는 편집들        ArrayDeque<Edit> redo = new ArrayDeque<>(); // 되돌렸다가 다시 할 수 있는 편집들        for (int i = 0; i < m; i++) {            String[] parts = in.readLine().trim().split(" ");            switch (parts[0]) {                case "type" -> {                    Edit e = new Edit(true, parts[1]);                    apply(e);                    undo.push(e);                    redo.clear(); // 새 편집을 하면 다시 하기 목록은 사라진다                }                case "erase" -> {                    int k = Math.min(Integer.parseInt(parts[1]), text.length());                    if (k == 0) continue; // 지운 글자가 없으면 편집이 아니다(기록도 안 하고 다시 하기도 그대로)                    Edit e = new Edit(false, text.substring(text.length() - k));                    apply(e);                    undo.push(e);                    redo.clear();                }                case "undo" -> {                    if (!undo.isEmpty()) {                        Edit e = undo.pop();                        revert(e);                        redo.push(e);                    }                }                case "redo" -> {                    if (!redo.isEmpty()) {                        Edit e = redo.pop();                        apply(e);                        undo.push(e);                    }                }            }        }        System.out.println(text.length() > 0 ? text : "EMPTY");    }}
C#Main.cs · 70 行
using System;using System.Collections.Generic;using System.Text; class Program{    // 편집 하나: 붙였는지(type) 지웠는지(erase)와 그 문자열    record Edit(bool Typed, string S);     static readonly StringBuilder text = new StringBuilder();     static void Apply(Edit e)    {        if (e.Typed) text.Append(e.S);        else text.Length -= e.S.Length;    }     static void Revert(Edit e)    {        if (e.Typed) text.Length -= e.S.Length;        else text.Append(e.S);    }     static void Main()    {        int m = int.Parse(Console.ReadLine().Trim());        var undo = new Stack<Edit>(); // 되돌릴 수 있는 편집들        var redo = new Stack<Edit>(); // 되돌렸다가 다시 할 수 있는 편집들        for (int i = 0; i < m; i++)        {            string[] parts = Console.ReadLine().Trim().Split(' ');            switch (parts[0])            {                case "type":                {                    var e = new Edit(true, parts[1]);                    Apply(e);                    undo.Push(e);                    redo.Clear(); // 새 편집을 하면 다시 하기 목록은 사라진다                    break;                }                case "erase":                {                    int k = Math.Min(int.Parse(parts[1]), text.Length);                    if (k == 0) break; // 지운 글자가 없으면 편집이 아니다(기록도 안 하고 다시 하기도 그대로)                    var e = new Edit(false, text.ToString(text.Length - k, k));                    Apply(e);                    undo.Push(e);                    redo.Clear();                    break;                }                case "undo":                    if (undo.TryPop(out var u))                    {                        Revert(u);                        redo.Push(u);                    }                    break;                case "redo":                    if (redo.TryPop(out var r))                    {                        Apply(r);                        undo.Push(r);                    }                    break;            }        }        Console.WriteLine(text.Length > 0 ? text.ToString() : "EMPTY");    }}
C++main.cpp · 64 行
#include <algorithm>#include <iostream>#include <stack>#include <string> // 편집 하나: 붙였는지(type) 지웠는지(erase)와 그 문자열struct Edit {    bool typed;    std::string s;}; std::string text; void apply(const Edit& e) {    if (e.typed) text += e.s;    else text.erase(text.size() - e.s.size());} void revert(const Edit& e) {    if (e.typed) text.erase(text.size() - e.s.size());    else text += e.s;} int main() {    int m;    std::cin >> m;    std::stack<Edit> undo;  // 되돌릴 수 있는 편집들    std::stack<Edit> redo;  // 되돌렸다가 다시 할 수 있는 편집들    for (int i = 0; i < m; i++) {        std::string cmd;        std::cin >> cmd;        if (cmd == "type") {            Edit e{true, ""};            std::cin >> e.s;            apply(e);            undo.push(e);            redo = {};  // 새 편집을 하면 다시 하기 목록은 사라진다        } else if (cmd == "erase") {            std::size_t k;            std::cin >> k;            k = std::min(k, text.size());            if (k == 0) continue;  // 지운 글자가 없으면 편집이 아니다(기록도 안 하고 다시 하기도 그대로)            Edit e{false, text.substr(text.size() - k)};            apply(e);            undo.push(e);            redo = {};        } else if (cmd == "undo") {            if (!undo.empty()) {                Edit e = undo.top();                undo.pop();                revert(e);                redo.push(e);            }        } else if (cmd == "redo") {            if (!redo.empty()) {                Edit e = redo.top();                redo.pop();                apply(e);                undo.push(e);            }        }    }    std::cout << (text.empty() ? "EMPTY" : text) << "\n";}
Cmain.c · 82 行
#include <stdio.h>#include <string.h> #define MAXM 10000#define MAXTEXT 50000 /* 편집 하나: 붙였는지(type) 지웠는지(erase)와 그 문자열(많아야 100자) */typedef struct {    int typed;    int len;    char s[101];} Edit; static char text[MAXTEXT + 1];static int textLen = 0;static Edit edits[MAXM];             /* 편집은 명령마다 많아야 하나 생긴다 */static int undoStack[MAXM], undoTop = 0; /* 되돌릴 수 있는 편집들(edits의 번호) */static int redoStack[MAXM], redoTop = 0; /* 되돌렸다가 다시 할 수 있는 편집들 */ static void apply(const Edit *e) {    if (e->typed) {        memcpy(text + textLen, e->s, e->len);        textLen += e->len;    } else {        textLen -= e->len;    }} static void revert(const Edit *e) {    if (e->typed) {        textLen -= e->len;    } else {        memcpy(text + textLen, e->s, e->len);        textLen += e->len;    }} int main(void) {    int m, count = 0;    char cmd[16], arg[16];    scanf("%d", &m);    for (int i = 0; i < m; i++) {        scanf("%15s", cmd);        if (strcmp(cmd, "type") == 0) {            scanf("%15s", arg);            Edit *e = &edits[count];            e->typed = 1;            e->len = (int)strlen(arg);            memcpy(e->s, arg, e->len);            apply(e);            undoStack[undoTop++] = count++;            redoTop = 0; /* 새 편집을 하면 다시 하기 목록은 사라진다 */        } else if (strcmp(cmd, "erase") == 0) {            int k;            scanf("%d", &k);            if (k > textLen) k = textLen;            if (k == 0) continue; /* 지운 글자가 없으면 편집이 아니다(기록도 안 하고 다시 하기도 그대로) */            Edit *e = &edits[count];            e->typed = 0;            e->len = k;            memcpy(e->s, text + textLen - k, k);            apply(e);            undoStack[undoTop++] = count++;            redoTop = 0;        } else if (strcmp(cmd, "undo") == 0) {            if (undoTop > 0) {                int id = undoStack[--undoTop];                revert(&edits[id]);                redoStack[redoTop++] = id;            }        } else if (strcmp(cmd, "redo") == 0) {            if (redoTop > 0) {                int id = redoStack[--redoTop];                apply(&edits[id]);                undoStack[undoTop++] = id;            }        }    }    text[textLen] = '\0';    puts(textLen > 0 ? text : "EMPTY");    return 0;}
Craykonmain.cray · 84 行
// 편집 하나는 [종류, 문자열]: 종류는 "type"(붙임) 또는 "erase"(지움)text = "";undo = []; // 되돌릴 수 있는 편집들redo = []; // 되돌렸다가 다시 할 수 있는 편집들 붙이기(s){    text = text + s;} 끝지우기(개수){    text = text.截取(0, text.长度 - 개수);} 하기(edit){    如果 (edit[0] == "type")    {        붙이기(edit[1]);    }    否则    {        끝지우기(edit[1].长度);    }} 되돌리기(edit){    如果 (edit[0] == "type")    {        끝지우기(edit[1].长度);    }    否则    {        붙이기(edit[1]);    }} m = 控制台.读行().去空白().转数字();循环 (i = 0; i < m; i++){    parts = 控制台.读行().去空白().分割(" ");    cmd = parts[0];    如果 (cmd == "type")    {        edit = ["type", parts[1]];        하기(edit);        undo.添加(edit);        redo.清空(); // 새 편집을 하면 다시 하기 목록은 사라진다    }    否则 如果 (cmd == "erase")    {        k = 数学.较小值(parts[1].转数字(), text.长度);        如果 (k > 0)        {            edit = ["erase", text.截取(text.长度 - k)];            하기(edit);            undo.添加(edit);            redo.清空();        }    }    否则 如果 (cmd == "undo")    {        如果 (undo.数量 > 0)        {            edit = undo[undo.数量 - 1];            undo.按位置移除(undo.数量 - 1);            되돌리기(edit);            redo.添加(edit);        }    }    否则 如果 (cmd == "redo")    {        如果 (redo.数量 > 0)        {            edit = redo[redo.数量 - 1];            redo.按位置移除(redo.数量 - 1);            하기(edit);            undo.添加(edit);        }    }}控制台.输出(text.长度 > 0 ? text : "EMPTY");