{ }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.连接(", ")); // 잇기는 '사이'에만 넣는다}