把「每个开关最多按一次」的操作集合写成 GF(2) 线性方程组,用整数按位压缩整行做高斯消元:解数就是 2 的自由元个数次方,出现 0=1 则无解。
OJ: roj
题目 ID: 3091
难度:提高+/省选-
标签:高斯消元线性代数位运算数学python
创建: 2026-10-01 16:26
更新: 2026-10-07 13:50
形式化题目
有 I J:
操作第 I,操作 I
一定翻转 I 自己。每个开关最多被操作一次。
求能达成目标状态的操作集合个数(不计操作顺序)。
记
约束:
一个必须先说清的读题点:
I J是有向的。 题面「其他的与此开关相关联的开关也会相应地发生变化」描述的是操作的效果, 而数据里的 I J是逐对列出的:给1 2不给2 1时,操作 2 不会影响 1。 真实数据switch 1-6的第 4 组(,答案为 Oh,it's impossible~!!) 只有1 10 / 1 2 / 1 3 / 1 4 / 2 5 / 2 1六条边:把它当无向图会算出,与标准答案不符。 本文与代码一律按有向处理。
正解
思路
朴素做法:枚举 个操作集合
每个开关只有「按 / 不按」两种选择,于是最直接的做法是枚举全部
这个做法绝对正确:同一个集合内的操作先后顺序不影响结果,枚举也既不重复也不遗漏。
但它的瓶颈极其明确——搜索空间是
换一个视角:不要枚举解,只要数解
题目只问「有多少种方法」,不问具体是哪些方法。所以只要能刻画解集的结构,就可以直接数出个数。
关键观察是:顺序无关 + 每个开关最多一次 + 翻转两次抵消,三者合起来说明第
一共有
有向边怎么进方程:方程按「位置」编号
这一步是本题最容易写反的地方。因为 I J 是有向的,方程要按输出位置
- 读到
i j:说明「操作会翻转 」,于是变量 出现在第 行; - 每个开关一定翻转自己,所以每行补上自环:变量
也出现在第 行; - 第
行的右端常数就是 。
位压缩:一行一个整数
^=。
常数项必须放在第 row == 1 << n。
用样例核对
样例 1:
| 行 | 常数 |
|||
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1 |
| 2 | 1 | 1 | 1 | 1 |
| 3 | 1 | 1 | 1 | 1 |
消元后只剩一行 1 1 1 | 1,即
样例 2:1 2 与 2 1:
| 行 | 常数 |
含义 | |||
|---|---|---|---|---|---|
| 1 | 1 | 1 | 0 | 1 | 第 1 位需要翻转 |
| 2 | 1 | 1 | 0 | 0 | 第 2 位不需要翻转 |
| 3 | 0 | 0 | 1 | 1 | 第 3 位需要翻转 |
第 1、2 行系数相同而常数不同,异或后得到 0 0 0 | 1,即 Oh,it's impossible~!!。直观上也确实做不到:第 1 位和第 2 位只能被
代码
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 16:27
# update_at: 2026-10-01 16:33
import sys
from collections.abc import Iterator
IMPOSSIBLE = "Oh,it's impossible~!!" # 无解时的固定输出(题面原文,含两个感叹号)
def build_equations(n: int, start: list[int], goal: list[int], data: Iterator[int]) -> list[int]:
"""把本组数据变成 n 个方程:第 j 行的低 n 位记录「哪些开关会改变第 j 位」。
第 j 位的最终状态只由「会带动 j 的那些开关」以及 j 自己决定,所以每个方程
对应一个输出位置 j,而不是一个开关。
"""
rows = [0] * n
i, j = next(data), next(data) # 一对 I J;读到 0 0 表示本组结束
while j:
rows[j - 1] ^= 1 << (i - 1) # 变量 i 出现在第 j 行:按 i 会带动 j
i, j = next(data), next(data)
for j in range(n):
rows[j] |= 1 << j # 主对角元:按 j 自己必定改变第 j 位
# 第 j 位要求翻转恰好 (start[j] ^ goal[j]) 次,把它作为常数项放到第 n 位。
return [row ^ (start[j] ^ goal[j]) << n for j, row in enumerate(rows)]
def free_variables(rows: list[int], n: int) -> int:
"""把每一行看成「变量系数写在低 n 位、右端常数写在第 n 位」的整数,返回自由元个数。
返回 -1 表示出现 0 = 1 的矛盾行(无解)。消元时用「取最低位的行」当主元行,
这样第 j 列的主元恰好是第 j 位。
"""
rows = [row for row in rows if row] # 全零行不含任何信息,直接丢掉
rank = 0
for col in range(n):
pivot = -1
for k in range(rank, len(rows)):
if rows[k] >> col & 1:
pivot = k
break
if pivot < 0:
continue # 这一列没有主元,该变量自由
rows[rank], rows[pivot] = rows[pivot], rows[rank]
pivot_row = rows[rank]
for k in range(len(rows)):
if k != rank and rows[k] >> col & 1:
rows[k] ^= pivot_row # 整行异或,右端常数跟着一起变
rank += 1
if any(row == 1 << n for row in rows): # 只剩常数位的 0 = 1
return -1
return n - rank
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
cases = next(data) # 题面的 K
for _ in range(cases):
n = next(data)
start = [next(data) for _ in range(n)]
goal = [next(data) for _ in range(n)]
free = free_variables(build_equations(n, start, goal, data), n)
out.append(IMPOSSIBLE if free < 0 else str(1 << free))
print('\n'.join(out))
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:34
* update_at: 2026-10-06 18:34
*/
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 30; // N < 29
const char IMPOSSIBLE[] = "Oh,it's impossible~!!";
int n; // 开关数量
ll row[MAXN]; // 第 j 行:低 n 位为系数,第 n 位为右端常数
// 读入一组数据并构建 n 个 GF(2) 方程
void build_equations() {
static int st[MAXN], ed[MAXN];
for (int i = 0; i < n; ++i) scanf("%d", &st[i]);
for (int i = 0; i < n; ++i) scanf("%d", &ed[i]);
for (int j = 0; j < n; ++j) row[j] = 0;
int i, j;
while (scanf("%d%d", &i, &j) == 2 && (i || j)) {
// 操作第 i 个开关会翻转第 j 个开关:变量 x_i 进入第 j 行
row[j - 1] ^= 1LL << (i - 1);
}
// 自环:操作第 j 个开关必定翻转第 j 个开关
for (int j = 0; j < n; ++j) row[j] |= 1LL << j;
// 右端常数:第 j 位是否需要翻转
for (int j = 0; j < n; ++j) row[j] ^= (ll)(st[j] ^ ed[j]) << n;
}
// 高斯消元求自由元个数;返回 -1 表示出现 0 = 1 的矛盾行
int free_variables() {
int rank = 0;
for (int col = 0; col < n; ++col) {
int pivot = -1;
for (int k = rank; k < n; ++k) {
if (row[k] >> col & 1LL) {
pivot = k;
break;
}
}
if (pivot < 0) continue; // 无主元,该列自由
swap(row[rank], row[pivot]);
for (int k = 0; k < n; ++k) {
if (k != rank && (row[k] >> col & 1LL)) row[k] ^= row[rank];
}
++rank;
}
// 检查只剩常数位的矛盾行 0 = 1
for (int k = 0; k < n; ++k) {
if (row[k] == (1LL << n)) return -1;
}
return n - rank;
}
int main() {
int K;
scanf("%d", &K);
while (K--) {
scanf("%d", &n);
build_equations();
int free_cnt = free_variables();
if (free_cnt < 0) {
printf("%s\n", IMPOSSIBLE);
} else {
printf("%lld\n", 1LL << free_cnt);
}
}
return 0;
}复杂度
设单组
实测:真实数据 0.017 s / 峰值 14.7 MB;构造的最坏情形(
总结
本题的全部难度在建模,不在写代码:
- 「每个开关最多按一次」+「顺序无关」⇒ 一个方法就是一个
向量 ; - 第
位的最终状态只由一组 的异或和决定 ⇒ 条 GF(2) 线性方程; - 解集的规模只由秩决定 ⇒ 答案
,出现 即无解。
有了这三步,「枚举 I J 是有向的:方程按位置编号,变量
图示解析
这张图串起本题从读题到得到答案的主线:
输入:初始/目标状态 + 有向边 I J
|- 每个开关最多按一次, 顺序无关
| `- 一个方法 = 0/1 向量 x
`- 第 j 位最终状态 = 影响 j 的 x_i 的异或和
`- N 条 GF(2) 方程: 行 j 的系数 = {i : i 影响 j} + 自环 j
`- 整行压成一个整数 (低 N 位系数, 第 N 位常数)
`- 高斯消元求秩 r, 并检查是否出现 0 = 1
|- 有 0 = 1 -> Oh,it's impossible~!!
`- 无矛盾 -> 答案 = 2^(N - r)从上往下看这条链:先由读题得到「一个方法是一个 ### 思路 里的样例推演。