{ }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);}控制台.输出(쓰는번호.数量);