{ }codelper
文字列の連結グループ化リストの並べ替えP0302

巻末の索引を作る

本の巻末につける索引を作りたい。
単語と、その単語が出てくるページ番号を書いた記録が n 行与えられる。
同じ単語が何行にも出てくることがあり、同じ単語と同じページ番号の行が何回も出てくることもある。
単語ごとに一行ずつ、単語、コロン(:)、空白一つ、ページの一覧をこの順につなげて出力せよ。
単語はアルファベットの辞書順に出力する(前の部分が同じなら短い単語が先)。
ページの一覧は次のように作る。
一つの単語のページ番号は、重なったものを一つだけ残して小さい順に並べる。
並べたページ番号を前からまとまりに分ける。
直前のページ番号よりちょうど 1 大きいページ番号は直前と同じまとまりに入り、そうでないページ番号から新しいまとまりが始まる。
ページ番号が三つ以上のまとまりは「最初のページ-最後のページ」に縮めて書く(5, 6, 7 は 5-7、5, 6, 7, 8 は 5-8)。
ページ番号が二つのまとまりは縮めずに二つを別々に書く(5, 6 はそのまま 5 と 6)。
ページ番号が一つのまとまりはそのページ番号を書く。
こうして作った項目の間ごとに、コンマと空白一つ(", ")を入れる。
行の終わりにはコンマも空白もつけない。

入力
一行目に n が与えられる。
次の n 行に単語とページ番号が空白一つで区切られて与えられる。
1 ≤ n ≤ 5,000、単語は英小文字だけからなる長さ 1〜10 の文字列、1 ≤ ページ番号 ≤ 100,000
出力
異なる単語ごとに一行。
「単語: ページの一覧」の形で、ページ番号はコンマなしの数字だけで書く(100000)
例 1 · stdin
9
tree 12
stack 5
tree 3
stack 6
tree 5
tree 6
stack 5
tree 7
tree 13
stdout
stack: 5, 6
tree: 3, 5-7, 12, 13
main.py
入力(stdin)

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

$ 準備完了

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

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

項目の「間」にだけ区切りの文字列を入れるのが join です。
Python ", ".join(parts)、JS・TS parts.join(", ")、Java String.join(", ", parts)、C# string.Join(", ", parts)、Craykon parts.つなぐ(", ") のように一行で済みます。
C には join がなく、C++ の std::string にもないので、「最初の項目でなければ前にコンマを書く」と自分でつなぎます。
項目ごとに後ろへ ", " をつけると行の終わりにコンマが残るので注意します。
join が受け取るものも違います:Python は文字列だけなので数が混じると TypeError、Java の String.join も文字列のリストだけです(List<Integer> はコンパイルエラー)。
JS・TS、C#、Craykon は数を自動で文字列に変えてつなぎます。
グループ化は辞書でします:Python dict+set、JS・TS Map+Set、Java TreeMap+TreeSet、C# SortedDictionary+SortedSet、C++ map+set(この三つは入れるだけで並びます)、Craykon は 辞書() にリストを入れます。
C には辞書がないので(単語, ページ)を丸ごと qsort して同じ単語を隣り合わせにします。
落とし穴その一:JS・TS の sort() は比較関数がないと数も文字列に変えて比べるので、9, 10, 100 が 10, 100, 9 になります。
sort((a, b) => a - b) と書きます。
Python の sorted、Craykon の 並べ替え() は数のリストなら大きさの順です(ただし Craykon で 数に を忘れて文字列のまま入れると、同じく辞書順になります)。
落とし穴その二:重なったページを消さないと、5, 5, 6, 7 で「前の数 + 1」が崩れて 5, 5-7 になります。
集合に入れるか、並べ替えてから直前と同じ数を飛ばします。
落とし穴その三:二つだけ続く 5, 6 を 5-6 に縮めてはいけません。
まとまりの長さが 3 以上のときだけ縮めます。
落とし穴その四:「続きが切れたときにまとまりを出す」と書くと、繰り返しが終わったあとで最後のまとまりをもう一度出す必要があります。
忘れると行ごとに最後の項目が消えます。

