• AcWing 95. 费解的开关


    题目传送门

    一、题目描述

    \(25\)盏灯排成一个\(5x5\)的方形。每一个灯都有一个开关,游戏者可以改变它的状态。每一步,游戏者可以改变某一个灯的状态。游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。

    我们用数字“\(1\)”表示一盏开着的灯,用数字“\(0\)”表示关着的灯。

    二、题目分析

    • 每个灯最多只能被按一次,按下多次其实是没用的,偶数次不如不按,奇数次和一次的效果是一样的。
    • 按的次序是无所谓的,先按哪个再按哪个都是一样的。

    三、\(bfs\)解法

    \(bfs\)可以解决这道题 时间复杂度:\(2^{25}\)

    #include <bits/stdc++.h>
    using namespace std;
    
    // bfs的思路采用的是逆向思维法
    // 从终止状态倒推6步,先做一遍预处理。看看从终止状态可以从哪些状态在6步之内到达。
    // 将状态用hash方法保存下来,2^{25},最多是3000W左右个状态,但合法状态不是很多。
    
    //当前状态,当前到最终状态所需步数
    unordered_map<int, int> vis;
    
    //改变这个灯及其上下左右相邻的灯的状态
    int turn(int st, int idx) {                // idx下标从0开始
        st ^= (1 << idx);                      //改变第idx个灯
        if (idx % 5) st ^= 1 << idx - 1;       //左,不为最左一个,就将左侧灯改变
        if (idx >= 5) st ^= 1 << idx - 5;      //上,不为第一排;就将上面的灯改变
        if (idx < 20) st ^= 1 << idx + 5;      //下,不为最后一排;就将下面的灯改变
        if ((idx % 5) < 4) st ^= 1 << idx + 1; //右,不为右一个,就将最右面的灯改变
        return st;
    }
    
    //从最终状态逆序遍历,遍历所有的状态,所以不用管地图什么样,直接bfs完,查对应map就完事了
    void bfs() {
        // 0-2^25-1(25个1),共2^25种状态
        int st = (1 << 25) - 1; //左移 右移的优先级是最低的,比加减还要低。所以这里的括号是必需的
        queue<int> q;
        q.push(st);
        vis[st] = 0;
    
        while (q.size()) {
            auto t = q.front();
            q.pop();
            if (vis[t] == 6) break;        //判断6步以内使所有的灯都变亮
            for (int i = 0; i < 25; i++) { //尝试当前状态的每盏灯
                st = turn(t, i);           //尝试改变每盏灯的状态=>新的状态
                if (!vis.count(st)) {      //该状态未被遍历过
                    vis[st] = vis[t] + 1;
                    q.push(st);
                }
            }
        }
    }
    int main() {
        bfs();
        int T;
        cin >> T;
        while (T--) {
            int g = 0; // g是地图的含义
            for (int i = 0; i < 25; i++) {
                char ch;
                cin >> ch;
                g += ((ch - '0') << i); // 25个字符二进制压缩成数字
            }
            if (vis.count(g) == 0)
                cout << -1 << endl;
            else
                cout << vis[g] << endl;
        }
        return 0;
    }
    

    四、递推解法

    从上往下递推,还有一道题叫扫雷,有时间可以做一下。

    • 二进制枚举+递推可以解决这道题 \(2^5\times 25 \times 5 \times 500\)
    #include <bits/stdc++.h>
    using namespace std;
    const int INF = 0x3f3f3f3f;
    const int N = 6;
    
    // char 数组版本
    char g[N][N], bg[N][N]; //工作的临时数组 和 原始状态数组
    
    //上右下左中
    int dx[] = {-1, 0, 1, 0, 0};
    int dy[] = {0, 1, 0, -1, 0};
    
    // 按一下第x行第y列的开关
    void turn(int x, int y) {
        for (int i = 0; i < 5; i++) {
            int a = x + dx[i], b = y + dy[i];
            if (a < 0 || a >= 5 || b < 0 || b >= 5) continue;
            g[a][b] ^= 1; //'0':48 '1':49
            // (48)_{10}=(110000)_{2} (49)_{10}=(110001)_{2}
            // 由于48的最后一位是0,而49最后一位是1,所以,异或就可以实现两者之间的切换,真是太神奇了~
        }
    }
    
    int main() {
        int T;
        cin >> T;
        while (T--) {
            //按一行的字符串读取
            for (int i = 0; i < 5; i++) cin >> bg[i]; //原来的字符状态数组
    
            //预求最小,先设最大
            int res = INF;
    
            //因为这一题里面的op代表着对第一行的操作,1表示按下,0表示不按下,并不是描述灯的状态
            //枚举对第1行的所有操作,注意:是操作,不是状态噢~
            for (int op = 0; op < (2 << 5); op++) { //从0 到 2^5-1,共 2^5=32个二进制状态模拟,就可以描述出第一行灯的点亮与否的所有可能
                int cnt = 0;
                memcpy(g, bg, sizeof g); //将原始状态复制出一份放入g数组,准备变形
    
                // 操作第一行的开关
                for (int i = 0; i < 5; i++)
                    if (op >> i & 1) { //如果此种状态op模拟情况下,第i位的灯是亮的
                        turn(0, i);    //将第1行,第i列的状态改变
                        cnt++;         //用去了一次机会
                    }
    
                // 递推出第1~4行开关的状态
                // 第0行推第1行,第1行推第2行,...
                for (int i = 0; i < 4; i++)
                    for (int j = 0; j < 5; j++)
                        if (g[i][j] == '0') {
                            turn(i + 1, j);
                            cnt++; //又用去一次机会
                        }
    
                // 检查最后一行灯是否全亮
                bool success = true;
                for (int i = 0; i < 5; i++)
                    if (g[4][i] == '0') success = false;
    
                if (success) res = min(res, cnt);
            }
            //题意要求,大于6次,算失败
            if (res > 6) res = -1;
            printf("%d\n", res);
        }
        return 0;
    }
    

    五、高斯消元解法

    • 高斯消元可以解决这道题 时间复杂度:\(n^6\),\(n\)是指边长。

    思路:为了练习高斯消元找来的题目,当然是用高斯消元了……

    解:既然要解方程,那么首先我们要清楚我们要求的是什么(虽然看起来像废话,但是我一开始真的想了半天

    显然,我们需要求每个开关是否打开。对于开关来说,只有不开两种情况,可以用\(0\) \(1\)表示

    输入有\(n\)个开关(本题中\(n=5\times 5=25\)),所以我们需要有\(n=25\)个方程。然后我们需要建立方程

    怎么把开关与灯的状态联系起来?

    很明显,这是一个 异或 的方程组~,为啥呢?原来是亮的,再点一下就灭了;原来是灭的,再点一下就亮了;这不是异或是啥?是啥?没做过高斯消元解异或方程组吗?

    对于灯\(1\)来说,一共有\(n=25\)个开关,我们可以把能改变灯\(1\)状态的开关设成\(1\),不能改变的设成\(0\),来描述不同的开关对于灯\(1\)的影响。

    显然这里要一个\(n*n\)二维数组,代码中用变量\(mat\)来记录。

    对于方程的结果,可以根据灯\(1\)的开始状态和结束状态设置,相同为\(0\),不同为\(1\)

    那么我们可以得到以下方程: \(use[1]\)表示开关\(1\),简写成\(u[1]\),\(relate[1][2]\)表示开关\(1\)对灯\(2\)的影响 简写成\(r[1][2]\)

    \(u[1]*r[1][1]\oplus u[2]*r[2][1]\oplus u[3]*r[3][1]\oplus u[4]*r[4][1]……\oplus u[n]*r[n][1] = start[1]\oplus end[1]\) //start,end 表示初始状态,结尾状态

    列出\(n\)个以后再把要求的\(u[1]\)\(u[n]\)提出来,就可以得到矩阵

    \[\large \begin{bmatrix} r[1][1] & r[2][1] & r[3][1] & r[4][1] & …… & r[n][1] & start[1]\oplus end[1] \\ r[1][2] & r[2][2] & r[3][2] & r[4][2] & …… & r[n][2] & start[2]\oplus end[2] \\ r[1][3] & r[2][3] & r[3][3] & r[4][3] & …… & r[n][3] & start[3]\oplus end[3] \\ ... \\ r[1][n] & r[2][n] & r[3][n] & r[4][n] & …… & r[n][n] & start[n]\oplus end[n] \end{bmatrix} \]

    这时候会发现,\(r[i][j]\),和正常的有点不一样,所以等会输入的时候需要处理一下(行列互换)

    到这里,前期的准备工作算是\(OK\)了,下面开始解方程

    所谓高斯消元法,就和我们平时解方程一样,通过不断地带入消除未知数,拿到一个变量的解以后再带回到其他方程,得到其它变量的解。

    在矩阵里,我们可以把矩阵转化为上三角的形式,然后通过原矩阵与增广矩阵的,来判断有没有解(如果有唯一解的话,可以带回去把解求出来,当然这题不用求)

    关于的关系,这里稍微列一下,因为我的线代其实也忘的差不多……

    #include <bits/stdc++.h>
    using namespace std;
    
    //方向
    int dx[] = {0, 0, 1, -1};
    int dy[] = {1, -1, 0, 0};
    
    //原始状态
    char mp[8][8];
    
    //增广矩阵 25*26,25行,26列
    bool mat[30][30];
    
    //返回x在二进制下1的个数
    #define lowbit(x) (x & (-x))
    inline int nbit(int x) {
        int cnt = 0;
        while (x) {
            cnt++;
            x -= lowbit(x);
        }
        return cnt;
    }
    
    //高斯消元
    int gauss(int row, int col) {
        for (int i = 1; i <= row; ++i) {
            if (!mat[i][i]) {
                int r = i;
                while (++r <= row)
                    if (mat[r][i]) break;
                if (r > row) continue;
                for (int j = i; j <= col; j++) swap(mat[i][j], mat[r][j]);
            }
            for (int r = 1; r <= row; r++) {
                if (r == i || mat[r][i] == 0) continue;
                for (int j = i; j <= col; j++) mat[r][j] ^= mat[i][j];
            }
        }
        //以上就是正常的消元
    
        int num = 0; //自由变量的个数
        for (int i = 1; i <= row; i++) {
            if (mat[i][i] == 0 && mat[i][col] == 1) return -1; //无解
            if (mat[i][i] == 0) num++;                         //+1
        }
    
        int res[30]; //自由变元对应的系数(二进制)
        for (int i = 1; i <= row - num; i++) {
            res[i] = 0;
            for (int j = row - num + 1; j < col; ++j) res[i] = (res[i] << 1) + mat[i][j];
        }
    
        int ans = 7;
        //枚举自由变量的取值
        for (int i = 0; i < (1 << num); i++) {
            int t_ans = nbit(i); //自由变元里操作的次数
            for (int r = 1; r <= row - num; r++) t_ans += (nbit(res[r] & i) + mat[r][col]) & 1;
            //两个二进制数按位与相当于对应二进制位相乘
            ans = min(ans, t_ans);
        }
        return ans > 6 ? -1 : ans;
    }
    
    //将坐标位置,变换为灯的编号,注意位置坐标从1开始
    int id(int i, int j) {
        return (i - 1) * 5 + j;
    }
    
    int main() {
        int T;
        scanf("%d", &T);
        while (T--) {
            //多测,还原增广矩阵初始状态
            memset(mat, 0, sizeof mat);
    
            //读入原始状态
            for (int i = 1; i <= 5; i++) scanf("%s", mp[i] + 1);
    
            //构造增广矩阵
            for (int i = 1; i <= 5; i++) {
                for (int j = 1; j <= 5; j++) {
                    for (int k = 0; k < 4; k++) {
                        int x = i + dx[k], y = j + dy[k];
                        //不出界
                        if (x <= 0 || x >= 6 || y <= 0 || y >= 6) continue;
    
                        mat[id(i, j)][id(x, y)] = 1; //灯(i,j)会改变灯(x,y)的状态
                    }
                    mat[id(i, j)][id(i, j)] = 1;              //灯(i,j)会改变灯(i,j)自己的状态
                    mat[id(i, j)][26] = (mp[i][j] - '0') ^ 1; //结果(右侧结果数据),保存的是char,减'0'还原为数字0或1,再异或1,就变成相反数
                }
            }
    
            //高斯消元,判断是不是有唯一解
            printf("%d\n", gauss(25, 26));
        }
        return 0;
    }
    

    https://blog.csdn.net/qq_38449464/article/details/79888022

    https://www.acwing.com/solution/content/8747/

  • 相关阅读:
    开源软件、自由软件及免费软件的区别 2015-04-13 22:50 45人阅读 评论(0) 收藏
    Linux中fork()函数详解 2015-04-13 22:48 30人阅读 评论(0) 收藏
    ubuntu系统文件夹目录说明 分类: Ubuntu学习笔记 2015-04-13 21:21 49人阅读 评论(0) 收藏
    gdb使用笔记 2015-04-11 19:55 32人阅读 评论(0) 收藏
    Vim 使用笔记 2015-04-10 19:50 51人阅读 评论(0) 收藏
    access_ok()
    linux内核驱动中_IO, _IOR, _IOW, _IOWR 宏的用法与解析(引用)
    编译andriod源码出错:java.lang.UnsupportedClassVersionError: com/google/doclava/Doclava : Unsupported
    Linux内核树的建立-基于ubuntu系统
    sysfs接口整理
  • 原文地址:https://www.cnblogs.com/littlehb/p/16396710.html
Copyright © 2020-2023  润新知