CCC 2022 J4/S2 Group Work Solution: Count Violated Constraints in One Pass with a Dictionary



☰

Start NorthStar

CP Weekly Challenge

CCC 2022 J4/S2 Group Work Solution: Count Violated Constraints in One Pass with a Dictionary

Map each name to its group with a dictionary, then count every violated constraint in two linear passes, with Python and C++ solutions.

CP Weekly Challenge

CCC 2022 J4/S2 Group Work 题解:用字典一次扫完所有约束

用字典把每个名字映射到组号,两轮扫完所有约束并统计违规数,附 Python 与 C++ 解法。

Share this article

Copy the link, open your phone share sheet, scan the QR code for WeChat, or send by email.




Email

The class has already been split into groups of three. Now the teacher hands you two more lists: one says these pairs of students must be in the same group, the other says these pairs must be kept apart. Your job is to count how many of these constraints the existing arrangement violates. This CCC 2022 J4/S2 problem (official title: Group Work; often called Good Groups in the community) looks like simple counting, but what it really tests is how fast and accurately you can answer "which group is this person in?". The whole solution is one dictionary and two passes.

Where This Fits in the CCC DSA Map

  • Level: Junior J4 ≡ Senior S2 (the same problem serves both levels)
  • Pattern: Constraint Checking with a Hash Map
  • Prerequisite: reading multi-line strings, loops and conditions (Python basics: input, operators, and conditions), dictionaries / hash maps (dedicated note coming soon)
  • What this teaches: turn "does this relation hold?" into one O(1) table lookup, and count violations instead of rebuilding the assignment
  • Where it appears again: counting-pattern topics (planned), CCC 2017 J5/S3 Nailed It! (planned), and any "given an assignment, count the conflicts" verification problem

What to Study Next

What the Problem Is Asking

A class has been divided into G groups of three, and the arrangement is fixed. The input has three sections: first X same-group constraints (these two students must be together), then Y different-group constraints (these two must not be together), and finally the G groups themselves, three names per line. Every name in a constraint appears in exactly one group, and any pair of students shows up in at most one constraint. Output a single integer: the number of violated constraints, between 0 and X+Y.

The official problem statement (titled Group Work) is published in the CEMC 2022 CCC Junior problem set.

Where Beginners Often Get Stuck

  • The input format: X lines, then immediately Y, with no blank line in between; miscounting blocks breaks the whole parse.
  • The task is verification, not optimization: many students start rearranging groups to satisfy as many constraints as possible, but the groups are already fixed — you only count violations.
  • Names are strings of 1 to 10 uppercase letters, not integers.
  • "Groups of three" sounds important, yet the algorithm never uses it: we only need each person's group number. Learning to separate scene-setting facts from information the algorithm actually needs is a core contest skill.

Core Algorithm Idea

Restate the problem: for every constraint, answer one yes/no question — "does the current grouping satisfy it?" That question is a table lookup. If we know everyone's group number in advance, each constraint costs two lookups and one comparison. The right structure is a hash map (a Python dict, a C++ unordered_map): store name → group id, then build the table once and sweep the constraints twice, counting each kind of violation.

State / Data Structure Design

One structure does all the work: group_of, a map from each name to its group id (0 to G-1). We do not need to keep the rosters or build a relationship graph — every constraint is checked independently against this single table.

Algorithm Steps

  1. Read the X same-group constraints into a list.
  2. Read the Y different-group constraints into another list.
  3. Read the G groups; for each group g, record group_of[name] = g for its three members.
  4. Sweep the same list: if group_of[a] != group_of[b], increment the answer.
  5. Sweep the diff list: if group_of[a] == group_of[b], increment the answer.
  6. Print the total.

Sample Check with the Official Data

As a check table (group ids: A C G = 0, B D F = 1, E H I = 2, J K L = 3):

Constraint Rule Actual Verdict
A-B must be together A in group 0, B in group 1 violated
G-L must be together G in group 0, L in group 3 violated
J-K must be together both in group 3 ok
D-F must be apart both in group 1 violated
D-G must be apart D in group 1, G in group 0 ok

Three of the five constraints are violated, so the answer is 3 — matching the official sample output. Dry-run this table by hand, then test both official samples.

Python Solution

import sys

def main():
    tokens = sys.stdin.read().split()
    pos = 0

    def next_token():
        nonlocal pos
        tok = tokens[pos]
        pos += 1
        return tok

    X = int(next_token())
    same = [(next_token(), next_token()) for _ in range(X)]

    Y = int(next_token())
    diff = [(next_token(), next_token()) for _ in range(Y)]

    G = int(next_token())
    group_of = {}
    for g in range(G):
        for _ in range(3):
            group_of[next_token()] = g

    violated = 0
    for a, b in same:
        if group_of[a] != group_of[b]:
            violated += 1
    for a, b in diff:
        if group_of[a] == group_of[b]:
            violated += 1

    print(violated)

