Skip to content

T28749: Top2招生工作

http://cs101.openjudge.cn/practice/28749/

小P作为一个学习过计算概论的贵校大一同学,参加了招生工作,负责整理今年考得比较好的同学们对于Top2两所学校的倾向,其中倾向于贵校的同学标记为“红色”,倾向于隔壁的同学标记为“紫色”,没有倾向的同学标记为“白色”。

小P进入招生组后了解到,招生组中靠谱的志愿者学长学姐已经在考前做好了招生的准备工作:每一位志愿者和一位或者多位考得比较好的同学建立了良好的联系,但是因为贵校志愿者数量有限,每一位考得比较好的同学至多和两位志愿者建立了良好的联系。

小P在接受招生组培训时,知道了招生中的重要特点:

  • 《联系不上》:在出分后,考得比较好的同学只会和建立了良好联系的志愿者进行有效沟通

  • 《懒惰的贵校同学》:一次有效沟通必须由志愿者发起,并且因为贵校志愿者比较懒,所以会一次性和所有具有良好联系的同学沟通

  • 《丰俭由人》:志愿者可以进行若干次有效沟通,也可以偷懒摸鱼不沟通

  • 《独自输出》:隔壁因为某些因素,在今年的招生中采取不作为策略,因此同学倾向的改变仅取决于贵校志愿者的有效沟通

  • 每次有效沟通的效果如下:

    • 《过犹不及》:原本就是“红色”倾向的同学会因为厌烦,一怒之下转变为“紫色”倾向
    • 《拉拢中间派》:原本是“白色”倾向的同学会感受到贵校的热情,转变为“红色”倾向
    • 《摇摆不定》:原本是”紫色“倾向的同学受到贵校的感召,决定转变为”白色“倾向,开始两边摇摆

招生组知道小P是学习过算法的大佬,于是将安排每一位志愿者有效沟通次数的重任交给了小P,希望能够用最少的总有效沟通次数,实现将所有考得比较好的同学倾向变为“红色”。

输入

输入的第一行包含两个整数n和m,其中n(1 <= n <= 2*10^5)是考得比较好的同学的数量,同学编号从1到n;m(0 <= m <= 2n)是志愿者的数量。

输入的第二行包含是一个由n个字符组成的字符串,这些字符是R,P,W中的一种(R代表倾向为红色,P代表倾向为紫色,W代表倾向为白色),字符串中第i个字符,就代表了第i位同学最初的倾向。

接下来的m行,描述的是与每一位志愿者建立了良好联系的同学的信息:每一行以一个整数k(1 <= k <= n)开始,代表了与这位志愿者建立良好联系的同学数量,之后跟着k个不同的整数,代表了这些同学的编号。每一位同学至多和两个志愿者建立了良好联系。

输出

如果可以实现将所有考得比较好的同学倾向变为“红色”,则输出一个整数,即所需要的最少总有效沟通次数;如果不能够实现,则输出"impossible"。

样例输入

sample1 input:
8 6
PWRWRRRP
2 1 4
1 2
4 4 5 6 7
3 5 6 7
1 8
1 8

sample1 output:
8

样例输出

sample2 input:
4 3
RPWR
2 1 2
2 2 3
2 3 4

sample2 output:
impossible

提示

Recursion, 洪水填充 , 1100

Python程序常用的优化有三个:sys.setrecursionlimit(1<<30)、@lru_cache(maxsize = None)、sys.stdin.read。前两个主要是解决递归程序爆栈和重复计算子问题,后一个解决输入数据太多的问题。同学可以使用其中的一个或多个来优化程序。

来源

2024 TA-Xjk

一、题意概括

每个学生属于三种类型之一:

  • R(红) → 0
  • P(蓝) → 1
  • W(白) → 2

每个学生最多可以报名两个志愿(即两个“志愿者/招生点”)。 每个志愿者也可以被多个学生报考。

题目给出的约束是: 对每个学生 i

  • 若他报了两个志愿者 v1, v2,则有一个约束方程: (xv1+xv2+ai)0(mod3)
  • 若他只报了一个志愿者 v,则: (xv+ai)0(mod3)
  • 若他一个志愿都没报,则必须是 R 类型(即 a_i = 0)。

目标:找到所有志愿者的取值(0,1,2),使所有方程成立,并使志愿者的值之和最小(即总代价最小)。


二、转化为图问题

  • 志愿者是节点
  • 学生提供了约束方程
  • 每个学生对应一条或多条