Pythonmain.py · 35 行
import sys  def page_parts(pages):    """정렬되고 겹치지 않는 쪽 번호를 '3', '5-7' 같은 조각으로 바꾼다."""    parts = []    i = 0    while i < len(pages):        j = i        while j + 1 < len(pages) and pages[j + 1] == pages[j] + 1:            j += 1        if j - i >= 2:  # 셋 이상 이어질 때만 범위로 줄인다            parts.append(f"{pages[i]}-{pages[j]}")        else:            parts.extend(str(p) for p in pages[i : j + 1])        i = j + 1    return parts  def main():    data = sys.stdin.read().split()    n = int(data[0])    index = {}    for i in range(n):        word, page = data[1 + 2 * i], int(data[2 + 2 * i])        index.setdefault(word, set()).add(page)  # 집합이라 같은 쪽은 한 번만 남는다     lines = []    for word in sorted(index):        parts = page_parts(sorted(index[word]))        lines.append(word + ": " + ", ".join(parts))  # join은 '사이'에만 넣는다    print("\n".join(lines))  main()
JavaScriptmain.js · 36 行
const data = require("fs").readFileSync(0, "utf8").split(/\s+/).filter((s) => s !== "");const n = Number(data[0]); // 낱말 → 쪽 번호 집합(Set이라 같은 쪽은 한 번만 남는다)const index = new Map();for (let i = 0; i < n; i++) {  const word = data[1 + 2 * i];  const page = Number(data[2 + 2 * i]);  if (!index.has(word)) index.set(word, new Set());  index.get(word).add(page);} // 정렬되고 겹치지 않는 쪽 번호를 "3", "5-7" 같은 조각으로 바꾼다function pageParts(pages) {  const parts = [];  let i = 0;  while (i < pages.length) {    let j = i;    while (j + 1 < pages.length && pages[j + 1] === pages[j] + 1) j++;    if (j - i >= 2) {      parts.push(`${pages[i]}-${pages[j]}`); // 셋 이상 이어질 때만 범위로 줄인다    } else {      for (let t = i; t <= j; t++) parts.push(String(pages[t]));    }    i = j + 1;  }  return parts;} const lines = [];for (const word of [...index.keys()].sort()) {  // 수는 비교 함수를 줘야 크기 순이 된다(그냥 sort()는 문자열로 바꿔 견준다)  const pages = [...index.get(word)].sort((a, b) => a - b);  lines.push(word + ": " + pageParts(pages).join(", "));}console.log(lines.join("\n"));
TypeScriptmain.ts · 42 行
import { readFileSync } from "node:fs"; const data: string[] = readFileSync(0, "utf8").split(/\s+/).filter((s) => s !== "");const n = Number(data[0]); // 낱말 → 쪽 번호 집합(Set이라 같은 쪽은 한 번만 남는다)const index = new Map<string, Set<number>>();for (let i = 0; i < n; i++) {  const word = data[1 + 2 * i];  const page = Number(data[2 + 2 * i]);  let pages = index.get(word);  if (pages === undefined) {    pages = new Set<number>();    index.set(word, pages);  }  pages.add(page);} // 정렬되고 겹치지 않는 쪽 번호를 "3", "5-7" 같은 조각으로 바꾼다function pageParts(pages: number[]): string[] {  const parts: string[] = [];  let i = 0;  while (i < pages.length) {    let j = i;    while (j + 1 < pages.length && pages[j + 1] === pages[j] + 1) j++;    if (j - i >= 2) {      parts.push(`${pages[i]}-${pages[j]}`); // 셋 이상 이어질 때만 범위로 줄인다    } else {      for (let t = i; t <= j; t++) parts.push(String(pages[t]));    }    i = j + 1;  }  return parts;} const lines: string[] = [];for (const [word, set] of [...index.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {  // 수는 비교 함수를 줘야 크기 순이 된다(그냥 sort()는 문자열로 바꿔 견준다)  const pages = [...set].sort((a, b) => a - b);  lines.push(word + ": " + pageParts(pages).join(", "));}console.log(lines.join("\n"));
JavaMain.java · 50 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.StringTokenizer;import java.util.TreeMap;import java.util.TreeSet; public class Main {    // 정렬되고 겹치지 않는 쪽 번호를 "3", "5-7" 같은 조각으로 바꾼다    static List<String> pageParts(List<Integer> pages) {        List<String> parts = new ArrayList<>();        int i = 0;        while (i < pages.size()) {            int j = i;            while (j + 1 < pages.size() && pages.get(j + 1) == pages.get(j) + 1) j++;            if (j - i >= 2) {                parts.add(pages.get(i) + "-" + pages.get(j)); // 셋 이상 이어질 때만 범위로 줄인다            } else {                for (int t = i; t <= j; t++) parts.add(String.valueOf(pages.get(t)));            }            i = j + 1;        }        return parts;    }     public static void main(String[] args) throws IOException {        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        int n = Integer.parseInt(br.readLine().trim());         // TreeMap·TreeSet은 넣기만 해도 정렬되고, 집합이라 같은 쪽은 한 번만 남는다        TreeMap<String, TreeSet<Integer>> index = new TreeMap<>();        for (int i = 0; i < n; i++) {            StringTokenizer st = new StringTokenizer(br.readLine());            String word = st.nextToken();            int page = Integer.parseInt(st.nextToken());            index.computeIfAbsent(word, k -> new TreeSet<>()).add(page);        }         StringBuilder sb = new StringBuilder();        for (Map.Entry<String, TreeSet<Integer>> e : index.entrySet()) {            List<String> parts = pageParts(new ArrayList<>(e.getValue()));            // String.join은 문자열만 받는다(수는 먼저 문자열로 바꿔 둔다)            sb.append(e.getKey()).append(": ").append(String.join(", ", parts)).append('\n');        }        System.out.print(sb);    }}
C#Main.cs · 54 行
using System;using System.Collections.Generic;using System.Text; class Program{    // 정렬되고 겹치지 않는 쪽 번호를 "3", "5-7" 같은 조각으로 바꾼다    static List<string> PageParts(List<int> pages)    {        var parts = new List<string>();        int i = 0;        while (i < pages.Count)        {            int j = i;            while (j + 1 < pages.Count && pages[j + 1] == pages[j] + 1) j++;            if (j - i >= 2)            {                parts.Add($"{pages[i]}-{pages[j]}"); // 셋 이상 이어질 때만 범위로 줄인다            }            else            {                for (int t = i; t <= j; t++) parts.Add(pages[t].ToString());            }            i = j + 1;        }        return parts;    }     static void Main()    {        int n = int.Parse(Console.ReadLine());         // SortedDictionary·SortedSet은 넣기만 해도 정렬되고, 집합이라 같은 쪽은 한 번만 남는다        var index = new SortedDictionary<string, SortedSet<int>>(StringComparer.Ordinal);        for (int i = 0; i < n; i++)        {            string[] t = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);            if (!index.TryGetValue(t[0], out var pages))            {                pages = new SortedSet<int>();                index[t[0]] = pages;            }            pages.Add(int.Parse(t[1]));        }         var sb = new StringBuilder();        foreach (var e in index)        {            List<string> parts = PageParts(new List<int>(e.Value));            sb.Append(e.Key).Append(": ").Append(string.Join(", ", parts)).Append('\n');        }        Console.Write(sb);    }}
C++main.cpp · 51 行
#include <iostream>#include <map>#include <set>#include <string>#include <vector>using namespace std; // 정렬되고 겹치지 않는 쪽 번호를 "3", "5-7" 같은 조각으로 바꾼다vector<string> pageParts(const vector<int>& pages) {    vector<string> parts;    size_t i = 0;    while (i < pages.size()) {        size_t j = i;        while (j + 1 < pages.size() && pages[j + 1] == pages[j] + 1) j++;        if (j - i >= 2) {            parts.push_back(to_string(pages[i]) + "-" + to_string(pages[j])); // 셋 이상만 범위로        } else {            for (size_t t = i; t <= j; t++) parts.push_back(to_string(pages[t]));        }        i = j + 1;    }    return parts;} int main() {    ios::sync_with_stdio(false);    cin.tie(nullptr);     int n;    cin >> n;    // map·set은 넣기만 해도 정렬되고, set이라 같은 쪽은 한 번만 남는다    map<string, set<int>> index;    for (int i = 0; i < n; i++) {        string word;        int page;        cin >> word >> page;        index[word].insert(page);    }     for (const auto& [word, pageSet] : index) {        vector<string> parts = pageParts(vector<int>(pageSet.begin(), pageSet.end()));        // std::string에는 join이 없으니 '첫 조각이 아니면 앞에 쉼표'로 잇는다        string line = word + ": ";        for (size_t t = 0; t < parts.size(); t++) {            if (t > 0) line += ", ";            line += parts[t];        }        cout << line << '\n';    }    return 0;}
Cmain.c · 62 行
#include <stdio.h>#include <stdlib.h>#include <string.h> typedef struct {    char word[11];    int page;} Entry; // 낱말 순, 같은 낱말이면 쪽 번호 순static int cmp(const void *a, const void *b) {    const Entry *x = a, *y = b;    int c = strcmp(x->word, y->word);    if (c != 0) return c;    return (x->page > y->page) - (x->page < y->page);} int main(void) {    int n;    if (scanf("%d", &n) != 1) return 1;    Entry *e = malloc(sizeof(Entry) * n);    for (int i = 0; i < n; i++) scanf("%10s %d", e[i].word, &e[i].page);     // C에는 사전이 없으니 정렬해서 같은 낱말을 한데 모은다    qsort(e, n, sizeof(Entry), cmp);     int *pages = malloc(sizeof(int) * n);    int i = 0;    while (i < n) {        // 같은 낱말의 쪽 번호를 겹치지 않게 모은다        int cnt = 0, k = i;        while (k < n && strcmp(e[k].word, e[i].word) == 0) {            if (cnt == 0 || pages[cnt - 1] != e[k].page) pages[cnt++] = e[k].page;            k++;        }         printf("%s: ", e[i].word);        // join이 없으니 '첫 조각이 아니면 앞에 쉼표'로 사이에만 넣는다        int first = 1;        int a = 0;        while (a < cnt) {            int b = a;            while (b + 1 < cnt && pages[b + 1] == pages[b] + 1) b++;            if (b - a >= 2) { // 셋 이상 이어질 때만 범위로 줄인다                printf(first ? "%d-%d" : ", %d-%d", pages[a], pages[b]);                first = 0;            } else {                for (int t = a; t <= b; t++) {                    printf(first ? "%d" : ", %d", pages[t]);                    first = 0;                }            }            a = b + 1;        }        printf("\n");        i = k;    }     free(pages);    free(e);    return 0;}
Craykonmain.cray · 55 行
n = コンソール.行読み().空白除去().数に(); // 낱말 → 쪽 번호 목록index = 辞書();繰り返す (i = 0; i < n; i++){    t = コンソール.行読み().空白除去().分ける();    もし (!index.含む(t[0]))    {        index[t[0]] = [];    }    index[t[0]].追加(t[1].数に()); // 수로 바꿔 넣어야 크기 순으로 정렬된다} words = index.キー一覧;words.並べ替え();それぞれ (word の中 words){    all = index[word];    all.並べ替え();     // 같은 쪽은 한 번만 남긴다(정렬했으니 바로 앞과만 견주면 된다)    pages = [];    それぞれ (p の中 all)    {        もし (pages.個数 == 0 || pages[pages.個数 - 1] != p)        {            pages.追加(p);        }    }     parts = [];    i = 0;    間 (i < pages.個数)    {        j = i;        間 (j + 1 < pages.個数 && pages[j + 1] == pages[j] + 1)        {            j++;        }        もし (j - i >= 2)        {            parts.追加(pages[i] + "-" + pages[j]); // 셋 이상 이어질 때만 범위로 줄인다        }        でなければ        {            繰り返す (k = i; k <= j; k++)            {                parts.追加(pages[k]);            }        }        i = j + 1;    }    コンソール.書く(word + ": " + parts.つなぐ(", ")); // 잇기는 '사이'에만 넣는다}