把「每条记录 = 一个线性等式」写成模 7 同余方程组,用高斯消元求解;3..9 天与 7 个剩余类一一对应,无解/自由元分别判 Inconsistent/Multiple。
OJ: roj
题目 ID: 3101
难度:提高+/省选-
标签:高斯消元同余线性代数数学python
创建: 2026-10-01 17:06
更新: 2026-10-07 13:50
形式化题目
有
- 该工人开工与收工是星期几(一周 7 天,首尾都算在工作日内);
- 他在这段时间里依次做完的
个部件的型号(同一型号可能出现多次)。
求每种型号的耗时;结果需要区分「唯一」「不唯一」「无解」三种情况。
记
约束:0 0 结束。
样例 1 的解是 MON 到 THU 的 4 天);
样例 2 的两条记录都把同一型号限制成不同的天数,因此无解。
两个必须先说清的读题点。 其一,天数按首尾都算的方式数:
MON THU是 4 天(周一、二、三、四), 所以右端是而不是 ,否则样例立刻出错。 其二,3…9 天共 7 个取值,模 7 后得到 七个互不相同的余数, 恰好覆盖全部剩余类,因此「模 7 的解」与「合法天数」一一对应; 这条性质是后面能把方程解直接翻译回天数、并且用「自由元是否为 0」判多解的根据。
正解
思路
朴素做法:枚举天数
最直接的做法是枚举每组
朴素做法失败的原因是它没有利用输入的结构:每条记录给的是一个线性等式, 而不是一次需要模拟的过程。把天数当未知量、把记录当方程,问题就落进了线性代数的框架。
第一步:一条记录 ⇔ 一条同余方程
一条记录 k s e 后面跟
而我们只看得到星期:工人可能做满几个星期,所以从星期上看不出
于是每条记录给出一条模 7 的线性同余方程,
第二步:为什么要用 7,而不是「3 到 9」
模数选 7 不是随手挑的:合法天数是 3…9 共 7 个数,模 7 后恰好落在 7 个不同的剩余类上, 映射是双射。
| 天数 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|
| 模 7 余数 | 3 | 4 | 5 | 6 | 0 | 1 | 2 |
这带来两个好处:其一,解出的余数
第三步:模 7 高斯消元
- 逐列找主元,把主元行换上来,整行乘逆元归一化成 1;
- 用主元行把它下方该列消成 0(只往下消,具体值留到回代再算, 比每一步都上下通消的高斯-约当少掉大约一半的整行运算);
- 消完后检查:
- 出现系数全 0 而右端非 0 的行(
, )→ 无解,输出 Inconsistent data.; - 主元个数
→ 有自由元,把自由元全取 0 就得到一组解,故至少两组解 → Multiple solutions.; - 主元个数
→ 唯一解,逆序回代求出每个 ,还原成天数输出。
- 出现系数全 0 而右端非 0 的行(
每找到一个主元就要扫一遍它下方的行、并对每行处理长度不超过
用样例核对
样例 1 的三条记录转成方程(
| 记录 | 右端 |
||
|---|---|---|---|
1 2 MON THU |
1 | 1 | |
2 3 MON FRI |
2 | 1 | |
3 3 MON SUN |
1 | 2 |
第 2 行减去第 1 行得 8 3 一致。
样例 2 的两条记录分别是 MON TUE 与 MON WED,右端为 2 和 3,
都只涉及型号 3,于是 Inconsistent data.。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 17:06
# update_at: 2026-10-01 17:15
import sys
from collections import Counter
from collections.abc import Iterator
from itertools import islice
DAYS = [b"MON", b"TUE", b"WED", b"THU", b"FRI", b"SAT", b"SUN"] # 星期几 → 0..6
INV = [0, 1, 4, 5, 2, 3, 6] # 模 7 的乘法逆元:3·5、2·4、6·6、1·1 都同余 1,INV[0] 用不到
MOD = 7
MIN_DAYS = 3 # 单件耗时下界;余数小于它时加一个周期 7 即还原成 7/8/9 天
UNIQUE, MULTIPLE, INCONSISTENT = 0, 1, 2 # 方程组解的状态;只有 UNIQUE 才有确定的答案
TEXT = {MULTIPLE: "Multiple solutions.", INCONSISTENT: "Inconsistent data."}
def tokens() -> Iterator[bytes]:
"""按行产出输入词元:整份数据有 450 万个数字,一次性读入会多占几百 MB。"""
for line in sys.stdin.buffer:
yield from line.split()
def read_case(data: Iterator[bytes], n: int, m: int) -> tuple[list[list[int]], list[int]]:
"""读入 m 条工人记录,返回 (模 7 系数矩阵, 右端余数向量)。
第 i 条记录给出一条同余方程 sum_j c[i][j] * x_j ≡ d_i (mod 7):
c[i][j] 是型号 j 在这批活里出现的次数(同型号做几次就累加几次耗时),
d_i 是工人在岗天数对 7 取余,开工星期 s、收工星期 e、首尾都算,故 d_i ≡ e - s + 1。
"""
mat: list[list[int]] = []
rhs: list[int] = []
for _ in range(m):
k = int(next(data))
start, end = next(data), next(data)
counts = Counter(map(int, islice(data, k)))
mat.append([counts[v] % MOD for v in range(1, n + 1)])
rhs.append((DAYS.index(end) - DAYS.index(start) + 1) % MOD)
return mat, rhs
def gauss(mat: list[list[int]], rhs: list[int], n: int) -> tuple[int, list[int]]:
"""模 7 高斯消元:先消成阶梯形,再回代,返回 (解的状态, 一组特解)。
7 是素数,非零系数都有逆元,所以消元过程与实数域一样,只是把除法换成乘逆元。
"""
pivot_cols: list[int] = []
for col in range(n):
pivot = next((r for r in range(len(pivot_cols), len(mat)) if mat[r][col]), None)
if pivot is None: # 剩余行在这一列全是 0,说明 x[col] 是自由元
continue
rank = len(pivot_cols)
mat[rank], mat[pivot] = mat[pivot], mat[rank]
rhs[rank], rhs[pivot] = rhs[pivot], rhs[rank]
inv = INV[mat[rank][col]] # 主元归一化成 1
mat[rank] = [0] * col + [v * inv % MOD for v in mat[rank][col:]]
rhs[rank] = rhs[rank] * inv % MOD
for r in range(rank + 1, len(mat)): # 只往下消成阶梯形,回代时再算具体值
factor = mat[r][col]
if factor:
row = mat[r]
# 补上前导 0 是为了让「列下标」始终等于变量的编号
mat[r] = [0] * col + [(v - factor * w) % MOD for v, w in zip(row[col:], mat[rank][col:])]
rhs[r] = (rhs[r] - factor * rhs[rank]) % MOD
pivot_cols.append(col)
if len(pivot_cols) == len(mat): # 每条方程都拿到主元,剩下的只可能是 0 = 0
break
x = [0] * n
if any(rhs[len(pivot_cols):]): # 系数全 0 而右端非 0 的行形如 0 = c,方程无解
return INCONSISTENT, x
if len(pivot_cols) < n: # 自由元取 0 就是一组解,故至少两族解对应多组天数
return MULTIPLE, x
for i in range(n - 1, -1, -1): # 回代:主元列右侧的变量此时都已确定
col = pivot_cols[i]
x[col] = (rhs[i] - sum(v * x[j] for j, v in enumerate(mat[i][col + 1:], col + 1))) % MOD
return UNIQUE, x
def solve() -> None:
data = tokens()
out: list[str] = []
while True:
n, m = int(next(data)), int(next(data))
if n == 0 and m == 0:
break
mat, rhs = read_case(data, n, m)
state, x = gauss(mat, rhs, n)
# 耗时 3..9 天与模 7 的 7 个剩余类一一对应:0/1/2 分别还原成 7/8/9 天。
out.append(" ".join(str(r if r >= MIN_DAYS else r + MOD) for r in x) if state == UNIQUE else TEXT[state])
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 18:44
* update_at: 2026-10-06 18:44
*/
// main.cpp:把每条工人记录看成一条模 7 的线性同余方程,用高斯消元判定
// 无解 / 多解 / 唯一解,并把唯一解的余数还原成 3..9 天。
//
// 关键建模:
// - 设型号 j 在一条记录里出现 c 次,则工人在岗天数 d = sum c[j] * x[j](同型号做几次加几次);
// - 记录只给星期,首尾都算,所以 d ≡ e - s + 1 (mod 7),得到一条模 7 方程;
// - 合法天数 3..9 共 7 个,模 7 后恰好覆盖 7 个剩余类(0/1/2 对应 7/8/9),
// 因此「模 7 有多组解」等价于「天数有多组解」,余数可唯一还原成天数;
// - 7 是素数,Z_7 是域,除法换成乘逆元即可照搬高斯消元。
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
typedef long long ll;
const int MAXN = 305; // 型号数 n <= 300
const int MAXM = 305; // 记录数 m <= 300
const int MOD = 7; // 一周 7 天,方程都在模 7 意义下
// 模 7 的乘法逆元:1->1, 2->4, 3->5, 4->2, 5->3, 6->6(2*4、3*5、6*6 都 ≡ 1)
const int INV[MOD] = {0, 1, 4, 5, 2, 3, 6};
// 星期字符串转 0..6:day_num("MON") = 0, ..., day_num("SUN") = 6
int day_num(const char *s) {
const char *DAYS[MOD] = {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};
for (int i = 0; i < MOD; ++i)
if (strcmp(s, DAYS[i]) == 0)
return i;
return -1; // 题面保证输入合法,不会走到这里
}
// 系数矩阵与右端向量:a[i][j] 是第 i 条方程里 x_j 的系数(已对 7 取余)
ll a[MAXM][MAXN];
ll b[MAXM]; // b[i] = 第 i 条方程的右端 d_i mod 7
ll x[MAXN]; // 唯一解时每个型号的耗时(余数形式,0..6)
ll cnt[MAXN]; // 读记录时的临时计数:cnt[j] = 型号 j 在本条记录出现的次数
int n, m;
// 读入一条记录并转成一条方程:sum_j c[j] * x_j ≡ e - s + 1 (mod 7)
bool read_record(int i) {
ll k;
char s1[10], s2[10];
if (scanf("%lld %9s %9s", &k, s1, s2) != 3)
return false;
for (int j = 1; j <= n; ++j) cnt[j] = 0; // 只清这题会用到的下标范围
for (ll t = 0; t < k; ++t) {
int v;
scanf("%d", &v);
cnt[v]++; // 同型号出现 c 次,耗时累加 c 次,系数就是 c
}
for (int j = 1; j <= n; ++j)
a[i][j] = cnt[j] % MOD;
// 在岗天数首尾都算,右端是 (收工 - 开工 + 1) mod 7
b[i] = ((day_num(s2) - day_num(s1) + 1) % MOD + MOD) % MOD;
return true;
}
// 模 7 高斯消元(7 是素数,非零系数都有逆元)
// 返回值:0 = 唯一解,1 = 多解,2 = 无解;唯一解时结果存入 x[]
int gauss() {
int r = 0; // 已处理的主元行数(也是当前的秩)
for (int col = 1; col <= n && r < m; ++col) {
int p = -1; // 在 r..m-1 行里找这一列系数非 0 的主元行
for (int i = r; i < m; ++i)
if (a[i][col] != 0) { p = i; break; }
if (p == -1)
continue; // 这一列全是 0,x[col] 是自由元
// 把主元行换上来
for (int j = col; j <= n; ++j) {
ll t = a[r][j]; a[r][j] = a[p][j]; a[p][j] = t;
}
ll tb = b[r]; b[r] = b[p]; b[p] = tb;
// 主元归一化成 1:整行乘主元的逆元
ll inv = INV[a[r][col]];
for (int j = col; j <= n; ++j)
a[r][j] = a[r][j] * inv % MOD;
b[r] = b[r] * inv % MOD;
// 用主元行把它下方各行消成 0(只往下消,回代时再算具体值)
for (int i = r + 1; i < m; ++i) {
ll f = a[i][col];
if (f == 0) continue;
for (int j = col; j <= n; ++j)
a[i][j] = (a[i][j] - f * a[r][j] % MOD + MOD) % MOD;
b[i] = (b[i] - f * b[r] % MOD + MOD) % MOD;
}
r++; // 主元个数增加
}
// 消元后第 r..m-1 行的系数已全为 0,检查有没有 0 ≡ c (c ≠ 0) 的矛盾行
for (int i = r; i < m; ++i)
if (b[i] != 0)
return 2; // Inconsistent data.
if (r < n)
return 1; // 有自由元,自由元取 0 就是一组解,故天数不唯一
// 满秩,逆序回代求出每个 x[j]
for (int i = r - 1; i >= 0; --i) {
// 找到这一行的主元列
int col = 1;
while (col <= n && a[i][col] == 0) col++;
x[col] = b[i];
for (int j = col + 1; j <= n; ++j)
x[col] = (x[col] - a[i][j] * x[j] % MOD + MOD) % MOD;
}
return 0;
}
// 处理一组测试用例,返回 false 表示读到 n = m = 0 的结束标记
bool solve_case() {
if (scanf("%d %d", &n, &m) != 2)
return false;
if (n == 0 && m == 0)
return false;
for (int i = 0; i < m; ++i)
read_record(i);
int st = gauss();
if (st == 2) {
printf("Inconsistent data.\n");
} else if (st == 1) {
printf("Multiple solutions.\n");
} else {
// 余数 0/1/2 对应 7/8/9 天,其余直接就是天数
for (int j = 1; j <= n; ++j) {
ll d = x[j] < 3 ? x[j] + MOD : x[j];
printf("%lld%c", d, j == n ? '\n' : ' ');
}
}
return true;
}
int main() {
while (solve_case())
;
return 0;
}代码与上面的推导一一对应:
read_case是「记录 → 方程」:Counter(map(int, islice(data, k)))一次遍历统计每个 型号的出现次数(对应), DAYS.index(end) - DAYS.index(start) + 1对应右端; gauss是模 7 消元:INV表存模 7 的逆元(、 、 , 的逆元是它自己), 前向消元把主元行以下的该列清成 0, pivot_cols记录主元列。消元结束后,下标主元数的那些行系数已经全是 0,于是 any(rhs[len(pivot_cols):])就是 「有没有的行」的判定,而 len(pivot_cols) < n是「主元够不够个」的判定; solve只管还原与输出:余数直接当天数,否则加 7。
复杂度
设一组数据的型号数为
- 读入
(真实数据里 ,是本题最大的常数项); - 建方程
; - 高斯消元
(主元最多 个),回代 ; - 空间
存矩阵,不随 增长。
朴素枚举是 g1.in(13.7 MB,26 组,总部件数约
实现上有一处必须强调:输入不能一次性
read().split()。整份数据有约 450 万个数字, 全部转成bytes对象会占到 257 MB,直接 MLE。代码里改成按行流式产出词元, 峰值内存降到 16 MB。
总结
本题的全部难度在建模,不在写代码:
- 一条记录 = 一条线性方程:天数是未知量,同型号做几次就累加几次系数;
- 只在模 7 下成立:星期只能给出在岗天数模 7 的余数,右端是
; - 3…9 天 ↔ 7 个剩余类:这个一一对应让「解出的余数」能唯一还原成天数, 也让「自由元个数」直接决定是唯一解还是多解;
- 模 7 是域:高斯消元照搬,无解判
,多解判主元个数 。
最需要留心的两个细节是天数首尾都算(
图示解析
这张图串起本题从输入到答案的主线:
输入: m 条记录 (开工星期 s, 收工星期 e, k 个型号)
|- 在岗天数 = 型号耗时求和 (同型号出现 c 次就加 c 次)
|- 星期只确定"模 7": d ≡ e - s + 1 (mod 7) -> m 条同余方程 C x ≡ d
`- 7 是素数 -> 高斯消元 (除法 = 乘逆元)
|- 有 0 ≡ c (c != 0) 的行 -> Inconsistent data.
|- 主元个数 < n (存在自由元) -> Multiple solutions.
`- 主元个数 = n -> 回代得 x; 余数 r < 3 时加 7 (即 0/1/2 -> 7/8/9)从上往下看:先是把「一个工人的在岗天数」写成型号耗时的加权和,再意识到记录只给出
模 7 的余数,于是得到同余方程组;因为 7 是素数,消元过程与实数域完全相同;最后只剩三种
出口,分别对应无解、多解和唯一解。最后一步的「余数加 7」正是前面那张天数↔余数对照表给
出的双射,图中每一步都对应正文已经说明的结论,不替代 ### 思路 里的样例推演。