{ }codelper
相等比较与类型转换分组复制列表P0205

完全相同的包裹与内容相同的包裹

仓库里依次到达 n 个包裹。
包裹按到达顺序从 1 开始编号。
每个包裹上写着一个标签,以及按顺序写着的 k 个物品编号。
两个包裹"完全相同",是指标签相同、物品编号个数相同、并且每个位置上的物品编号都相同。
两个包裹"内容相同",是指标签相同,并且不考虑顺序时,相同的编号出现的次数也相同。
换句话说,把双方的物品编号从小到大排列后完全一样。
完全相同的两个包裹内容也相同。
没有任何物品的包裹之间,只要标签相同就完全相同。
第一行给出 n,接下来 n 行按 1 号起的顺序给出包裹,格式为"标签 k 编号1 … 编号k"。
为每个包裹在一行中输出两个数 a 和 b,用一个空格隔开。
a 是与它完全相同的包裹中最早到达的那个的编号,b 是与它内容相同的包裹中最早到达的那个的编号。
包裹自己也算在内。
最后一行输出两个数,用一个空格隔开。
第一个数是把完全相同的包裹算作一种时,包裹一共有几种;第二个数是把内容相同的包裹算作一种时,一共有几种。

输入
1 ≤ n ≤ 5,000,0 ≤ k ≤ 10,0 ≤ 物品编号 ≤ 10⁹。
标签是 1 到 10 个英文小写字母。
输出
n+1 行:每个包裹一行 "a b",最后一行 "完全不同的种数 内容不同的种数"
样例 1 · stdin
6
box 3 1 23 5
box 3 12 3 5
box 3 23 1 5
box 3 1 23 5
bag 0
box 3 5 1 23
stdout
1 1
2 2
3 1
1 1
5 5
6 1
5 3
main.py
输入(stdin)

运行时这些行会依次作为 input()/标准输入传入(运行中不会等待输入)。

$ 就绪

请在电脑上打开以编写和运行代码。在手机上可以查看题目和题解。

其他语言怎么写?查看各语言题解 · 8 种语言题解

关键是把"值相同的列表"当作字典的键,而列表之间的 == 和键的比较在各语言中不同。
Python 中 [1, 2] == [1, 2] 为真,但可变的 list 不能当键,所以要像 (label, tuple(codes)) 这样做成元组。
C++ 的 std::vector、std::string 的 ==、< 按值比较,所以 std::map<std::pair<std::string, std::vector<int>>, int> 可以直接用。
Java 的 List.equals 逐个元素用 equals 比较,所以 record Key(String label, List<Integer> codes) 可以当键(record 会生成 equals 和 hashCode)。
JavaScript、TypeScript 的 Map 和 ===、C# 的 List<int>、Craykon 列表的 == 只问是不是同一个列表(引用),所以要构造值相同就相同的字符串键 label + " " + codes.join(",")(Craykon codes.连接(","))。
C 没有字典,先用 qsort 把相同的聚在一起再比较相邻的,标签用 strcmp 比较。
内容的键用排好序的副本来构造。
Python sorted()、C# OrderBy、Java new ArrayList<>(codes)、JS [...codes]、C++ std::vector<int> sorted = codes;、Craykon codes.子列表(0, codes.数量) 都是副本。
七个陷阱:① 在 JS 中把数组 [label, codes] 当 Map 的键,每行都是新数组,所有包裹都互不相同。
② C# 的 (string, List<int>) 元组键也一样:元组按值比较,但里面的 List<int> 按引用比较。
③ Java 的 record Key(String label, int[] codes) 能编译,但数组成员只比较是不是同一个数组(不是 Arrays.equals)。
④ 不加分隔符像 label + "".join(...) 这样拼接,[1, 23] 和 [12, 3] 都变成 123,不同的包裹就成了相同的。
⑤ JS 的 codes.sort() 排的不是副本而是 codes 本身,并返回同一个数组,于是完全相同的键也用排好序的编号来构造了。
⑥ Craykon 的 sorted = codes; 也只是给同一个列表多起一个名字,结果一样。
⑦ 在 C 中 a->label == b->label 和 < 比较的是两个数组的地址,即使标签相同也总是不同。