main()

Reading all tokens up front with a pointer avoids every "how many lines per block" pitfall; the dictionary group_of does every lookup.

C++ Solution

#include <iostream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int X;
    std::cin >> X;
    std::vector<std::pair<std::string, std::string>> same(X);
    for (auto &p : same) {
        std::cin >> p.first >> p.second;
    }

    int Y;
    std::cin >> Y;
    std::vector<std::pair<std::string, std::string>> diff(Y);
    for (auto &p : diff) {
        std::cin >> p.first >> p.second;
    }

    int G;
    std::cin >> G;
    std::unordered_map<std::string, int> group_of;
    group_of.reserve(static_cast<size_t>(3) * G);
    for (int g = 0; g < G; ++g) {
        for (int i = 0; i < 3; ++i) {
            std::string name;
            std::cin >> name;
            group_of.emplace(std::move(name), g);
        }
    }

    long long violated = 0;
    for (const auto &p : same) {
        if (group_of.at(p.first) != group_of.at(p.second)) {
            ++violated;
        }
    }
    for (const auto &p : diff) {
        if (group_of.at(p.first) == group_of.at(p.second)) {
            ++violated;
        }
    }

    std::cout << violated << '\n';
    return 0;
}

unordered_map mirrors the Python dictionary; reserve avoids rehashing; stream synchronization is off for speed. Both versions use identical logic and conditions, cross-verified on 200 random cases and at full-mark scale.

Why the Algorithm Is Correct

  • Correct table: the statement guarantees every constrained name appears in exactly one group, so group_of holds exactly one correct entry per name.
  • Complete checks: a same-group rule is violated if and only if the two group ids differ; an apart rule is violated if and only if they match. Two lookups plus one comparison cover every case with no gaps.
  • No double counting: each pair appears in at most one constraint, so every constraint is counted exactly once.

Therefore the final count equals the number of violated constraints, which is exactly what the problem asks for.

Complexity Analysis

Time O(X + Y + 3G): one linear pass over every constraint and every group member, with O(1)-average lookups. Space O(3G) for the name-to-group map. The full-marks bounds (G, X, Y ≤ 100000) pass comfortably in both languages.

Common Mistakes

  1. Linear search through a list of groups for every constraint, degrading to O((X+Y)·G) and timing out on full data.
  2. Flipping the conditions: a same-group constraint is violated when the two ids differ; a different-group constraint is violated when they are equal.
  3. Misreading the input blocks — always test both official samples after writing the parser.
  4. Over-thinking deduplication: the problem guarantees each pair appears at most once, so just count each line once.
  5. Adding defensive lookups "in case a name is missing" — the statement guarantees every constrained name is in exactly one group, so plain lookups are safe and correct.

Review Questions

  1. Why is no search or dynamic programming needed here? Which sentence in the statement tells you this is a verification task rather than an optimization task?
  2. If "groups of three" became "groups of any size", would the solution change? What does that tell you about which input guarantees the algorithm actually uses?
  3. Why does the statement promise each pair appears at most once? What would the output mean if pairs could repeat?
  4. Using the official Sample 2 data, walk the table by hand: if D moved into group 3 (with J, K, L), what would the answer become?

分组名单已经定了。现在老师手里还有两份清单:一份写着"这些学生必须在同一组",另一份写着"这些学生不能在同一组"。你的任务是数一数:已经排好的分组,一共违反了多少条要求?CCC 2022 的 J4/S2(CEMC 官方题名 Group Work,社区里也常叫 Good Groups)看起来像一道数数题,真正考的却是:你能不能把"人名 → 组别"这件事查得又快又准。整道题只需要一个字典,加两轮遍历。

本题在 CCC DSA 地图里的位置

  • Level: Junior J4 ≡ Senior S2(同一道题,两个级别共用)
  • Pattern: Constraint Checking with a Hash Map(用哈希表做约束校验)
  • Prerequisite: 多行字符串读入、循环与条件判断(Python 基础:输入、运算符与条件)、字典与哈希表(专项笔记计划中)
  • What this teaches: 把"关系是否成立"翻译成一次 O(1) 的查表,用"计数验证"代替"重新构造"
  • Where it appears again: 计数模式专题(计划中)、CCC 2017 J5/S3 Nailed It!(计划中)、一切"给定分配、统计违规"的验证类问题

