{ }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() 算出。
③ 销售额在 1,000,000 × 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);    }}