ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

AcWing 95. 费解的开关

2022-06-21 14:33:04  阅读:166  来源: 互联网

标签:状态 25 int 费解 ++ 开关 95 高斯消 AcWing


题目传送门

一、题目描述

\(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/

标签:状态,25,int,费解,++,开关,95,高斯消,AcWing
来源: https://www.cnblogs.com/littlehb/p/16396710.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有