{ }codelper
クエリ式(LINQ・ストリーム)レコードグループ化P0160

商品表と注文表の突き合わせ

商品の一覧と注文の一覧をそれぞれレコード(record)のリストにし、二つのリストを商品番号で突き合わせて(join)、商品の種類ごとの売上を計算せよ。
ループの代わりにクエリ式の処理(C# LINQ、Java ストリーム)で解いてみよう。
一行目に商品数 p と注文数 m が与えられる。
続く p 行に商品が「商品番号 種類 価格」で、その次の m 行に注文が「商品番号 数量」で一つずつ与えられる。
注文の商品番号が商品の一覧にない場合、その注文は計算から除く。
一行目に、こうして除いた注文の数を出力せよ。
続いて、商品の一覧に出てきた種類ごとに「種類 販売数量 売上」を一行ずつ出力せよ。
販売数量はその種類の商品の注文数量の合計で、売上は注文ごとの価格×数量を足した値である。
注文が一つもない種類も「種類 0 0」として出力する。
種類は売上が大きい順に、売上が同じなら種類名の辞書順(文字コード順なので数字が英小文字より前、ab は abc より前)に出力する。

入力
1 ≤ p ≤ 1,000、1 ≤ m ≤ 5,000。
商品番号と種類は英小文字・数字 1 ~ 10 文字で、商品番号は互いに異なる。
1 ≤ 価格 ≤ 1,000,000、1 ≤ 数量 ≤ 1,000(整数)
出力
一行目に除いた注文数、続いて種類ごとに「種類 販売数量 売上」
例 1 · stdin
4 5
m1 milk 1200
b1 bread 2500
s1 soap 900
m2 milk 3000
m1 2
b1 1
k9 4
m2 1
b1 1
stdout
1
milk 3 5400
bread 2 5000
soap 0 0
main.py
入力(stdin)

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

$ 準備完了

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

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

この問題はクエリ式の処理が言語に組み込まれている C#(LINQ)と Java(ストリーム)だけを受け付けます。
二つのリストの突き合わせ方が違います。
C# には結合があります。
orders.Join(products, o => o.ProductId, p => p.Id, ...) は対応があるものだけを残す内部結合で、products.GroupJoin(orders, ...) は商品ごとに注文のまとまりを付け、注文のない商品も空のまとまりとして残します(クエリ構文の join ... into がこれです)。
Java のストリームには結合がないので、片方を Collectors.toMap(Product::id, Function.identity()) で Map にしておき、もう片方を回りながら突き合わせます。
落とし穴は四つ:① 内部結合だけだと、注文のない種類が丸ごと消えます。
C# は GroupJoin のように商品の側から出発し、Java は groupingBy した Map ではなく商品に出てきた種類の一覧(distinct())から出発して getOrDefault(c, List.of()) で付けます。
② 存在しない商品番号:Java の byId.get(id) は null を返すので、そのまま .price() を呼ぶと NullPointerException、C# Dictionary の dict[id] は KeyNotFoundException を投げます。
Join は対応のない注文を黙って除くので、除いた数は orders.Count - matched.Count() で数えられます。
③ 売上は 100 万 × 1,000 個 × 5,000 件で 5 兆になるので long でなければなりません。
Java の mapToInt(...).sum() は黙ってあふれ、C# LINQ の Sum は int であふれると OverflowException を投げます。
④ 注文のない種類はみな売上 0 で並ぶので、名前順の並べ替えが欠かせません。
OrderByDescending・sorted は安定ソートなので、ThenBy・thenComparing を忘れると商品一覧に出てきた順がそのまま残ります。

JavaMain.java · 72 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.Comparator;import java.util.List;import java.util.Map;import java.util.function.Function;import java.util.stream.Collectors; record Product(String id, String category, long price) {} record Order(String productId, int qty) {} record Line(String category, int qty, long amount) {} record CategoryStat(String category, int units, long revenue) {} public class Main {    public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        String[] first = in.readLine().trim().split(" ");        int p = Integer.parseInt(first[0]), m = Integer.parseInt(first[1]);        List<Product> products = new ArrayList<>();        for (int i = 0; i < p; i++) {            String[] s = in.readLine().trim().split(" ");            products.add(new Product(s[0], s[1], Long.parseLong(s[2])));        }        List<Order> orders = new ArrayList<>();        for (int i = 0; i < m; i++) {            String[] s = in.readLine().trim().split(" ");            orders.add(new Order(s[0], Integer.parseInt(s[1])));        }         // 스트림에는 join이 없다: 한쪽을 id → 상품 Map으로 만들어 두고 맞춰 본다        Map<String, Product> byId = products.stream()            .collect(Collectors.toMap(Product::id, Function.identity()));         // 안쪽 조인: 짝이 되는 상품이 있는 주문만        List<Line> lines = orders.stream()            .filter(o -> byId.containsKey(o.productId())) // 없으면 get이 null을 준다            .map(o -> {                Product pr = byId.get(o.productId());                return new Line(pr.category(), o.qty(), pr.price() * o.qty());            })            .toList();        int unknown = orders.size() - lines.size();         // 종류별로 묶기. 주문이 없는 종류는 이 Map에 없다        Map<String, List<Line>> byCategory = lines.stream()            .collect(Collectors.groupingBy(Line::category));         // 바깥 조인: 상품에 나온 모든 종류에서 출발해 묶음을 붙인다(없으면 빈 목록)        List<CategoryStat> stats = products.stream()            .map(Product::category)            .distinct()            .map(c -> {                List<Line> ls = byCategory.getOrDefault(c, List.of());                return new CategoryStat(c,                    ls.stream().mapToInt(Line::qty).sum(),                    ls.stream().mapToLong(Line::amount).sum());            })            .sorted(Comparator.comparingLong(CategoryStat::revenue).reversed()                .thenComparing(CategoryStat::category))            .toList();         StringBuilder out = new StringBuilder().append(unknown).append('\n');        for (CategoryStat s : stats)            out.append(s.category()).append(' ').append(s.units()).append(' ').append(s.revenue()).append('\n');        System.out.print(out);    }}
C#Main.cs · 46 行
using System;using System.Collections.Generic;using System.Linq;using System.Text; record Product(string Id, string Category, long Price);record Order(string ProductId, int Qty); class Program{    static void Main()    {        string[] first = Console.ReadLine()!.Trim().Split(' ');        int p = int.Parse(first[0]), m = int.Parse(first[1]);        var products = new List<Product>();        for (int i = 0; i < p; i++)        {            string[] s = Console.ReadLine()!.Trim().Split(' ');            products.Add(new Product(s[0], s[1], long.Parse(s[2])));        }        var orders = new List<Order>();        for (int i = 0; i < m; i++)        {            string[] s = Console.ReadLine()!.Trim().Split(' ');            orders.Add(new Order(s[0], int.Parse(s[1])));        }         // 안쪽 조인(Join): 짝이 되는 상품이 없는 주문은 빠진다        var matched = orders.Join(products, o => o.ProductId, pr => pr.Id,                                  (o, pr) => new { pr.Category, o.Qty, Amount = pr.Price * o.Qty });        int unknown = orders.Count - matched.Count();         // 묶음 조인(GroupJoin): 상품마다 그 상품의 주문 묶음. 주문이 없는 상품도 빈 묶음으로 남는다        var stats = products            .GroupJoin(orders, pr => pr.Id, o => o.ProductId,                       (pr, os) => new { pr.Category, Units = os.Sum(o => o.Qty), Revenue = os.Sum(o => pr.Price * o.Qty) })            .GroupBy(x => x.Category)            .Select(g => new { Category = g.Key, Units = g.Sum(x => x.Units), Revenue = g.Sum(x => x.Revenue) })            .OrderByDescending(s => s.Revenue)            .ThenBy(s => s.Category, StringComparer.Ordinal);         var sb = new StringBuilder().Append(unknown).Append('\n');        foreach (var s in stats) sb.Append($"{s.Category} {s.Units} {s.Revenue}\n");        Console.Write(sb);    }}