学生类型志愿个数对志愿者约束
0 个志愿若非R,则不可能
1 个志愿1x_v = (-a_i) mod 3 → 志愿者取定值
2 个志愿2x_u + x_v = (-a_i) mod 3 → 志愿者间关系

于是我们构建志愿者图:

  • (u,v,b) 表示:x_u + x_v ≡ b (mod3)
  • 节点可能有固定值(来自单志愿学生)。

三、算法步骤

  1. 读入学生与志愿者信息,转换成数字(R=0, P=1, W=2)。
  2. 检测无志愿学生:若不是R,则直接 impossible
  3. 建立志愿者图:
    • 对双志愿学生,加边;
    • 对单志愿学生,记录节点的固定值。
  4. 检测矛盾固定值(同一志愿者被要求多个不同固定值)。
  5. 在志愿者图上进行 BFS/DFS 连通分量遍历:
    • 若分量内有固定点 → 以这些为根进行值传播;
    • 若没有固定点 → 任选一个节点为根,尝试 3 种起始值(0/1/2),选出合法且总代价最小的方案;
    • 若出现冲突(方程不成立)→ impossible
  6. 累加每个分量的最小代价
  7. 输出总代价

四、复杂度分析

  • 建图:O(n + m)
  • BFS/DFS:O(m + n)
  • 每个分量最多尝试 3 次传播 总体复杂度 O((n + m) * 3) ≈ O(n + m),可轻松通过。

✅ 带中文注释的完整代码

python
import sys
from collections import deque, defaultdict
sys.setrecursionlimit(1 << 25)
input = sys.stdin.readline

# === 输入阶段 ===
n, m = map(int, input().split())
s = input().strip()

# R=0, P=1, W=2
enc = {'R':0, 'P':1, 'W':2}
a = [enc[ch] for ch in s]

# 每个学生的志愿列表(0-index)
stu = [[] for _ in range(n)]
# 每个志愿者对应的学生
vol = [[] for _ in range(m)]

for vid in range(m):
    parts = list(map(int, input().split()))
    k = parts[0]
    for x in parts[1:]:
        x0 = x-1
        stu[x0].append(vid)
        vol[vid].append(x0)

# === 基本合法性检测 ===
for i in range(n):
    if not stu[i] and a[i] != 0:
        print("impossible")
        sys.exit(0)

# === 建立志愿者图 ===
# 对于双志愿学生,增加一条边 (v1,v2,b)
# 对于单志愿学生,记录该志愿者固定值 b
adj = [[] for _ in range(m)]
fixed_constraints = defaultdict(list)  # vid -> 期望值列表

for i in range(n):
    vs = stu[i]
    if len(vs) == 2:
        v1, v2 = vs
        b = (-a[i]) % 3
        adj[v1].append((v2, b))
        adj[v2].append((v1, b))
    elif len(vs) == 1:
        v = vs[0]
        b = (-a[i]) % 3
        fixed_constraints[v].append(b)

# === 检查固定值冲突 ===
for v, lst in fixed_constraints.items():
    first = lst[0]
    for val in lst:
        if val % 3 != first % 3:
            print("impossible")
            sys.exit(0)

fixed_value = {v: lst[0] % 3 for v, lst in fixed_constraints.items()}

# === BFS 遍历志愿者图 ===
visited = [False]*m
total_cost = 0

# 辅助函数:给定初始根节点赋值,传播约束
def try_with_roots(root_assignments):
    assigned = {}
    dq = deque()
    for k, vv in root_assignments.items():
        assigned[k] = vv % 3
        dq.append(k)
    while dq:
        u = dq.popleft()
        xu = assigned[u]
        for (w, b) in adj[u]:
            exp = (b - xu) % 3
            if w in assigned:
                if assigned[w] != exp:
                    return None  # 冲突
            else:
                assigned[w] = exp
                dq.append(w)
    return assigned

# === 处理所有连通分量 ===
for v_start in range(m):
    if visited[v_start] or not vol[v_start]:
        visited[v_start] = True
        continue

    # 收集该连通分量所有节点
    comp_nodes = []
    q = deque([v_start])
    visited[v_start] = True
    while q:
        u = q.popleft()
        comp_nodes.append(u)
        for (w, _) in adj[u]:
            if not visited[w]:
                visited[w] = True
                q.append(w)

    # 若该分量内有固定值,直接传播
    comp_fixed = {u: fixed_value[u] for u in comp_nodes if u in fixed_value}
    if comp_fixed:
        res = try_with_roots(comp_fixed)
        if res is None:
            print("impossible")
            sys.exit(0)
        # 未被约束的节点可设为0以最小化代价
        for u in comp_nodes:
            if u not in res:
                res[u] = 0
        total_cost += sum(res[u] for u in comp_nodes)
        continue

    # 否则,尝试 root=0,1,2 三种情况
    root = comp_nodes[0]
    best = None
    for r in range(3):
        res = try_with_roots({root: r})
        if res is None:
            continue
        for u in comp_nodes:
            if u not in res:
                res[u] = 0
        cost = sum(res[u] for u in comp_nodes)
        if best is None or cost < best:
            best = cost

    if best is None:
        print("impossible")
        sys.exit(0)
    total_cost += best

