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

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

视频讲解

一、题目描述

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

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

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

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

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

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

第一行:一个数 N

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

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

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

每组数据以 0 0 结束。

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

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

数据范围
1K10,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 从等号左边转到等号右边来了.

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

三、题目解析

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

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

我们使用 x1x2x3 分别表示 是不是操作这个开关,没操作是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个全开。
至此,示例数据理解了。按数学的形式写一下就是
x1=1,x2=0,x3=0
x1=0,x2=1,x3=0
x1=0,x2=0,x3=1
x1=1,x2=1,x3=1

按上面这样按下开关,就可以从状态(0,0,0)到达目标状态(1,1,1)

我们只用数学符号xi来描述某个开关是否进行了操作,还不足以描述整个事情,为什么呢?因为在数学公式中,没有体现出谁影响谁这个关键问题!需要进一步的进行抽象整理:

aij表示当j开关按下时,相应的第i个开关也要发生变化。

举个栗子
对于开关1,假设初始状态为s,目标状态是t,那么它会如何变化到t的呢?
肯定是操作了自己,或者是,操作了那些能影响它的开关~!

ai,j: j按下,会影响开关i

它自己如何表示呢? 就是a11嘛,而且a11肯定是1,因为根据异或的性质,只有是1才能保证按下后出现相反状态!
影响的开关怎么表示呢?就是a1j=1啊!这样才会表示j按下,影响开关1。而a1j=0就是表示j不会影响1

{a11=1sa11x1a12x2...a1nxn=t

xi: i号开关是否按下

这个玩意怎么理解呢?

  • 比如x1=1,因为a11=1,所以当1号开关按下时,它的状态将会变化,可能是由01,也可能是由10
  • 比如xn=1,表示第n个开关操作了,但是,由于a1n=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;
}
posted @   糖豆爸爸  阅读(89)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2018-06-14 StackExchange.Redis的使用
2016-06-14 修正MYSQL错误数据的一个存储过程
Live2D
点击右上角即可分享
微信分享提示