{ }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 を一回むだにします。
③ redo した編集を元に戻すスタックに戻さないと、その編集はもう元に戻せません。
言語ごとにスタックと文字列の扱いが違います。
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");