|
|
|
|
##[$AcWing$ $208$. 开关问题(高斯消元)](https://www.acwing.com/problem/content/description/210/)
|
|
|
|
|
|
|
|
|
|
[视频讲解](https://www.bilibili.com/video/av679114156)
|
|
|
|
|
|
|
|
|
|
### 一、题目描述
|
|
|
|
|
有 $N$ 个相同的开关,每个开关都与某些开关有着联系,每当你打开或者关闭某个开关的时候,其他的与此开关相关联的开关也会相应地发生变化,即这些相联系的开关的状态如果原来为开就变为关,如果为关就变为开。
|
|
|
|
|
|
|
|
|
|
你的目标是经过若干次开关操作后使得最后 $N$ 个开关达到一个特定的状态。
|
|
|
|
|
|
|
|
|
|
对于任意一个开关,**最多只能进行一次开关操作**。
|
|
|
|
|
|
|
|
|
|
你的任务是,计算有多少种可以达到指定状态的方法。(不计开关操作的顺序)
|
|
|
|
|
|
|
|
|
|
**输入格式**
|
|
|
|
|
输入第一行有一个数 $K$,表示以下有 $K$ 组测试数据。
|
|
|
|
|
|
|
|
|
|
每组测试数据的格式如下:
|
|
|
|
|
|
|
|
|
|
第一行:一个数 $N$。
|
|
|
|
|
|
|
|
|
|
第二行:$N$ 个 $0$ 或者 $1$ 的数,表示开始时 $N$ 个开关状态。
|
|
|
|
|
|
|
|
|
|
第三行:$N$ 个 $0$ 或者 $1$ 的数,表示操作结束后 $N$ 个开关的状态。
|
|
|
|
|
|
|
|
|
|
接下来每行两个数 $I,J$,表示如果操作第 $I$ 个开关,第 $J$ 个开关的状态也会变化。
|
|
|
|
|
|
|
|
|
|
每组数据以 $0$ $0$ 结束。
|
|
|
|
|
|
|
|
|
|
**输出格式**
|
|
|
|
|
每组数据输出占一行。
|
|
|
|
|
|
|
|
|
|
如果有可行方法,输出总数,否则输出 `Oh,it's impossible~!!` 。
|
|
|
|
|
|
|
|
|
|
**数据范围**
|
|
|
|
|
$1≤K≤10,0<N<29$
|
|
|
|
|
|
|
|
|
|
**输入样例:**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
2
|
|
|
|
|
3
|
|
|
|
|
0 0 0
|
|
|
|
|
1 1 1
|
|
|
|
|
1 2
|
|
|
|
|
1 3
|
|
|
|
|
2 1
|
|
|
|
|
2 3
|
|
|
|
|
3 1
|
|
|
|
|
3 2
|
|
|
|
|
0 0
|
|
|
|
|
3
|
|
|
|
|
0 0 0
|
|
|
|
|
1 0 1
|
|
|
|
|
1 2
|
|
|
|
|
2 1
|
|
|
|
|
0 0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**输出样例:**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
4
|
|
|
|
|
Oh,it's impossible~!!
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 二、异或知识
|
|
|
|
|
|
|
|
|
|
**灯**,有一个明显的特征,原来是亮的,按一下就灭了;原来是灭的,按一下就亮了。
|
|
|
|
|
这明显就是一个 **异或** 操作,比如 `0 ^ 1 = 1`,`1 ^ 1 = 0`,也就是,不管原来是啥样,直接异或一个$1$就可以达到 **目标状态**。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* 一个值与自身的异或 总是为 $0$
|
|
|
|
|
```c++
|
|
|
|
|
x ^ x = 0
|
|
|
|
|
```
|
|
|
|
|
* 一个值与 $0$ 异或 等于本身
|
|
|
|
|
```c++
|
|
|
|
|
x ^ 0 = x
|
|
|
|
|
```
|
|
|
|
|
* 可交换性
|
|
|
|
|
```c++
|
|
|
|
|
a ^ b = b ^ a
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
根据以上的四个特点
|
|
|
|
|
我们可以推导:
|
|
|
|
|
```c++
|
|
|
|
|
a ^ b = c
|
|
|
|
|
等式两边都增加对b的异或, 等价于
|
|
|
|
|
a ^ b ^ b = c ^ b
|
|
|
|
|
等式左边的 b^b=0, a^0=a, 所以有
|
|
|
|
|
a = c ^ b
|
|
|
|
|
最终相当于把 b 从等号左边转到等号右边来了.
|
|
|
|
|
```
|
|
|
|
|
上面的推论,一会在下面的解题中将会用到。
|
|
|
|
|
|
|
|
|
|
### 三、题目解析
|
|
|
|
|
|
|
|
|
|
**[前导知识:高斯消元求解异或方程组](https://www.cnblogs.com/littlehb/p/15386303.html)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
对于任意一个开关,至多可以进行一次操作!**这个非常重要,否则不断的关停,方案数就没头了**~
|
|
|
|
|
|
|
|
|
|
我们使用 $x_1、x_2、x_3$ 分别表示 **是不是操作这个开关**,没操作是$0$,操作了就是$1$。
|
|
|
|
|
|
|
|
|
|
以题目给出的例子来理解一下:
|
|
|
|
|
|
|
|
|
|
初始状态$0,0,0$,目标状态$1,1,1$。
|
|
|
|
|
|
|
|
|
|
* 打开一个开关
|
|
|
|
|
* 假设我们操作第一个开关,题目中说了,操作$1$的话,开关$1,2,3$都会变化。初始状态都是$0$,开一下开关$1$,则$1,2,3$全亮了,达到目标状态,看来只开开关$1$就是一种方案。
|
|
|
|
|
|
|
|
|
|
* 假设我们操作第二个开关,题目中说了,操作$2$的话,开关$1,2,3$都会变化。初始状态都是$0$,开一下开关$2$,则$1,2,3$全亮了,达到目标状态,看来只开开关$2$就是一种方案。
|
|
|
|
|
|
|
|
|
|
* 假设我们操作第三个开关,题目中说了,操作$3$的话,开关$1,2,3$都会变化。初始状态都是$0$,开一下开关$3$,则$1,2,3$全亮了,达到目标状态,看来只开开关$2$就是一种方案。
|
|
|
|
|
|
|
|
|
|
* 打开两个开关
|
|
|
|
|
* 如果我们选择两个打开,比如$1,2$,那么$1$打开所有的灯,结果被$2$全关了,不是目标状态。我们选择其它任意两个,都是一样的效果,结论就是选择两个打开是不行的。
|
|
|
|
|
|
|
|
|
|
* 打开三个开关
|
|
|
|
|
$1$打开,全亮;$2$打开,全灭;$3$打开,全亮。$OK$!
|
|
|
|
|
|
|
|
|
|
所以,结论就是有四种情况,分别是:只开$1$,只开$2$,只开$3$,$3$个全开。
|
|
|
|
|
至此,示例数据理解了。按数学的形式写一下就是
|
|
|
|
|
$x_1=1,x_2=0,x_3=0$
|
|
|
|
|
$x_1=0,x_2=1,x_3=0$
|
|
|
|
|
$x_1=0,x_2=0,x_3=1$
|
|
|
|
|
$x_1=1,x_2=1,x_3=1$
|
|
|
|
|
|
|
|
|
|
按上面这样按下开关,就可以从状态$(0,0,0)$到达目标状态$(1,1,1)$。
|
|
|
|
|
|
|
|
|
|
我们只用数学符号$x_i$来描述某个开关是否进行了操作,还不足以描述整个事情,为什么呢?因为在数学公式中,**没有体现出谁影响谁这个关键问题**!需要进一步的进行抽象整理:
|
|
|
|
|
|
|
|
|
|
> 设$a_{ij}$表示当$j$开关按下时,相应的第$i$个开关也要发生变化。
|
|
|
|
|
|
|
|
|
|
**举个栗子**
|
|
|
|
|
对于开关$1$,假设初始状态为$s$,目标状态是$t$,那么它会如何变化到$t$的呢?
|
|
|
|
|
肯定是操作了自己,或者是,操作了那些能影响它的开关~!
|
|
|
|
|
|
|
|
|
|
#### ① $a_{i,j}$: $j$按下,会影响开关$i$
|
|
|
|
|
它自己如何表示呢? 就是$a_{11}$嘛,而且$a_{11}$肯定是$1$,因为根据异或的性质,只有是$1$才能保证按下后出现相反状态!
|
|
|
|
|
影响的开关怎么表示呢?就是$a_{1j}=1$啊!这样才会表示$j$按下,影响开关$1$。而$a_{1j}=0$就是表示$j$不会影响$1$。
|
|
|
|
|
|
|
|
|
|
$$
|
|
|
|
|
\large \left\{\begin{matrix}
|
|
|
|
|
a_{11}=1 \\
|
|
|
|
|
s \bigoplus a_{11} \cdot x_1 \bigoplus a_{12} \cdot x_2...\bigoplus a_{1n}\cdot x_n =t
|
|
|
|
|
\end{matrix}\right.
|
|
|
|
|
$$
|
|
|
|
|
|
|
|
|
|
#### ② $x_i$: $i$号开关是否按下
|
|
|
|
|
这个玩意怎么理解呢?
|
|
|
|
|
* 比如$x_1=1$,因为$a_{11}=1$,所以当$1$号开关按下时,它的状态将会变化,可能是由$0$到$1$,也可能是由$1$到$0$。
|
|
|
|
|
* 比如$x_n=1$,表示第$n$个开关操作了,但是,由于$a_{1n}=0$,也就是根据题意知道,操作$n$号开关,$1$号开关不会变化,那么$n$的变化,不会影响$1$后开关最后的状态。
|
|
|
|
|
|
|
|
|
|
进一下抽象,把这样$n$个开关的状态变化关系列出来,就是一个异或方程组,剩下的就是高斯消元求解异或方程组了。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这里面还需要一个小的变化:
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
x^y^z=t => y^z=t^x
|
|
|
|
|
```
|
|
|
|
|
这样就可以把$x$移动到右侧去。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### $Code$
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
using namespace std;
|
|
|
|
|
const int N = 110;
|
|
|
|
|
int n;
|
|
|
|
|
int a[N][N];
|
|
|
|
|
|
|
|
|
|
int gauss() {
|
|
|
|
|
int r = 1;
|
|
|
|
|
for (int c = 1; c <= n; c++) {
|
|
|
|
|
int t = r;
|
|
|
|
|
for (int i = r + 1; i <= n; i++)
|
|
|
|
|
if (a[i][c]) t = i;
|
|
|
|
|
if (!a[t][c]) continue;
|
|
|
|
|
swap(a[t], a[r]);
|
|
|
|
|
for (int i = r + 1; i <= n; i++)
|
|
|
|
|
for (int j = n + 1; j >= c; j--)
|
|
|
|
|
if (a[i][c]) a[i][j] ^= a[r][j];
|
|
|
|
|
r++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int res = 1;
|
|
|
|
|
// 此时已经到了全零行
|
|
|
|
|
if (r < n + 1) {
|
|
|
|
|
for (int i = r; i <= n; i++) {
|
|
|
|
|
// 全零行的右边出现非零 无解
|
|
|
|
|
if (a[i][n + 1]) return -1; // 出现了 0 == !0,无解
|
|
|
|
|
// 如果出现了0=0这样的情况,可能是 0x1+0x2+0x3 这样的情况,
|
|
|
|
|
// 此时,不管x1,x3取什么值(0,1),都与结果无关,所以自由元数量的2次方就是答案
|
|
|
|
|
// 比如x1=0,x1=1-->2个答案
|
|
|
|
|
// 比如x2=0,x2=1-->2个答案
|
|
|
|
|
// 比如x3=0,x3=1....
|
|
|
|
|
// 同时这些 x1,x2,x3的取值是可以随意取的,每个有2种取法,是一个典型的乘法原理,即2*2*2*...,数量就是自由元的数量
|
|
|
|
|
// 现在就是循环中,所以,可以利用循环,每次乘2就完成了2次方的计算
|
|
|
|
|
res <<= 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
int T;
|
|
|
|
|
cin >> T; // T组测试数据
|
|
|
|
|
while (T--) {
|
|
|
|
|
memset(a, 0, sizeof a); // 多组测试数据,不清空OI一场空
|
|
|
|
|
cin >> n; // 开关个数
|
|
|
|
|
for (int i = 1; i <= n; i++) cin >> a[i][n + 1]; // 初始状态
|
|
|
|
|
for (int i = 1; i <= n; i++) { // 终止状态
|
|
|
|
|
int t; // 第i个开关的终止状态
|
|
|
|
|
cin >> t;
|
|
|
|
|
// s1: 1号开关的初始状态 t1:1号开关的结束状态
|
|
|
|
|
// x1 x2 x3 ... xn 1~n个开关是否按下,0:不按下,1:按下
|
|
|
|
|
// a13:3号开关影响1号开关状态, a1n:n号开关影响1号开关状态.
|
|
|
|
|
// 推导的方程
|
|
|
|
|
// 含义:从初始状态 s1开始出发,最终到达t1这个状态。
|
|
|
|
|
// 有些开关是可以影响1号开关的最终状态,有些变化了也不影响。我们把开关之间的关联关系设为a_ij,描述j开关变化,可以影响到i开关
|
|
|
|
|
// 如果 a_ij=0,表示j开关不会影响i开关,不管x_j=1,还是x_j=0都无法影响i开关的状态。
|
|
|
|
|
|
|
|
|
|
// s1^ a11*x1 ^ a12*x2 ^ a13*x3 ^ ... ^a1n*xn=t1
|
|
|
|
|
// <=>
|
|
|
|
|
// s1^ s1 ^ a11*x1 ^ a12*x2 ^ a13*x3 ^ ... ^a1n*xn= t1 ^ s1
|
|
|
|
|
// <=>
|
|
|
|
|
// a11*x1 ^ a12*x2 ^ a13*x3 ^ ... ^a1n*xn= t1 ^ s1
|
|
|
|
|
|
|
|
|
|
// 这里初始化时 a[1][n+1]就是s1,下面这行的意思就是 t1 ^ s1
|
|
|
|
|
a[i][n + 1] ^= t; // 在维护增广矩阵的最后一列数值
|
|
|
|
|
a[i][i] = 1; // 第i个开关一定会改变第i个灯,形成一个三角?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int x, y;
|
|
|
|
|
while (cin >> x >> y, x && y) a[y][x] = 1; // 操作开关x,x影响y。生成左侧方程系数。给定的是1,未说明的是0
|
|
|
|
|
// 这个矩阵系数,第一维的是行,第二维的是列
|
|
|
|
|
// 上面的输入,其实是反的,比如它说,3影响1,其实真正的含义是a_13=1
|
|
|
|
|
|
|
|
|
|
// 系数矩阵准备完毕,可以用高斯消元求解方程了
|
|
|
|
|
int t = gauss();
|
|
|
|
|
if (t == -1)
|
|
|
|
|
puts("Oh,it's impossible~!!");
|
|
|
|
|
else
|
|
|
|
|
printf("%d\n", t);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|