下一步推荐

如果你已经理解这道题,可以继续看:

题目要我们做什么

一个班被分成了 G 个三人小组,分组方案已经给定、不可更改。输入分三段:

  1. 第一行一个整数 X,接下来 X 行,每行两个名字:这两个学生必须同组。
  2. 接下来一行一个整数 Y,再接下来 Y 行,每行两个名字:这两个学生不能同组。
  3. 接下来一行一个整数 G,最后 G 行是已经排好的分组,每行三个名字。

所有约束里,同一对学生至多出现一次;名字是 1 到 10 个大写字母;每个出现在约束里的名字,一定恰好属于某一个小组。

输出一个整数:一共有多少条约束被违反了(范围是 0 到 X+Y)。

官方题目(Group Work)收录在 CEMC 2022 CCC Junior 官方题集中。

官方样例 2:

3
A B
G L
J K
2
D F
D G
4
A C G
B D F
E H I
J K L

答案是 3:A-B 被拆散(违规),G-L 被拆散(违规),J-K 在一起(不违规),D-F 被放进了同一组(违规),D-G 在不同组(不违规)。五条约规,违反三条。

初学者容易卡在哪里

  • 读入结构看晕:X 行之后没有空行,直接就是 Y,然后是 G。如果按"先读一个总数"的习惯去猜结构,很容易读错位。
  • 以为要重新分组:很多人看到"必须同组/不能同组",就开始想怎么安排分组才最优。题目并不是这个意思——分组已经定死了,你只负责验收和计数。
  • 把名字当数字:人名是字符串,不能用 int 读入;而且名字长短不一,必须用字符串处理。
  • "每组三人"这个条件用不上:乍看像关键信息,其实我们的算法根本不关心每组几个人,只关心"某个人在哪一组"。这正是读题时要区分"描述场景的设定"和"算法真正需要的信息"。

核心算法概念

把问题翻译一下:对每一条约束,我们只要回答一个判断题——"这两个人现在的组别,和约束要求的一致吗?"

判断"两个人在不在同一组",本质上是一次查表:如果我们事先知道每个人的组号,那么任何一条约束都只需要两次查表加一次比较。关键数据结构是哈希表(Python 里叫字典 dict,C++ 里叫 unordered_map):把"人名 → 组号"存成键值对,查询平均 O(1)。

于是整道题变成:建一次表,扫两遍约束,每遍统计一种违规。

状态定义

只有一份核心数据:

  • group_of:字典,键是人名(字符串),值是组号(0 到 G-1 的整数)。

不需要保存分组名单本身,也不需要保存"谁和谁一起"的关系图——关系都已经浓缩成约束列表里的逐条比对了。

算法步骤

  1. 依次读入 X 条"必须同组"约束,存成列表 same。
  2. 读入 Y 条"不能同组"约束,存成列表 diff。
  3. 读入 G 个小组,给每组一个组号 g,把组里三个名字都写进 group_of[name] = g。
  4. 遍历 same:若 group_of[a] != group_of[b],违规数加一。
  5. 遍历 diff:若 group_of[a] == group_of[b],违规数加一。
  6. 输出违规总数。

用官方样例 2 走一遍:四组编号 0 到 3,查表得到 A=0、B=1、G=0、L=3、J=3、K=3、D=1、F=1。逐条检查:A-B(0≠1,违规 +1)、G-L(0≠3,违规 +1)、J-K(3=3,不违规)、D-F(同组,违规 +1)、D-G(1≠0,不违规)。合计 3,与样例输出一致。

官方样例核对

把上面的 walkthrough 整理成一张核对表(组号:A C G=0,B D F=1,E H I=2,J K L=3):

约束 要求 实际 判定
A-B 必须同组 A 在 0 组,B 在 1 组 违规
G-L 必须同组 G 在 0 组,L 在 3 组 违规
J-K 必须同组 同在 3 组 通过
D-F 不能同组 同在 1 组 违规
D-G 不能同组 D 在 1 组,G 在 0 组 通过

五条约束违反三条,输出 3,与官方样例输出一致。写完代码后,先用这张表 dry run,再跑两个官方样例。

Python 解法

import sys

def main():
    tokens = sys.stdin.read().split()
    pos = 0

    def next_token():
        nonlocal pos
        tok = tokens[pos]
        pos += 1
        return tok

    X = int(next_token())
    same = [(next_token(), next_token()) for _ in range(X)]

    Y = int(next_token())
    diff = [(next_token(), next_token()) for _ in range(Y)]

    G = int(next_token())
    group_of = {}
    for g in range(G):
        for _ in range(3):
            group_of[next_token()] = g

    violated = 0
    for a, b in same:
        if group_of[a] != group_of[b]:
            violated += 1
    for a, b in diff:
        if group_of[a] == group_of[b]:
            violated += 1

    print(violated)

