{ }codelper
スタック辞書の検索64ビット整数P0305

名前つきセーブポイントで巻き戻す帳簿

1 番から s 番までの s 個の欄がある帳簿があり、最初はすべての欄の値が 0 である。
命令が m 個順に与えられ、命令は次の六種類である。
'set k v' は k 番の欄の値を v に変える。
'add k v' は k 番の欄の値に v を足す。
'get k' は k 番の欄の値を出力する。
'save 名前' は今の帳簿の状態を覚えておくセーブポイントをその名前で新しく作る。
同じ名前のセーブポイントがすでにあっても、新しくもう一つ作る。
'back 名前' は、まだ消えていないその名前のセーブポイントのうち最も後に作ったものを選び、すべての欄の値をそのセーブポイントを作ったときの値に戻す。
そのセーブポイントより後に作ったセーブポイントはすべて消え、選んだセーブポイントは残る。
'drop 名前' は、まだ消えていないその名前のセーブポイントのうち最も後に作ったものを選び、そのセーブポイントとそれより後に作ったセーブポイントをすべて消す。
欄の値は変えない。
back と drop は、処理を終えたあとに残っているセーブポイントの数(名前に関係なくすべて数えた数)を出力する。
その名前のセーブポイントが残っていなければ、back と drop は何も変えずに -1 を出力する。
出力は命令が与えられた順に一行に一つずつ行う。

入力
一行目に s と m が空白で区切られて与えられる。
次の m 行に命令が一行に一つずつ与えられ、単語の間は空白一つである。
1 ≤ s ≤ 10,000、1 ≤ m ≤ 10,000、1 ≤ k ≤ s、-1,000,000,000 ≤ v ≤ 1,000,000,000、名前は英小文字だけからなる長さ 1〜8 の文字列。
get、back、drop のうち少なくとも一つは必ず現れる
出力
get、back、drop の命令ごとに一行。
get は欄の値、back と drop は残ったセーブポイントの数または -1。
数はコンマなしの数字だけで書く
例 1 · stdin
3 14
set 1 5
save a
add 1 10
save b
set 2 7
save a
add 2 1
get 2
back a
get 2
drop a
back a
get 1
back b
stdout
8
3
7
2
1
5
-1
main.py
入力(stdin)

実行すると、これらの行が順に input()・標準入力に渡されます(実行中に入力を求めません)。

$ 準備完了

コードを入力して解くにはパソコンで開いてください。モバイルでは問題と解答を見られます。

ほかの言語では?言語別の解答を見る · 8 言語の解答

セーブポイントは後に作ったものから消えるのでスタックです。
けれども保存のたびに帳簿を丸ごとコピーすると、欄 10,000 個 × 保存 10,000 回で 1 億欄になります。
代わりにスタックをもう一つ持ちます:値を変えるたびに(欄, 変える前の値)を記録スタックに積み、セーブポイントには「そのときの記録の長さ」だけを書きます。
back は記録がその長さになるまで上から取り出して古い値を戻します。
スタックの道具は Python list の append・pop、JS・TS の配列の push・pop、C# Stack<T>、C++ vector の push_back・pop_back(途中の位置 marks[p] を読む必要があるので std::stack は使えません)、Java と C は配列と個数の変数、Craykon は 追加 と 位置で取り除く(個数 - 1) です。
TS では pop() の型が T | undefined なので、長さを確かめていても ! をつけるか別に検査しないと strict の検査を通りません。
名前でセーブポイントを探すときにスタックを上からたどると遅いので、辞書に「名前 → 一番上の位置」を書き、セーブポイントごとに「同じ名前のすぐ下の位置」を書いておきます。
落とし穴その一:記録は必ず後のものから戻します。
同じ欄を二回変えたとき、先にしたものから戻すと途中の値が残ります。
落とし穴その二:back は選んだセーブポイントを残し(同じ名前でもう一度 back できます)、drop は選んだセーブポイントまで消します。
落とし穴その三:drop はそのセーブポイント一つだけでなく、後に作ったセーブポイントも一緒に消します。
落とし穴その四:同じ名前が重なっていれば最も後のものを探し、それが消えたらその下のものがまた見えなければなりません。
セーブポイントを消すときに辞書も一緒に直さないと、もう消えたセーブポイントを見つけてしまいます。
落とし穴その五:ない名前を探すときです。
C++ unordered_map の top[name] はないキーに 0 を新しく入れて返すので、0 番のセーブポイントがあるように見えます。
find を使います。
Python の top[name] は KeyError(get(name, -1))、Java の get は null(getOrDefault)、C# の top[name] は KeyNotFoundException(TryGetValue)、JS・TS の Map.get は undefined(?? -1)、Craykon は 含む で先に尋ねます。
C には辞書がないのでハッシュ表を自分で書きます。
落とし穴その六:10 億を何回も足すと 32 ビットを超えます。
Java・C# の int はエラーなしに一周してでたらめな数になり、C・C++ では未定義動作なので、long・long long を使います。
Python の整数には上限がなく、JS・TS・Craykon の数は 2^53 まで正確です。

