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


    题目传送门

    一、异或知识整理

    ,有一个明显的特征,原来是亮的,按一下就灭了;原来是灭的,按一下就亮了。
    这明显就是一个异或操作,比如 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\)的话,开关\(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_{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_1=1\),因为\(a_{11}=1\),所以当\(1\)号开关按下时,它的状态将会变化,可能是由\(0\)\(1\),也可能是由\(1\)\(0\)
    • 比如\(x_n=1\),表示第\(n\)个开关操作了,但是,由于\(a_{1n}=0\),也就是根据题意知道,操作\(n\)号开关,\(1\)号开关不会变化,那么\(n\)的变化,不会影响\(1\)后开关最后的状态。

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

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

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

    这样就可以把\(s\)移动到右侧去。

    因为是异或运算,所以我们可以用二进制来压缩掉一维,优化运行速度。

    比如有一个整数\(int\),那么它有

    \(n...,3 \ 2 \ 1 \ 0\)
    \(a_{1n}...,a_{13} \ a_{12} \ a_{11} \ b\)

    三、实现代码

    #include <bits/stdc++.h>
    using namespace std;
    
    const int N = 35;
    
    int n;
    int a[N][N];
    
    int gauss() {
        int r, c;
        for (r = 1, 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;
            if (r != t) 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,无解
                res *= 2;
            }
        }
        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;
    }
    

    https://www.bilibili.com/video/av679114156?vd_source=13b33731bb79a73783e9f2c0e11857ae

  • 相关阅读:
    Leetcode 141. 环形链表
    Leetcode 53. 最大子数组和
    Golang的常用常量
    Leetcode 21. 合并两个有序链表
    Leetcode 206. 反转链表
    Leetcode 70. 爬楼梯
    Leetcode 20. 有效的括号
    Leetcode 13. 罗马数字转整数
    Leetcode 234. 回文链表
    Linux – Memory Management insights
  • 原文地址:https://www.cnblogs.com/littlehb/p/16373749.html
Copyright © 2020-2023  润新知