main()

要点:一次性读入全部 token,指针式取用,避免被"多少行一组"的边界问题坑到;字典 group_of 完成所有查找。

C++ 解法

#include <iostream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int X;
    std::cin >> X;
    std::vector<std::pair<std::string, std::string>> same(X);
    for (auto &p : same) {
        std::cin >> p.first >> p.second;
    }

    int Y;
    std::cin >> Y;
    std::vector<std::pair<std::string, std::string>> diff(Y);
    for (auto &p : diff) {
        std::cin >> p.first >> p.second;
    }

    int G;
    std::cin >> G;
    std::unordered_map<std::string, int> group_of;
    group_of.reserve(static_cast<size_t>(3) * G);
    for (int g = 0; g < G; ++g) {
        for (int i = 0; i < 3; ++i) {
            std::string name;
            std::cin >> name;
            group_of.emplace(std::move(name), g);
        }
    }

    long long violated = 0;
    for (const auto &p : same) {
        if (group_of.at(p.first) != group_of.at(p.second)) {
            ++violated;
        }
    }
    for (const auto &p : diff) {
        if (group_of.at(p.first) == group_of.at(p.second)) {
            ++violated;
        }
    }

    std::cout << violated << '\n';
    return 0;
}

要点:unordered_map 等价于 Python 字典;reserve 预分配避免 rehash;关闭流同步加快读入。两个版本的思路、判断条件完全一致,已用 200 组随机数据和满分级规模数据互相验证过输出。

正确性说明

  • 建表正确:题目保证每个出现在约束中的名字恰好属于一个组,因此每个名字在 group_of 中恰有一条记录,查到的组号唯一且正确。
  • 判定完备:一条"必须同组"约束被违反,当且仅当两人的组号不同;一条"不能同组"约束被违反,当且仅当两人的组号相同。两次查表加一次比较覆盖了全部情况,没有遗漏。
  • 计数不重不漏:题目保证同一对名字在所有约束中至多出现一次,因此每条约束被恰好统计一次。

因此最终的违规总数就是题目所求的答案。

复杂度分析

  • 时间:O(X + Y + 3G),即线性扫描所有约束和所有成员,每次查询平均 O(1)。
  • 空间:O(3G),存储所有人名到组号的映射。
  • 满分数据范围 G ≤ 100000、X,Y ≤ 100000,两个语言都在毫秒到秒级轻松通过,不需要任何优化技巧。

常见错误

  1. 用列表线性查找:把分组存成 list,每次约束都扫一遍找人,复杂度退化成 O((X+Y)·G),满分数据必超时。这题的第一课就是"查询多,就上哈希表"。
  2. 判断条件写反:"必须同组"的违规条件是"组号不同";"不能同组"的违规条件是"组号相同"。把两条规则的方向记混,是最常见的失分点。
  3. 读错位:X 行之后紧接 Y,没有空行分隔;写完读入代码后务必用两个官方样例各跑一遍。
  4. 重复计数:题面保证每对至多出现一次,所以不需要去重;但也不要因为"防止重复"而把不同行的同约束误删——按行计数即可。
  5. 忽略约束名字必然在分组里:题面保证了约束中的每个名字都恰好出现在一个组中,所以查表不会出现 KeyError(Python)或未命中(C++),不需要写兜底分支。

复习问题

  1. 为什么这道题不需要搜索、不需要动态规划?把"验证"和"求解"区分开的关键线索是哪句话?
  2. 如果把"每组三人"改成"每组人数任意",解法需要改吗?这说明算法用到了哪些输入保证?
  3. 题目为什么强调"每对学生至多出现一次"?如果允许重复出现,输出语义会变成什么?
  4. 用官方样例 2 的数据,手算一遍查表过程:如果 D 被换到第 3 组(J K L 那组),答案会变成多少?

Questions or feedback?

Have a question about this article or want to suggest an improvement? Send us a private message.

Send Feedback

有问题或建议?

如果你对本文有疑问,或希望提出改进建议,请给我们发送私密留言。

发送反馈

Back to CP Weekly Challenge
返回 CP Weekly Challenge

CP ACADEMY

Not sure which challenges fit your level?

Use the CP assessment to explore your current starting point and choose topics to practise next. You can keep reading and practising without taking an assessment.

60 minutes continuously · Python or C++ · Choose your competition goal

Discover Your CP Level

Scroll to Top