You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.7 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##AcWing 208. 开关问题(高斯消元)

视频讲解

一、题目描述

N 个相同的开关,每个开关都与某些开关有着联系,每当你打开或者关闭某个开关的时候,其他的与此开关相关联的开关也会相应地发生变化,即这些相联系的开关的状态如果原来为开就变为关,如果为关就变为开。

你的目标是经过若干次开关操作后使得最后 N 个开关达到一个特定的状态。

对于任意一个开关,最多只能进行一次开关操作

你的任务是,计算有多少种可以达到指定状态的方法。(不计开关操作的顺序)

输入格式 输入第一行有一个数 K,表示以下有 K 组测试数据。

每组测试数据的格式如下:

第一行:一个数 N

第二行:N0 或者 1 的数,表示开始时 N 个开关状态。

第三行:N0 或者 1 的数,表示操作结束后 N 个开关的状态。

接下来每行两个数 I,J,表示如果操作第 I 个开关,第 J 个开关的状态也会变化。

每组数据以 0 0 结束。

输出格式 每组数据输出占一行。

如果有可行方法,输出总数,否则输出 Oh,it's impossible~!!

数据范围 1≤K≤10,0<N<29

输入样例:

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

输出样例:

4
Oh,it's impossible~!!

二、异或知识

,有一个明显的特征,原来是亮的,按一下就灭了;原来是灭的,按一下就亮了。 这明显就是一个 异或 操作,比如 0 ^ 1 = 1,1 ^ 1 = 0,也就是,不管原来是啥样,直接异或一个1就可以达到 目标状态

  • 一个值与自身的异或 总是为 0
x ^ x = 0
  • 一个值与 0 异或 等于本身
x ^ 0 = x
  • 可交换性
a ^ b = b ^ a

根据以上的四个特点 我们可以推导:

a ^ b = c
等式两边都增加对b的异或, 等价于
a ^ b ^ b = c ^ b
等式左边的 b^b=0, a^0=a, 所以有
a = c ^ b
最终相当于把 b 从等号左边转到等号右边来了.

上面的推论,一会在下面的解题中将会用到。

三、题目解析

前导知识:高斯消元求解异或方程组

对于任意一个开关,至多可以进行一次操作!这个非常重要,否则不断的关停,方案数就没头了~

我们使用 x_1、x_2、x_3 分别表示 是不是操作这个开关,没操作是0,操作了就是1

以题目给出的例子来理解一下:

初始状态0,0,0,目标状态1,1,1

  • 打开一个开关

    • 假设我们操作第一个开关,题目中说了,操作1的话,开关123都会变化。初始状态都是0,开一下开关1,则1,2,3全亮了,达到目标状态,看来只开开关1就是一种方案。

    • 假设我们操作第二个开关,题目中说了,操作2的话,开关123都会变化。初始状态都是0,开一下开关2,则1,2,3全亮了,达到目标状态,看来只开开关2就是一种方案。

    • 假设我们操作第三个开关,题目中说了,操作3的话,开关123都会变化。初始状态都是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号开关按下时,它的状态将会变化,可能是由01,也可能是由10
  • 比如x_n=1,表示第n个开关操作了,但是,由于a_{1n}=0,也就是根据题意知道,操作n号开关,1号开关不会变化,那么n的变化,不会影响1后开关最后的状态。

进一下抽象,把这样n个开关的状态变化关系列出来,就是一个异或方程组,剩下的就是高斯消元求解异或方程组了。

这里面还需要一个小的变化:

x^y^z=t => y^z=t^x

这样就可以把x移动到右侧去。

Code

#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;
}