{ }codelper
null 値P0286

三回たどって探す電源ボックスの担当者

キャンプ場に客が n 人、区画が m 個、電源ボックスが k 個あり、それぞれ 1 番から番号がついている。
表が三つある。
一つ目の表には、客ごとに割り当てられた区画の番号が書かれている。
二つ目の表には、区画ごとにつながっている電源ボックスの番号が書かれている。
三つ目の表には、電源ボックスごとに担当の職員の名前が書かれている。
三つの表とも、決まっていない欄には番号や名前の代わりに -(マイナス記号一つ)が書かれている。
電気が切れたという客の番号が q 回与えられる。
そのたびに、客 → 区画 → 電源ボックス → 担当の職員の順に表をたどり、最後まで届いたら担当の職員の名前を出力せよ。
途中で - に出会ったらそこで止まり、- と、切れた段階の番号を空白一つで区切って出力せよ。
客に区画がなければ第 1 段階、その区画に電源ボックスがなければ第 2 段階、その電源ボックスに担当の職員がいなければ第 3 段階である。
複数の客が同じ区画を使うことがあり、複数の区画が同じ電源ボックスにつながることもある。

入力
一行目に n, m, k, q が空白一つで区切って与えられる(1 ≤ n, m, k, q ≤ 100)。
二行目に 1 番の客から順に区画の番号(1 以上 m 以下)または - が n 個、三行目に 1 番の区画から順に電源ボックスの番号(1 以上 k 以下)または - が m 個、四行目に 1 番の電源ボックスから順に担当の職員の名前(英小文字 1~8 文字)または - が k 個、それぞれ空白一つで区切って与えられる。
続く q 行に客の番号(1 以上 n 以下)が一つずつ与えられる。
出力
q 行。
各行は担当の職員の名前、または "- 段階の番号"(段階の番号は 1、2、3 のいずれか)
例 1 · stdin
5 4 3 6
2 - 4 1 3
1 3 - 2
dana - miro
1
2
3
4
5
1
stdout
miro
- 1
- 3
dana
- 2
miro
main.py
入力(stdin)

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

$ 準備完了

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

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

調べた値を次の表の番号としてもう一度使う問題なので、「ない」を番号として使うとどうなるかが言語ごとに違います。
「ない」の入れ方:Python None、JS・TS null、Java は Integer と String の null(int には入りません)、C# int?、C++ std::optional<int>、Craykon なし。
C の int には「ない」がないので、番号として使わない 0 を「ない」と決めて使い、名前はポインタなので NULL が使えます。
「ない」を番号に使うと:Python box_of[None] は TypeError、Java は null の Integer を番号に使ったり int に入れたりした瞬間に NullPointerException、Craykon は boxOf[なし] でプログラムが止まります(なし で計算しても止まります)。
TS は厳格モード(strict)で number | null を番号に使うとコンパイルエラー(TS2538)なので === null の確認を忘れられず、C# も int? をそのまま番号には使えないので .Value で取り出します。
逆に JS の boxOf[null] はエラーなしに undefined を返します。
だからどの言語でも「調べる → ないか確認 → 次を調べる」の順に書きます。
「ない」を最後まで流す道具もあります。
Java Optional.ofNullable(siteOf[g]).map(s -> boxOf[s]).map(b -> staffOf[b]) は途中で null が出ると空の Optional で終わります。
ただしどの段階で切れたかは教えてくれないので、この問題では段階ごとに確認が必要です。
JS・TS・C# の ?. は点の前のものがないときを防ぐだけで、角かっこの中の番号がないことは防げません。
落とし穴は七つ:① JS で三つの値を先に全部調べると、前の段階が空のとき後ろの値は null ではなく undefined です。
=== null には引っかかりません(== null は両方を拾います)。
② Python で「ない」を -1 としておくと、box_of[-1] がエラーなしに末尾の欄を読みます。
③ Java で int site = siteOf[g] のように null を int に入れると、0 になるのではなく例外が出ます。
④ C# GetValueOrDefault() は null を 0 に変えるので、空の段階を通り過ぎて次の段階で切れたように見えます。
⑤ C で 0 番の欄も「ない」(0)だと、「ない」が最後まで伝わります。
後ろから確認するとどこで切れても第 3 段階と答えてしまうので、前の段階から確認します。
⑥ C++ で空の optional に .value() を呼ぶと std::bad_optional_access 例外が出て、* を使うと未定義動作です。
⑦ Craykon で確認より先に調べる行を書くと、なし を番号に使う行で止まります。

