{ }codelper
標準コンテナP0288

新しいロッカーの両隣

廊下に番号のついたロッカーが番号順に一列に並んでいる。
はじめは使っているロッカーが一つもない。
n 人の生徒が順番に来て、ロッカーの番号 x を一つずつ選ぶ。
x がすでに使っているロッカーなら "使用中" を出力し、何も変えない。
そうでなければ、それまでに使っているロッカーのうち両隣の番号 L と R を空白一つで区切って一行に出力し、そのあと x を使っているロッカーにする。
L は、使っているロッカーのうち番号が x より小さいものの中でいちばん大きい番号である。
R は、使っているロッカーのうち番号が x より大きいものの中でいちばん小さい番号である。
そのようなロッカーがなければ、その位置に数字の代わりに -(マイナス記号一つ)を書く。
最後の行には使っているロッカーの数を出力せよ。
番号をいつも整列したまま入れておける標準ライブラリのコンテナがある言語なら、それを使ってみよう。

入力
一行目に n(1 ≤ n ≤ 2,000)、続く n 行に x が一つずつ与えられる(1 ≤ x ≤ 1,000,000,000)。
番号はコンマなしで数字だけで書かれる。
出力
n + 1 行。
はじめの n 行は生徒ごとに "使用中" または "L R"、最後の行は使っているロッカーの数
例 1 · stdin
8
50
20
80
50
9
100
35
20
stdout
- -
- 50
50 -
使用中
- 20
80 -
20 50
使用中
6
main.py
入力(stdin)

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

$ 準備完了

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

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

「いつも整列している集合」が標準ライブラリにある言語とない言語が分かれる問題です。
Java TreeSet は lower(x)(x より小さいものの中で最大)と higher(x) がそのまま答えで、なければ null を返します。
C++ std::set は lower_bound(x) が x 以上の最初の位置を返すのでそれが右隣、std::prev(it) が左隣です。
C# SortedSet<int> には前後の値を返すメソッドがないので、GetViewBetween(下, 上) で区間を切り出して Max・Min を見ます。
Python には整列した集合がなく、整列した list に bisect モジュール(bisect_left で位置を探し、insert や insort で挿入)を使います。
JavaScript・TypeScript の Set と Craykon のリストは入れた順を保つだけで整列しないので、一つずつ見て「x より小さいものの中で最大の値」を自分で探します。
C にはコンテナがまったくないので、配列に入れた順のまま置いておき、同じように一つずつ見ていきます。
ここでは n が小さいので全部見ても間に合いますが、整列した集合なら一回の問いが log n 歩ほどで済みます。
落とし穴は六つ:① Java floor(x)・ceiling(x) は「等しいか」小さい・大きい値なので、先に add してから聞くと x 自身が返ります。
② C++ で it が begin() なら左隣はありません。
集合が空かどうかだけを見ると、いちばん小さい番号が来たときに間違え、begin() で std::prev をすると未定義動作です。
③ Python で位置 i が 0 のとき、used[i - 1] はエラーではなく末尾の値 used[-1] です。
④ C# で空の SortedSet<int> の Max・Min はエラーなしに 0 を返します。
先に Count を見ます。
⑤ JS の配列の sort() は比較関数がないと数を文字列に変えて辞書順に並べるので、10 が 9 より前に来ます。
sort((a, b) => a - b) と書きます。
⑥ リストは集合と違って同じ値をもう一度受け入れます。
すでにあるかを自分で聞かないと "taken" が抜け、個数が増えます(Java の add はすでにあれば false を返します)。