# === 输出结果 ===
print(total_cost)

💡 核心思路总结

  • 本题实质是一个 模 3 线性方程系统
  • 志愿者为变量,学生为约束;
  • 每个连通分量独立求解;
  • 有固定点的分量直接确定;
  • 无固定点的分量需尝试三种起点;
  • 取最小代价(即志愿者取值之和最小)。

💡 题目理解与建模

题目看似描述招生故事,实际上是一个模 3 的线性方程系统 + 连通分量最小化问题


一、状态变化规律(核心)

每个学生有三种状态:

  • R(红)= 0
  • P(紫)= 1
  • W(白)= 2

一次“沟通”操作会让志愿者联系到的所有学生的状态发生如下变化:

初始沟通后
R(0)P(1)
P(1)W(2)
W(2)R(0)

即:状态值 +1(mod 3)


二、志愿者变量定义

令第 i 个志愿者进行 xi 次沟通(取模 3)。

对某个学生 s,若与志愿者集合 Vs=v1,v2 建立联系,则该学生的最终状态为:

as+xv1+xv20(mod3)

(要求最终为红色,即状态值为 0)

因此:

xv1+xv2as(mod3)


三、根据学生的志愿者数构造约束

✅ 情况 1:学生有两个志愿者

得到一条 志愿者之间的边

xu+xv=b(mod3) 其中 b=(as)mod3

即:在志愿者图中添加一条带权边 (u,v,b)

✅ 情况 2:学生只有一个志愿者

得到一条 固定约束

xv=b(mod3)

即:该志愿者的值被固定

❌ 情况 3:学生没有志愿者

若他不是红色,则永远无法变红,直接输出 impossible


四、问题转化为图问题

我们得到一个 带模约束的图

  • 节点:志愿者
  • 边:来自两个志愿者共联一个学生的约束
  • 节点固定值:来自单志愿者学生
  • 每个连通分量可以独立求解

任务:

  1. 判断是否存在冲突(mod 3 矛盾)
  2. 若存在多种可行赋值,选取沟通次数之和最小的方案(每个志愿者的沟通次数是 0/1/2)

五、连通分量求解逻辑

  1. 若该分量中存在固定志愿者值,则从它出发 BFS 推导所有节点的值。若出现矛盾 → impossible。
  2. 若该分量没有固定志愿者:
    • 任意选一个志愿者为根,尝试令其取值 0、1、2;
    • 每种情况 BFS 推导;
    • 若冲突则丢弃;
    • 在所有可行方案中选总沟通次数最小的方案。

最终把每个分量的最优解相加。


✅ AC 代码(带详细中文注释)

python
import sys
from collections import deque