Pythonmain.py · 25 行
def read_row(convert):    # "-"는 None으로, 나머지는 convert로 바꾼다. 맨 앞에 None을 하나 두어 번호 1이 1번 칸이 되게 한다    return [None] + [None if word == "-" else convert(word) for word in input().split()]  n, m, k, q = map(int, input().split())site_of = read_row(int)   # 손님 → 자리box_of = read_row(int)    # 자리 → 전기함staff_of = read_row(str)  # 전기함 → 담당자 for _ in range(q):    guest = int(input())    site = site_of[guest]    if site is None:        print("- 1")        continue    box = box_of[site]  # site가 None이면 box_of[None]은 TypeError라서, 위에서 먼저 걸렀다    if box is None:        print("- 2")        continue    staff = staff_of[box]    if staff is None:        print("- 3")    else:        print(staff)
JavaScriptmain.js · 28 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const q = Number(lines[0].split(" ")[3]); // "-"는 null로 바꾼다. 맨 앞에 null을 하나 두어 번호 1이 1번 칸이 되게 한다function readRow(line, convert) {  return [null, ...line.split(" ").map((word) => (word === "-" ? null : convert(word)))];}const siteOf = readRow(lines[1], Number); // 손님 → 자리const boxOf = readRow(lines[2], Number); // 자리 → 전기함const staffOf = readRow(lines[3], String); // 전기함 → 담당자 const out = [];for (let i = 0; i < q; i++) {  const guest = Number(lines[4 + i]);  const site = siteOf[guest];  if (site === null) {    out.push("- 1");    continue;  }  const box = boxOf[site]; // boxOf[null]은 오류 없이 undefined가 되므로, 위에서 먼저 걸렀다  if (box === null) {    out.push("- 2");    continue;  }  const staff = staffOf[box];  out.push(staff === null ? "- 3" : staff);}console.log(out.join("\n"));
TypeScriptmain.ts · 30 行
import { readFileSync } from "node:fs";const lines: string[] = readFileSync(0, "utf8").split("\n");const q = Number(lines[0].split(" ")[3]); // "-"는 null로 바꾼다. 맨 앞에 null을 하나 두어 번호 1이 1번 칸이 되게 한다function readRow<T>(line: string, convert: (word: string) => T): (T | null)[] {  return [null, ...line.split(" ").map((word) => (word === "-" ? null : convert(word)))];}const siteOf: (number | null)[] = readRow(lines[1], Number); // 손님 → 자리const boxOf: (number | null)[] = readRow(lines[2], Number); // 자리 → 전기함const staffOf: (string | null)[] = readRow(lines[3], String); // 전기함 → 담당자 const out: string[] = [];for (let i = 0; i < q; i++) {  const guest = Number(lines[4 + i]);  const site = siteOf[guest];  if (site === null) {    out.push("- 1");    continue;  }  // 여기서부터 site는 number다. 위의 검사를 빼면 boxOf[site]는 컴파일 오류(null은 번호로 쓸 수 없다)가 된다  const box = boxOf[site];  if (box === null) {    out.push("- 2");    continue;  }  const staff = staffOf[box];  out.push(staff === null ? "- 3" : staff);}console.log(out.join("\n"));
JavaMain.java · 45 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader; public class Main {    // int에는 null을 담을 수 없어서 Integer를 쓴다. "-"는 null로 두고, 번호 1이 1번 칸이 되게 한 칸 밀어 담는다    static Integer[] readNumbers(String line) {        String[] words = line.split(" ");        Integer[] row = new Integer[words.length + 1];        for (int i = 0; i < words.length; i++) {            row[i + 1] = words[i].equals("-") ? null : Integer.valueOf(words[i]);        }        return row;    }     public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        int q = Integer.parseInt(in.readLine().split(" ")[3]);        Integer[] siteOf = readNumbers(in.readLine()); // 손님 → 자리        Integer[] boxOf = readNumbers(in.readLine()); // 자리 → 전기함        String[] words = in.readLine().split(" ");        String[] staffOf = new String[words.length + 1]; // 전기함 → 담당자        for (int i = 0; i < words.length; i++) {            staffOf[i + 1] = words[i].equals("-") ? null : words[i];        }         StringBuilder out = new StringBuilder();        for (int i = 0; i < q; i++) {            int guest = Integer.parseInt(in.readLine().trim());            Integer site = siteOf[guest];            if (site == null) {                out.append("- 1\n");                continue;            }            Integer box = boxOf[site]; // site가 null이면 번호로 쓰는 순간 NullPointerException이 난다            if (box == null) {                out.append("- 2\n");                continue;            }            String staff = staffOf[box];            out.append(staff == null ? "- 3" : staff).append('\n');        }        System.out.print(out);    }}
C#Main.cs · 51 行
using System;using System.Text; class Program{    // int? 는 '수 또는 null'이다. "-"는 null로 두고, 번호 1이 1번 칸이 되게 한 칸 밀어 담는다    static int?[] ReadNumbers(string line)    {        string[] words = line.Split(' ');        var row = new int?[words.Length + 1];        for (int i = 0; i < words.Length; i++)        {            row[i + 1] = words[i] == "-" ? (int?)null : int.Parse(words[i]);        }        return row;    }     static void Main()    {        int q = int.Parse(Console.ReadLine().Split(' ')[3]);        int?[] siteOf = ReadNumbers(Console.ReadLine()); // 손님 → 자리        int?[] boxOf = ReadNumbers(Console.ReadLine());  // 자리 → 전기함        string[] words = Console.ReadLine().Split(' ');        var staffOf = new string[words.Length + 1];      // 전기함 → 담당자        for (int i = 0; i < words.Length; i++)        {            staffOf[i + 1] = words[i] == "-" ? null : words[i];        }         var sb = new StringBuilder();        for (int i = 0; i < q; i++)        {            int guest = int.Parse(Console.ReadLine());            int? site = siteOf[guest];            if (site == null)            {                sb.Append("- 1\n");                continue;            }            int? box = boxOf[site.Value]; // int? 는 그대로 번호로 쓸 수 없다. 값이 있음을 확인한 뒤 .Value로 꺼낸다            if (box == null)            {                sb.Append("- 2\n");                continue;            }            string staff = staffOf[box.Value];            sb.Append(staff ?? "- 3").Append('\n');        }        Console.Write(sb);    }}
C++main.cpp · 45 行
#include <iostream>#include <optional>#include <string>#include <vector> // std::optional<int>는 '수가 있거나 비어 있거나'다. "-"는 빈 optional로 두고, 번호 1이 1번 칸이 되게 한다std::vector<std::optional<int>> readNumbers(int count) {    std::vector<std::optional<int>> row(count + 1);    for (int i = 1; i <= count; i++) {        std::string word;        std::cin >> word;        if (word != "-") row[i] = std::stoi(word);    }    return row;} int main() {    int n, m, k, q;    std::cin >> n >> m >> k >> q;    auto siteOf = readNumbers(n);  // 손님 → 자리    auto boxOf = readNumbers(m);   // 자리 → 전기함    std::vector<std::optional<std::string>> staffOf(k + 1);  // 전기함 → 담당자    for (int i = 1; i <= k; i++) {        std::string word;        std::cin >> word;        if (word != "-") staffOf[i] = word;    }    for (int i = 0; i < q; i++) {        int guest;        std::cin >> guest;        std::optional<int> site = siteOf[guest];        if (!site) {            std::cout << "- 1\n";            continue;        }        std::optional<int> box = boxOf[*site];  // 비어 있는 optional에 *를 쓰면 정의되지 않은 동작이라, 먼저 확인했다        if (!box) {            std::cout << "- 2\n";            continue;        }        const std::optional<std::string>& staff = staffOf[*box];        if (!staff) std::cout << "- 3\n";        else std::cout << *staff << '\n';    }}
Cmain.c · 44 行
#include <stdio.h>#include <stdlib.h>#include <string.h> /* C의 int에는 '없음'이 없다. 번호가 1부터라서 0을 '없음'으로 정해 쓴다 */static void read_numbers(int *row, int count) {    char word[16];    for (int i = 1; i <= count; i++) {        scanf("%15s", word);        row[i] = strcmp(word, "-") == 0 ? 0 : atoi(word);    }} int main(void) {    int n, m, k, q;    scanf("%d %d %d %d", &n, &m, &k, &q);    static int site_of[101], box_of[101];    static char names[101][16];    static const char *staff_of[101]; /* 포인터에는 '없음'이 있다: NULL */    read_numbers(site_of, n);    read_numbers(box_of, m);    for (int i = 1; i <= k; i++) {        scanf("%15s", names[i]);        staff_of[i] = strcmp(names[i], "-") == 0 ? NULL : names[i];    }    for (int i = 0; i < q; i++) {        int guest;        scanf("%d", &guest);        int site = site_of[guest];        if (site == 0) {            puts("- 1");            continue;        }        int box = box_of[site];        if (box == 0) {            puts("- 2");            continue;        }        const char *staff = staff_of[box];        if (staff == NULL) puts("- 3"); /* NULL을 puts에 넘기면 정의되지 않은 동작이다 */        else puts(staff);    }    return 0;}
Craykonmain.cray · 59 行
// "-"는 빈값으로 바꾼다. 맨 앞에 빈값을 하나 두어 번호 1이 1번 칸이 되게 한다수표읽기(){    표 = [なし];    それぞれ (낱말 の中 コンソール.行読み().分ける(" "))    {        もし (낱말 == "-")        {            표.追加(なし);        }        でなければ        {            표.追加(낱말.数に());        }    }    返す 표;} q = コンソール.行読み().分ける(" ")[3].数に();자리표 = 수표읽기(); // 손님 → 자리전기함표 = 수표읽기(); // 자리 → 전기함담당표 = [なし]; // 전기함 → 담당자それぞれ (낱말 の中 コンソール.行読み().分ける(" ")){    もし (낱말 == "-")    {        담당표.追加(なし);    }    でなければ    {        담당표.追加(낱말);    }} 繰り返す (i = 0; i < q; i++){    손님 = コンソール.行読み().数に();    자리 = 자리표[손님];    もし (자리 == なし)    {        コンソール.書く("- 1");        次へ;    }    전기함 = 전기함표[자리]; // 자리가 빈값이면 여기서 프로그램이 멈추므로, 위에서 먼저 걸렀다    もし (전기함 == なし)    {        コンソール.書く("- 2");        次へ;    }    담당 = 담당표[전기함];    もし (담당 == なし)    {        コンソール.書く("- 3");    }    でなければ    {        コンソール.書く(담당);    }}