Pythonmain.py · 53 行
import sys  def main():    lines = sys.stdin.read().split("\n")    s, m = map(int, lines[0].split())    val = [0] * (s + 1)     log = []  # 되돌리기 기록 스택: (칸, 바꾸기 전 값)    names = []  # 저장점 스택: 이름    marks = []  # 저장점 스택: 저장할 때의 기록 길이    below = []  # 저장점 스택: 같은 이름의 바로 아래 저장점 자리(없으면 -1)    top = {}  # 이름 → 그 이름의 살아 있는 저장점 가운데 가장 위의 자리     def cut(size):        """저장점 스택을 size개만 남기고 위에서부터 없앤다."""        while len(names) > size:            top[names.pop()] = below.pop()            marks.pop()     out = []    for line in lines[1 : 1 + m]:        t = line.split()        cmd = t[0]        if cmd == "set" or cmd == "add":            k, x = int(t[1]), int(t[2])            log.append((k, val[k]))            val[k] = x if cmd == "set" else val[k] + x        elif cmd == "get":            out.append(val[int(t[1])])        elif cmd == "save":            below.append(top.get(t[1], -1))            top[t[1]] = len(names)            names.append(t[1])            marks.append(len(log))        else:            p = top.get(t[1], -1)            if p == -1:                out.append(-1)            elif cmd == "back":                # 기록을 위에서부터(나중 것부터) 꺼내며 옛 값을 되살린다                while len(log) > marks[p]:                    k, old = log.pop()                    val[k] = old                cut(p + 1)  # 그 저장점은 남는다                out.append(p + 1)            else:  # drop                cut(p)  # 그 저장점까지 없앤다(값은 그대로)                out.append(p)    print("\n".join(map(str, out)))  main()
JavaScriptmain.js · 52 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const [s, m] = lines[0].trim().split(/\s+/).map(Number);const val = new Array(s + 1).fill(0); const logSlot = []; // 되돌리기 기록 스택: 칸const logOld = []; //  되돌리기 기록 스택: 바꾸기 전 값const names = []; // 저장점 스택: 이름const marks = []; // 저장점 스택: 저장할 때의 기록 길이const below = []; // 저장점 스택: 같은 이름의 바로 아래 저장점 자리(없으면 -1)const top = new Map(); // 이름 → 그 이름의 살아 있는 저장점 가운데 가장 위의 자리 // 저장점 스택을 size개만 남기고 위에서부터 없앤다function cut(size) {  while (names.length > size) {    top.set(names.pop(), below.pop());    marks.pop();  }} const out = [];for (let i = 1; i <= m; i++) {  const t = lines[i].trim().split(/\s+/);  const cmd = t[0];  if (cmd === "set" || cmd === "add") {    const k = Number(t[1]);    const x = Number(t[2]);    logSlot.push(k);    logOld.push(val[k]);    val[k] = cmd === "set" ? x : val[k] + x;  } else if (cmd === "get") {    out.push(val[Number(t[1])]);  } else if (cmd === "save") {    below.push(top.has(t[1]) ? top.get(t[1]) : -1);    top.set(t[1], names.length);    names.push(t[1]);    marks.push(logSlot.length);  } else {    const p = top.has(t[1]) ? top.get(t[1]) : -1;    if (p === -1) {      out.push(-1);    } else if (cmd === "back") {      // 기록을 위에서부터(나중 것부터) 꺼내며 옛 값을 되살린다      while (logSlot.length > marks[p]) val[logSlot.pop()] = logOld.pop();      cut(p + 1); // 그 저장점은 남는다      out.push(p + 1);    } else {      cut(p); // drop: 그 저장점까지 없앤다(값은 그대로)      out.push(p);    }  }}console.log(out.join("\n"));
TypeScriptmain.ts · 55 行
import { readFileSync } from "node:fs"; const lines: string[] = readFileSync(0, "utf8").split("\n");const [s, m] = lines[0].trim().split(/\s+/).map(Number);const val: number[] = new Array<number>(s + 1).fill(0); const logSlot: number[] = []; // 되돌리기 기록 스택: 칸const logOld: number[] = []; //  되돌리기 기록 스택: 바꾸기 전 값const names: string[] = []; // 저장점 스택: 이름const marks: number[] = []; // 저장점 스택: 저장할 때의 기록 길이const below: number[] = []; // 저장점 스택: 같은 이름의 바로 아래 저장점 자리(없으면 -1)const top = new Map<string, number>(); // 이름 → 그 이름의 살아 있는 저장점 가운데 가장 위의 자리 // 저장점 스택을 size개만 남기고 위에서부터 없앤다function cut(size: number): void {  while (names.length > size) {    // pop()의 타입은 `T | undefined`다. 길이를 확인했으니 !로 undefined가 아님을 알린다    top.set(names.pop()!, below.pop()!);    marks.pop();  }} const out: number[] = [];for (let i = 1; i <= m; i++) {  const t = lines[i].trim().split(/\s+/);  const cmd = t[0];  if (cmd === "set" || cmd === "add") {    const k = Number(t[1]);    const x = Number(t[2]);    logSlot.push(k);    logOld.push(val[k]);    val[k] = cmd === "set" ? x : val[k] + x;  } else if (cmd === "get") {    out.push(val[Number(t[1])]);  } else if (cmd === "save") {    below.push(top.get(t[1]) ?? -1);    top.set(t[1], names.length);    names.push(t[1]);    marks.push(logSlot.length);  } else {    const p = top.get(t[1]) ?? -1;    if (p === -1) {      out.push(-1);    } else if (cmd === "back") {      // 기록을 위에서부터(나중 것부터) 꺼내며 옛 값을 되살린다      while (logSlot.length > marks[p]) val[logSlot.pop()!] = logOld.pop()!;      cut(p + 1); // 그 저장점은 남는다      out.push(p + 1);    } else {      cut(p); // drop: 그 저장점까지 없앤다(값은 그대로)      out.push(p);    }  }}console.log(out.join("\n"));
JavaMain.java · 76 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.HashMap;import java.util.StringTokenizer; public class Main {    static String[] names; // 저장점 스택: 이름    static int[] marks;    // 저장점 스택: 저장할 때의 기록 길이    static int[] below;    // 저장점 스택: 같은 이름의 바로 아래 저장점 자리(없으면 -1)    static int count = 0;  // 살아 있는 저장점 수    static HashMap<String, Integer> top = new HashMap<>(); // 이름 → 가장 위의 자리     // 저장점 스택을 size개만 남기고 위에서부터 없앤다    static void cut(int size) {        while (count > size) {            count--;            top.put(names[count], below[count]);        }    }     public static void main(String[] args) throws IOException {        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        StringTokenizer st = new StringTokenizer(br.readLine());        int s = Integer.parseInt(st.nextToken());        int m = Integer.parseInt(st.nextToken());         long[] val = new long[s + 1]; // add가 쌓이면 int를 넘는다        int[] logSlot = new int[m];   // 되돌리기 기록 스택: 칸        long[] logOld = new long[m];  // 되돌리기 기록 스택: 바꾸기 전 값        int logSize = 0;        names = new String[m];        marks = new int[m];        below = new int[m];         StringBuilder sb = new StringBuilder();        for (int i = 0; i < m; i++) {            st = new StringTokenizer(br.readLine());            String cmd = st.nextToken();            if (cmd.equals("set") || cmd.equals("add")) {                int k = Integer.parseInt(st.nextToken());                long x = Long.parseLong(st.nextToken());                logSlot[logSize] = k;                logOld[logSize] = val[k];                logSize++;                val[k] = cmd.equals("set") ? x : val[k] + x;            } else if (cmd.equals("get")) {                sb.append(val[Integer.parseInt(st.nextToken())]).append('\n');            } else if (cmd.equals("save")) {                String name = st.nextToken();                below[count] = top.getOrDefault(name, -1);                top.put(name, count);                names[count] = name;                marks[count] = logSize;                count++;            } else {                int p = top.getOrDefault(st.nextToken(), -1);                if (p == -1) {                    sb.append(-1).append('\n');                } else if (cmd.equals("back")) {                    // 기록을 위에서부터(나중 것부터) 꺼내며 옛 값을 되살린다                    while (logSize > marks[p]) {                        logSize--;                        val[logSlot[logSize]] = logOld[logSize];                    }                    cut(p + 1); // 그 저장점은 남는다                    sb.append(p + 1).append('\n');                } else {                    cut(p); // drop: 그 저장점까지 없앤다(값은 그대로)                    sb.append(p).append('\n');                }            }        }        System.out.print(sb);    }}
C#Main.cs · 83 行
using System;using System.Collections.Generic;using System.Text; class Program{    static void Main()    {        string[] first = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);        int s = int.Parse(first[0]);        int m = int.Parse(first[1]);         long[] val = new long[s + 1]; // add가 쌓이면 int를 넘는다        var log = new Stack<(int Slot, long Old)>(); // 되돌리기 기록: (칸, 바꾸기 전 값)        var names = new List<string>(); // 저장점 스택: 이름        var marks = new List<int>();    // 저장점 스택: 저장할 때의 기록 길이        var below = new List<int>();    // 저장점 스택: 같은 이름의 바로 아래 저장점 자리(없으면 -1)        var top = new Dictionary<string, int>(); // 이름 → 가장 위의 자리         // 저장점 스택을 size개만 남기고 위에서부터 없앤다        void Cut(int size)        {            while (names.Count > size)            {                int last = names.Count - 1;                top[names[last]] = below[last];                names.RemoveAt(last);                marks.RemoveAt(last);                below.RemoveAt(last);            }        }         var sb = new StringBuilder();        for (int i = 0; i < m; i++)        {            string[] t = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);            string cmd = t[0];            if (cmd == "set" || cmd == "add")            {                int k = int.Parse(t[1]);                long x = long.Parse(t[2]);                log.Push((k, val[k]));                val[k] = cmd == "set" ? x : val[k] + x;            }            else if (cmd == "get")            {                sb.Append(val[int.Parse(t[1])]).Append('\n');            }            else if (cmd == "save")            {                below.Add(top.TryGetValue(t[1], out int under) ? under : -1);                top[t[1]] = names.Count;                names.Add(t[1]);                marks.Add(log.Count);            }            else            {                int p = top.TryGetValue(t[1], out int found) ? found : -1;                if (p == -1)                {                    sb.Append(-1).Append('\n');                }                else if (cmd == "back")                {                    // 기록을 위에서부터(나중 것부터) 꺼내며 옛 값을 되살린다                    while (log.Count > marks[p])                    {                        var (k, old) = log.Pop();                        val[k] = old;                    }                    Cut(p + 1); // 그 저장점은 남는다                    sb.Append(p + 1).Append('\n');                }                else                {                    Cut(p); // drop: 그 저장점까지 없앤다(값은 그대로)                    sb.Append(p).Append('\n');                }            }        }        Console.Write(sb);    }}
C++main.cpp · 72 行
#include <iostream>#include <string>#include <unordered_map>#include <utility>#include <vector>using namespace std; vector<string> names;            // 저장점 스택: 이름vector<int> marks;               // 저장점 스택: 저장할 때의 기록 길이vector<int> below;               // 저장점 스택: 같은 이름의 바로 아래 저장점 자리(없으면 -1)unordered_map<string, int> top;  // 이름 → 그 이름의 살아 있는 저장점 가운데 가장 위의 자리 // 저장점 스택을 size개만 남기고 위에서부터 없앤다void cut(size_t size) {    while (names.size() > size) {        top[names.back()] = below.back();        names.pop_back();        marks.pop_back();        below.pop_back();    }} int main() {    ios::sync_with_stdio(false);    cin.tie(nullptr);     int s, m;    cin >> s >> m;    vector<long long> val(s + 1, 0);     // add가 쌓이면 int를 넘는다    vector<pair<int, long long>> log;    // 되돌리기 기록 스택: (칸, 바꾸기 전 값)     for (int i = 0; i < m; i++) {        string cmd;        cin >> cmd;        if (cmd == "set" || cmd == "add") {            int k;            long long x;            cin >> k >> x;            log.push_back({k, val[k]});            val[k] = cmd == "set" ? x : val[k] + x;        } else if (cmd == "get") {            int k;            cin >> k;            cout << val[k] << '\n';        } else {            string name;            cin >> name;            auto it = top.find(name);            int p = it == top.end() ? -1 : it->second;            if (cmd == "save") {                below.push_back(p);                top[name] = (int)names.size();                names.push_back(name);                marks.push_back((int)log.size());            } else if (p == -1) {                cout << -1 << '\n';            } else if (cmd == "back") {                // 기록을 위에서부터(나중 것부터) 꺼내며 옛 값을 되살린다                while ((int)log.size() > marks[p]) {                    val[log.back().first] = log.back().second;                    log.pop_back();                }                cut(p + 1); // 그 저장점은 남는다                cout << p + 1 << '\n';            } else {                cut(p); // drop: 그 저장점까지 없앤다(값은 그대로)                cout << p << '\n';            }        }    }    return 0;}
Cmain.c · 91 行
#include <stdio.h>#include <stdlib.h>#include <string.h> #define TABLE 32768 // 이름은 많아야 10,000가지라 넉넉하다(2의 거듭제곱) // 이름 → 그 이름의 살아 있는 저장점 가운데 가장 위의 자리(해시 표, 빈칸은 이름이 "")static char keys[TABLE][9];static int tops[TABLE]; // 이름이 들어 있는(또는 들어갈) 칸을 찾는다static int find(const char *name) {    unsigned h = 2166136261u;    for (const char *p = name; *p; p++) h = (h ^ (unsigned char)*p) * 16777619u;    int i = (int)(h & (TABLE - 1));    while (keys[i][0] != '\0' && strcmp(keys[i], name) != 0) i = (i + 1) & (TABLE - 1);    return i;} static int *name_cell; // 저장점 스택: 이름이 든 해시 표의 칸static int *marks;     // 저장점 스택: 저장할 때의 기록 길이static int *below;     // 저장점 스택: 같은 이름의 바로 아래 저장점 자리(없으면 -1)static int count = 0;  // 살아 있는 저장점 수 // 저장점 스택을 size개만 남기고 위에서부터 없앤다static void cut(int size) {    while (count > size) {        count--;        tops[name_cell[count]] = below[count];    }} int main(void) {    int s, m;    if (scanf("%d %d", &s, &m) != 2) return 1;     long long *val = calloc(s + 1, sizeof(long long)); // add가 쌓이면 int를 넘는다    int *log_slot = malloc(sizeof(int) * m);            // 되돌리기 기록 스택: 칸    long long *log_old = malloc(sizeof(long long) * m); // 되돌리기 기록 스택: 바꾸기 전 값    int log_size = 0;    name_cell = malloc(sizeof(int) * m);    marks = malloc(sizeof(int) * m);    below = malloc(sizeof(int) * m);     for (int i = 0; i < m; i++) {        char cmd[8];        scanf("%7s", cmd);        if (strcmp(cmd, "set") == 0 || strcmp(cmd, "add") == 0) {            int k;            long long x;            scanf("%d %lld", &k, &x);            log_slot[log_size] = k;            log_old[log_size] = val[k];            log_size++;            val[k] = cmd[0] == 's' ? x : val[k] + x;        } else if (strcmp(cmd, "get") == 0) {            int k;            scanf("%d", &k);            printf("%lld\n", val[k]);        } else {            char name[9];            scanf("%8s", name);            int cell = find(name);            int p = keys[cell][0] == '\0' ? -1 : tops[cell];            if (strcmp(cmd, "save") == 0) {                if (keys[cell][0] == '\0') strcpy(keys[cell], name);                below[count] = p;                tops[cell] = count;                name_cell[count] = cell;                marks[count] = log_size;                count++;            } else if (p == -1) {                printf("-1\n");            } else if (strcmp(cmd, "back") == 0) {                // 기록을 위에서부터(나중 것부터) 꺼내며 옛 값을 되살린다                while (log_size > marks[p]) {                    log_size--;                    val[log_slot[log_size]] = log_old[log_size];                }                cut(p + 1); // 그 저장점은 남는다                printf("%d\n", p + 1);            } else {                cut(p); // drop: 그 저장점까지 없앤다(값은 그대로)                printf("%d\n", p);            }        }    }     free(val); free(log_slot); free(log_old); free(name_cell); free(marks); free(below);    return 0;}
Craykonmain.cray · 86 行
첫줄 = コンソール.数読み();s = 첫줄[0];m = 첫줄[1];val = 新リスト(s, 0); // 칸 k는 val[k - 1]에 둔다 logSlot = []; // 되돌리기 기록 스택: 칸logOld = []; // 되돌리기 기록 스택: 바꾸기 전 값names = []; // 저장점 스택: 이름marks = []; // 저장점 스택: 저장할 때의 기록 길이below = []; // 저장점 스택: 같은 이름의 바로 아래 저장점 자리(없으면 -1)top = 辞書(); // 이름 → 그 이름의 살아 있는 저장점 가운데 가장 위의 자리 繰り返す (i = 0; i < m; i++){    t = コンソール.行読み().空白除去().分ける();    cmd = t[0];    もし (cmd == "set" || cmd == "add")    {        k = t[1].数に() - 1;        x = t[2].数に();        logSlot.追加(k);        logOld.追加(val[k]);        もし (cmd == "set")        {            val[k] = x;        }        でなければ        {            val[k] = val[k] + x;        }    }    でなければ もし (cmd == "get")    {        コンソール.書く(val[t[1].数に() - 1]);    }    でなければ    {        p = -1;        もし (top.含む(t[1]))        {            p = top[t[1]];        }        もし (cmd == "save")        {            below.追加(p);            top[t[1]] = names.個数;            names.追加(t[1]);            marks.追加(logSlot.個数);        }        でなければ もし (p == -1)        {            コンソール.書く(-1);        }        でなければ もし (cmd == "back")        {            // 기록을 위에서부터(나중 것부터) 꺼내며 옛 값을 되살린다            間 (logSlot.個数 > marks[p])            {                끝 = logSlot.個数 - 1;                val[logSlot[끝]] = logOld[끝];                logSlot.位置で取り除く(끝);                logOld.位置で取り除く(끝);            }            자르기(p + 1); // 그 저장점은 남는다            コンソール.書く(p + 1);        }        でなければ        {            자르기(p); // drop: 그 저장점까지 없앤다(값은 그대로)            コンソール.書く(p);        }    }} // 저장점 스택을 size개만 남기고 위에서부터 없앤다자르기(size){    間 (names.個数 > size)    {        끝 = names.個数 - 1;        top[names[끝]] = below[끝];        names.位置で取り除く(끝);        marks.位置で取り除く(끝);        below.位置で取り除く(끝);    }}