Pythonmain.py · 15 行
from bisect import bisect_left n = int(input())used = []  # 쓰는 사물함 번호를 늘 작은 것부터 둔다for _ in range(n):    x = int(input())    i = bisect_left(used, x)  # x가 들어갈 자리: 그 왼쪽은 모두 x보다 작다    if i < len(used) and used[i] == x:        print("使用中")        continue    left = used[i - 1] if i > 0 else "-"  # i가 0일 때 used[-1]은 맨 끝 값이라 따로 본다    right = used[i] if i < len(used) else "-"    print(left, right)    used.insert(i, x)print(len(used))
JavaScriptmain.js · 22 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const n = Number(lines[0]);// JavaScript에는 정렬된 집합이 없어서, 쓰는 번호를 Set에 두고 양옆 이웃은 하나씩 훑어 찾는다const used = new Set();const out = [];for (let i = 1; i <= n; i++) {  const x = Number(lines[i]);  if (used.has(x)) {    out.push("使用中");    continue;  }  let left = null; // x보다 작은 번호 가운데 가장 큰 것  let right = null; // x보다 큰 번호 가운데 가장 작은 것  for (const y of used) {    if (y < x && (left === null || y > left)) left = y;    if (y > x && (right === null || y < right)) right = y;  }  out.push(`${left ?? "-"} ${right ?? "-"}`);  used.add(x);}out.push(used.size);console.log(out.join("\n"));
TypeScriptmain.ts · 23 行
import { readFileSync } from "node:fs";const lines: string[] = readFileSync(0, "utf8").split("\n");const n = Number(lines[0]);// TypeScript에도 정렬된 집합이 없어서, 쓰는 번호를 Set에 두고 양옆 이웃은 하나씩 훑어 찾는다const used = new Set<number>();const out: string[] = [];for (let i = 1; i <= n; i++) {  const x = Number(lines[i]);  if (used.has(x)) {    out.push("使用中");    continue;  }  let left: number | null = null; // x보다 작은 번호 가운데 가장 큰 것  let right: number | null = null; // x보다 큰 번호 가운데 가장 작은 것  for (const y of used) {    if (y < x && (left === null || y > left)) left = y;    if (y > x && (right === null || y < right)) right = y;  }  out.push(`${left ?? "-"} ${right ?? "-"}`);  used.add(x);}out.push(String(used.size));console.log(out.join("\n"));
JavaMain.java · 27 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.TreeSet; public class Main {    public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        int n = Integer.parseInt(in.readLine().trim());        TreeSet<Integer> used = new TreeSet<>(); // 늘 정렬되어 있는 집합        StringBuilder out = new StringBuilder();        for (int i = 0; i < n; i++) {            int x = Integer.parseInt(in.readLine().trim());            if (used.contains(x)) {                out.append("使用中\n");                continue;            }            Integer left = used.lower(x); // x보다 작은 것 가운데 가장 큰 것, 없으면 null            Integer right = used.higher(x); // x보다 큰 것 가운데 가장 작은 것, 없으면 null            out.append(left == null ? "-" : left.toString()).append(' ');            out.append(right == null ? "-" : right.toString()).append('\n');            used.add(x);        }        out.append(used.size()).append('\n');        System.out.print(out);    }}
C#Main.cs · 31 行
using System;using System.Collections.Generic;using System.Text; class Program{    static void Main()    {        int n = int.Parse(Console.ReadLine());        var used = new SortedSet<int>(); // 늘 정렬되어 있는 집합        var sb = new StringBuilder();        for (int i = 0; i < n; i++)        {            int x = int.Parse(Console.ReadLine());            if (used.Contains(x))            {                sb.Append("使用中\n");                continue;            }            // SortedSet에는 '바로 앞·뒤 값' 메서드가 없어서, x의 왼쪽·오른쪽 구간을 잘라 보고 끝 값을 본다.            // 빈 구간의 Max·Min은 0을 돌려주므로 Count를 먼저 확인한다            var below = used.GetViewBetween(int.MinValue, x);            var above = used.GetViewBetween(x, int.MaxValue);            sb.Append(below.Count > 0 ? below.Max.ToString() : "-").Append(' ');            sb.Append(above.Count > 0 ? above.Min.ToString() : "-").Append('\n');            used.Add(x);        }        sb.Append(used.Count).Append('\n');        Console.Write(sb);    }}
C++main.cpp · 24 行
#include <iostream>#include <set> int main() {    int n;    std::cin >> n;    std::set<int> used;  // 늘 정렬되어 있는 집합    for (int i = 0; i < n; i++) {        int x;        std::cin >> x;        auto it = used.lower_bound(x);  // x 이상인 값이 처음 나오는 자리        if (it != used.end() && *it == x) {            std::cout << "使用中\n";            continue;        }        // it이 오른쪽 이웃이고, 그 바로 앞이 왼쪽 이웃이다. begin()보다 앞으로는 갈 수 없다        if (it != used.begin()) std::cout << *std::prev(it) << ' ';        else std::cout << "- ";        if (it != used.end()) std::cout << *it << '\n';        else std::cout << "-\n";        used.insert(it, x);  // 찾은 자리를 알려 주면 바로 그 앞에 넣는다    }    std::cout << used.size() << '\n';}
Cmain.c · 34 行
#include <stdio.h> int main(void) {    int n;    scanf("%d", &n);    /* C에는 정렬된 집합이 없어서, 쓰는 번호를 배열에 두고 양옆 이웃은 하나씩 훑어 찾는다 */    static int used[2000];    int count = 0;    for (int k = 0; k < n; k++) {        int x;        scanf("%d", &x);        int taken = 0;        int left = 0;  /* x보다 작은 번호 가운데 가장 큰 것. 번호는 1부터라서 0이면 아직 없다는 뜻 */        int right = 0; /* x보다 큰 번호 가운데 가장 작은 것. 0이면 아직 없다는 뜻 */        for (int i = 0; i < count; i++) {            int y = used[i];            if (y == x) taken = 1;            if (y < x && y > left) left = y;            if (y > x && (right == 0 || y < right)) right = y;        }        if (taken) {            puts("使用中");            continue;        }        if (left != 0) printf("%d ", left);        else printf("- ");        if (right != 0) printf("%d\n", right);        else printf("-\n");        used[count] = x; /* 배열은 같은 값도 받아 주므로, 위에서 이미 있는지 먼저 확인했다 */        count++;    }    printf("%d\n", count);    return 0;}
Craykonmain.cray · 29 行
n = コンソール.行読み().数に();// 크레이콘에는 정렬된 집합이 없어서, 쓰는 번호를 목록에 두고 양옆 이웃은 하나씩 훑어 찾는다쓰는번호 = [];繰り返す (k = 0; k < n; k++){    x = コンソール.行読み().数に();    もし (쓰는번호.含む(x))    {        コンソール.書く("使用中");        次へ;    }    왼쪽 = なし; // x보다 작은 번호 가운데 가장 큰 것    오른쪽 = なし; // x보다 큰 번호 가운데 가장 작은 것    それぞれ (y の中 쓰는번호)    {        もし (y < x && (왼쪽 == なし || y > 왼쪽))        {            왼쪽 = y;        }        もし (y > x && (오른쪽 == なし || y < 오른쪽))        {            오른쪽 = y;        }    }    줄 = (왼쪽 == なし ? "-" : "" + 왼쪽) + " " + (오른쪽 == なし ? "-" : "" + 오른쪽);    コンソール.書く(줄);    쓰는번호.追加(x);}コンソール.書く(쓰는번호.個数);