def main():
    # ---------- 快速读入 ----------
    data = sys.stdin.buffer.read().split()
    it = iter(data)
    n = int(next(it)); m = int(next(it))
    s = next(it).decode()

    # ---------- 编码倾向 ----------
    enc = {'R':0, 'P':1, 'W':2}
    a = [enc[ch] for ch in s]

    # ---------- 建立学生和志愿者关系 ----------
    stu = [[] for _ in range(n)]   # 每个学生对应的志愿者
    vol = [[] for _ in range(m)]   # 每个志愿者对应的学生
    for vid in range(m):
        k = int(next(it))
        for _ in range(k):
            x = int(next(it)) - 1
            stu[x].append(vid)
            vol[vid].append(x)

    # ---------- 检查无志愿者的学生 ----------
    for i in range(n):
        if not stu[i] and a[i] != 0:
            print("impossible")
            return

    # ---------- 构造志愿者之间的约束图 ----------
    adj = [[] for _ in range(m)]     # 邻接表,存储 (邻居, b)
    fixed_value = [-1] * m           # 某志愿者的固定值(若有单志愿者学生约束)

    for i in range(n):
        vs = stu[i]
        if len(vs) == 2:
            v1, v2 = vs
            b = (-a[i]) % 3
            adj[v1].append((v2, b))
            adj[v2].append((v1, b))
        elif len(vs) == 1:
            v = vs[0]
            b = (-a[i]) % 3
            if fixed_value[v] == -1:
                fixed_value[v] = b
            elif fixed_value[v] != b:
                print("impossible")
                return
        # len==0 情况前面已排除

    # ---------- 初始化 ----------
    visited = [False] * m
    assigned = [-2] * m  # 志愿者沟通次数:-2=未赋值,0/1/2=次数 mod3
    total_cost = 0

    # ---------- 逐个连通分量处理 ----------
    for v in range(m):
        if visited[v]:
            continue
        if not vol[v]:  # 没关联学生的志愿者,跳过
            visited[v] = True
            continue

        # BFS 收集该连通分量内所有志愿者
        comp = []
        dq = deque([v])
        visited[v] = True
        while dq:
            u = dq.popleft()
            comp.append(u)
            for w, _ in adj[u]:
                if not visited[w]:
                    visited[w] = True
                    dq.append(w)

        # 找出该分量内的固定节点
        comp_fixed_nodes = [u for u in comp if fixed_value[u] != -1]

        # ---------- BFS传播函数 ----------
        def propagate_init(init_list):
            touched = []  # 记录被修改过的志愿者,方便回滚
            q = deque()
            for (uu, val) in init_list:
                assigned[uu] = val
                touched.append(uu)
                q.append(uu)
            # 传播约束:x_v = (b - x_u) mod 3
            while q:
                uu = q.popleft()
                xuu = assigned[uu]
                for vv, b in adj[uu]:
                    need = (b - xuu) % 3
                    cur = assigned[vv]
                    if cur == -2:
                        assigned[vv] = need
                        touched.append(vv)
                        q.append(vv)
                    elif cur != need:
                        # 冲突 -> 回滚并返回失败
                        for t in touched:
                            assigned[t] = -2
                        return None
            return touched  # 返回成功修改的节点列表

        # ---------- 情况 1:该分量有固定志愿者 ----------
        if comp_fixed_nodes:
            init_list = [(u, fixed_value[u]) for u in comp_fixed_nodes]
            touched = propagate_init(init_list)
            if touched is None:
                print("impossible")
                return
            # 未连接的节点设为0
            comp_cost = 0
            for u in comp:
                if assigned[u] == -2:
                    assigned[u] = 0
                comp_cost += assigned[u]
            total_cost += comp_cost
            continue

        # ---------- 情况 2:该分量无固定志愿者 ----------
        root = comp[0]
        best_cost = None
        best_touched = None

        # 尝试根节点为 0,1,2 的三种情况
        for r in (0,1,2):
            touched = propagate_init([(root, r)])
            if touched is None:
                continue
            # 未赋值的节点设为0
            for u in comp:
                if assigned[u] == -2:
                    assigned[u] = 0
                    touched.append(u)
            cost = sum(assigned[u] for u in comp)
            if best_cost is None or cost < best_cost:
                best_cost = cost
                best_touched = list(touched)
            # 回滚,为下次尝试清空状态
            for t in touched:
                assigned[t] = -2

        if best_cost is None:
            print("impossible")
            return

        # 重新传播最优方案并保留结果
        propagate_init([(root, 0)])
        for u in comp:
            if assigned[u] == -2:
                assigned[u] = 0
        total_cost += best_cost

    # ---------- 输出最小总沟通次数 ----------
    print(total_cost)

if __name__ == "__main__":
    main()

🧠 思路总结(简洁版)

步骤含义操作
1把每个学生的状态转为数字 0/1/2R→0, P→1, W→2
2根据学生连接的志愿者数,构造模 3 方程两志愿者→等式;一志愿者→固定值
3构造志愿者图每个志愿者是节点,边权表示模 3 关系
4分别处理每个连通分量BFS 传播解模 3 线性系统
5若有多解,枚举根取最小代价root=0/1/2 三种方案
6汇总最小沟通次数输出结果

✅ 复杂度分析

部分复杂度
构建图O(n + m)
BFS传播O(n + m)
每分量枚举3种root最多 3×O(节点数)
总复杂度O(n + m) (常数极小)

内存占用也为 O(n + m),完全可通过 2×10⁵ 级别数据。


如果你愿意,我还可以帮你画一张 “志愿者-学生图 + 状态变化 mod3 动态示意图”(比如样例输入),帮助直观理解 “x_u + x_v ≡ b (mod 3)” 是如何传播的。是否需要?