Pythonmain.py · 18 行
import sys lines = sys.stdin.read().split("\n")n = int(lines[0])first_same = {}  # (이름표, 번호 튜플) → 가장 먼저 들어온 번호first_bag = {}   # (이름표, 정렬한 번호 튜플) → 가장 먼저 들어온 번호out = []for i in range(1, n + 1):    label, k, *rest = lines[i].split()    codes = [int(x) for x in rest]    # 목록(list)은 바뀔 수 있어서 열쇠가 못 된다. 튜플은 값으로 같고 해시도 값으로 정해진다    same = (label, tuple(codes))    bag = (label, tuple(sorted(codes)))  # sorted는 새 목록을 만든다: codes는 그대로    a = first_same.setdefault(same, i)    b = first_bag.setdefault(bag, i)    out.append(f"{a} {b}")out.append(f"{len(first_same)} {len(first_bag)}")print("\n".join(out))
JavaScriptmain.js · 20 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const n = Number(lines[0]);// Map은 열쇠를 SameValueZero로 비교한다: 배열은 같은 배열(참조)일 때만 같다.// 그래서 값이 같으면 같아지는 문자열 열쇠로 바꾼다. 사이에 쉼표·빈칸을 넣어 경계를 지킨다const firstSame = new Map();const firstBag = new Map();const out = [];for (let i = 1; i <= n; i++) {  const [label, , ...rest] = lines[i].trim().split(" ");  const codes = rest.map(Number);  const sameKey = `${label} ${codes.join(",")}`;  // sort는 제자리에서 바꾸고 같은 배열을 돌려준다: 복사본([...codes])을 정렬한다.  // 비교 함수가 없으면 문자열로 정렬하니 (a, b) => a - b를 준다  const bagKey = `${label} ${[...codes].sort((a, b) => a - b).join(",")}`;  if (!firstSame.has(sameKey)) firstSame.set(sameKey, i);  if (!firstBag.has(bagKey)) firstBag.set(bagKey, i);  out.push(`${firstSame.get(sameKey)} ${firstBag.get(bagKey)}`);}out.push(`${firstSame.size} ${firstBag.size}`);console.log(out.join("\n"));
TypeScriptmain.ts · 25 行
import { readFileSync } from "node:fs"; const lines = readFileSync(0, "utf8").split("\n");const n = Number(lines[0]); // 배열은 ===로도 Map 열쇠로도 같은 배열(참조)일 때만 같다: 값으로 같아지는 문자열 열쇠를 만든다function keyOf(label: string, codes: readonly number[]): string {  return `${label} ${codes.join(",")}`; // 사이에 빈칸·쉼표를 넣어 [1, 23]과 [12, 3]을 가른다} const firstSame = new Map<string, number>();const firstBag = new Map<string, number>();const out: string[] = [];for (let i = 1; i <= n; i++) {  const [label, , ...rest] = lines[i].trim().split(" ");  const codes: readonly number[] = rest.map(Number);  const sorted = [...codes].sort((a, b) => a - b); // 복사본을 정렬한다(readonly라 codes.sort는 타입 오류)  const sameKey = keyOf(label, codes);  const bagKey = keyOf(label, sorted);  if (!firstSame.has(sameKey)) firstSame.set(sameKey, i);  if (!firstBag.has(bagKey)) firstBag.set(bagKey, i);  out.push(`${firstSame.get(sameKey)} ${firstBag.get(bagKey)}`);}out.push(`${firstSame.size} ${firstBag.size}`);console.log(out.join("\n"));
JavaMain.java · 36 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; public class Main {    // record는 equals·hashCode를 멤버마다 equals로 만들어 준다.    // List.equals는 길이와 자리마다의 값을 비교한다(배열 int[]라면 같은 배열인지만 본다)    record Key(String label, List<Integer> codes) {}     public static void main(String[] args) throws IOException {        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        int n = Integer.parseInt(br.readLine().trim());        Map<Key, Integer> firstSame = new HashMap<>();        Map<Key, Integer> firstBag = new HashMap<>();        StringBuilder out = new StringBuilder();        for (int i = 1; i <= n; i++) {            String[] p = br.readLine().trim().split(" ");            List<Integer> codes = new ArrayList<>();            for (int j = 2; j < p.length; j++) codes.add(Integer.parseInt(p[j]));            List<Integer> sorted = new ArrayList<>(codes); // 복사본을 정렬한다            sorted.sort(null);            // putIfAbsent: 열쇠가 없으면 i를 넣고 null을, 있으면 먼저 넣은 값을 돌려준다            Integer a = firstSame.putIfAbsent(new Key(p[0], codes), i);            Integer b = firstBag.putIfAbsent(new Key(p[0], sorted), i);            if (a == null) a = i;            if (b == null) b = i;            out.append(a).append(' ').append(b).append('\n');        }        out.append(firstSame.size()).append(' ').append(firstBag.size()).append('\n');        System.out.print(out);    }}
C#Main.cs · 32 行
using System;using System.Collections.Generic;using System.Linq;using System.Text; class Program{    static void Main()    {        var input = Console.In;        int n = int.Parse(input.ReadLine()!.Trim());        // List<int>는 Equals가 참조 비교라 열쇠로 쓰면 값이 같아도 다른 열쇠가 된다.        // 값으로 같아지는 문자열 열쇠를 만든다(사이에 빈칸·쉼표를 넣어 경계를 지킨다)        var firstSame = new Dictionary<string, int>();        var firstBag = new Dictionary<string, int>();        var sb = new StringBuilder();        for (int i = 1; i <= n; i++)        {            string[] p = input.ReadLine()!.Trim().Split(' ');            string label = p[0];            int[] codes = p.Skip(2).Select(int.Parse).ToArray();            int[] sorted = codes.OrderBy(x => x).ToArray();  // 새 배열: codes는 그대로            string sameKey = label + " " + string.Join(",", codes);            string bagKey = label + " " + string.Join(",", sorted);            if (!firstSame.TryGetValue(sameKey, out int a)) firstSame[sameKey] = a = i;            if (!firstBag.TryGetValue(bagKey, out int b)) firstBag[bagKey] = b = i;            sb.Append(a).Append(' ').Append(b).Append('\n');        }        sb.Append(firstSame.Count).Append(' ').Append(firstBag.Count).Append('\n');        Console.Write(sb);    }}
C++main.cpp · 33 行
#include <algorithm>#include <iostream>#include <map>#include <string>#include <utility>#include <vector> int main() {    std::ios::sync_with_stdio(false);    std::cin.tie(nullptr);    int n;    std::cin >> n;    // std::string과 std::vector는 ==와 <가 값(길이와 자리마다의 원소)으로 비교한다.    // 그래서 pair<string, vector<int>>를 그대로 map의 열쇠로 쓸 수 있다    using Key = std::pair<std::string, std::vector<int>>;    std::map<Key, int> firstSame, firstBag;    std::string out;    for (int i = 1; i <= n; i++) {        std::string label;        int k;        std::cin >> label >> k;        std::vector<int> codes(k);        for (int &x : codes) std::cin >> x;        std::vector<int> sorted = codes;  // vector 대입은 복사: codes는 그대로        std::sort(sorted.begin(), sorted.end());        // emplace는 열쇠가 이미 있으면 넣지 않고, 있던 칸을 가리키는 반복자를 돌려준다        int a = firstSame.emplace(Key{label, codes}, i).first->second;        int b = firstBag.emplace(Key{label, sorted}, i).first->second;        out += std::to_string(a) + ' ' + std::to_string(b) + '\n';    }    out += std::to_string(firstSame.size()) + ' ' + std::to_string(firstBag.size()) + '\n';    std::cout << out;}
Cmain.c · 73 行
#include <stdio.h>#include <stdlib.h>#include <string.h> #define MAXN 5000 typedef struct {    char label[11];    int k;    int codes[10];  /* 들어온 차례 그대로 */    int sorted[10]; /* 작은 것부터 늘어놓은 것 */} Parcel; static Parcel p[MAXN];static int order[MAXN]; static int cmp_int(const void *a, const void *b) {    int x = *(const int *)a, y = *(const int *)b;    return (x > y) - (x < y);} /* 두 꾸러미를 비교한다. 문자열은 ==로 비교하면 주소를 비교하니 strcmp로,   배열은 길이를 먼저 보고 칸마다 비교한다 */static int compare(const Parcel *a, const Parcel *b, int use_sorted) {    int c = strcmp(a->label, b->label);    if (c != 0) return c;    if (a->k != b->k) return a->k - b->k;    const int *x = use_sorted ? a->sorted : a->codes;    const int *y = use_sorted ? b->sorted : b->codes;    for (int j = 0; j < a->k; j++)        if (x[j] != y[j]) return x[j] < y[j] ? -1 : 1;    return 0;} static int use_sorted_now;static int cmp_order(const void *a, const void *b) {    int i = *(const int *)a, j = *(const int *)b;    int c = compare(&p[i], &p[j], use_sorted_now);    return c != 0 ? c : i - j; /* 같은 것끼리는 먼저 들어온 것이 앞에 */} /* 같은 것끼리 모이도록 번호를 정렬하고, 무리마다 맨 앞(가장 먼저 들어온 것)의 번호를 적는다 */static int group(int n, int use_sorted, int *first) {    for (int i = 0; i < n; i++) order[i] = i;    use_sorted_now = use_sorted;    qsort(order, (size_t)n, sizeof order[0], cmp_order);    int kinds = 0, head = 0;    for (int t = 0; t < n; t++) {        if (t == 0 || compare(&p[order[t]], &p[order[t - 1]], use_sorted) != 0) {            head = order[t];            kinds++;        }        first[order[t]] = head;    }    return kinds;} int main(void) {    static int first_same[MAXN], first_bag[MAXN];    int n;    scanf("%d", &n);    for (int i = 0; i < n; i++) {        scanf("%10s %d", p[i].label, &p[i].k);        for (int j = 0; j < p[i].k; j++) scanf("%d", &p[i].codes[j]);        memcpy(p[i].sorted, p[i].codes, sizeof p[i].codes);        qsort(p[i].sorted, (size_t)p[i].k, sizeof(int), cmp_int);    }    int same_kinds = group(n, 0, first_same);    int bag_kinds = group(n, 1, first_bag);    for (int i = 0; i < n; i++) printf("%d %d\n", first_same[i] + 1, first_bag[i] + 1);    printf("%d %d\n", same_kinds, bag_kinds);    return 0;}
Craykonmain.cray · 33 行
n = 控制台.读行().转数字();// 목록끼리 ==는 같은 목록인지만 묻고, 사전의 열쇠는 문자열이나 수만 된다.// 그래서 값이 같으면 같아지는 문자열 열쇠를 만든다(사이에 빈칸·쉼표를 넣는다)firstSame = 字典();firstBag = 字典();kinds = 0;bagKinds = 0;循环 (i = 1; i <= n; i++){    p = 控制台.读行().去空白().分割(" ");    label = p[0];    codes = [];    循环 (j = 2; j < p.数量; j++)    {        codes.添加(p[j].转数字());    }    sorted = codes.子列表(0, codes.数量); // 새 목록: 정렬해도 codes는 그대로    sorted.排序();    sameKey = label + " " + codes.连接(",");    bagKey = label + " " + sorted.连接(",");    如果 (!firstSame.包含(sameKey))    {        firstSame[sameKey] = i;        kinds = kinds + 1;    }    如果 (!firstBag.包含(bagKey))    {        firstBag[bagKey] = i;        bagKinds = bagKinds + 1;    }    控制台.输出(firstSame[sameKey] + " " + firstBag[bagKey]);}控制台.输出(kinds + " " + bagKinds);