[枚举] aw116. 飞行员兄弟(二维递推+开关问题+二进制枚举)

Posted Ypuyu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[枚举] aw116. 飞行员兄弟(二维递推+开关问题+二进制枚举)相关的知识,希望对你有一定的参考价值。

1. 题目来源

链接:116. 飞行员兄弟

区别题:[递推] aw95. 费解的开关(二维递推+开关问题+二进制枚举)

2. 题目解析

本题数据范围比较小,4*4 的棋盘,总共的状态数量是 2 4 ∗ 4 2^{4*4} 244 种,也就是 65536 种,很少的状态数量。

本题不可以递推, 要区别于:[递推] aw95. 费解的开关(二维递推+开关问题+二进制枚举)。因为本题的状态改变是行列全部改变,故不会区别上一层、本层,就没办法去递推,互相影响的灯泡太多了,无法直接去递推。

但同时也满足两个性质。

  • 每个点只会操作一次。
  • 操作顺序是任意的。

这也是开关问题的一个共性了。


数据范围小,直接暴力枚举所有方案,因为每个开关只会有两种情况,按或不按,共 2 4 ∗ 4 2^{4*4} 244 种,也就是 65536 种情况,最后判断当前枚举的这种方案能否将整个灯泡亮即可。

技巧:

  • 方案要求按字典序最小输出,我们可以将二进制枚举的状态数对应到二维数组中,即移位的时候,让其每一位对应二维数组中的一个格子就行了。
  • 当然也可以直接用位运算直接做

时间复杂度: O ( 2 16 ∗ ( 16 ∗ 7 + 16 + 16 ) ) = 9 , 437 , 184 O(2^{16}*(16*7+16+16))= 9,437,184 O(216(167+16+16))=9,437,184,开关状态,每个点需要改变 7 个点(行列自己,共7个点),判断每个灯泡状态,记录方案。

空间复杂度: O ( 4 ∗ 4 ) O(4*4) O(44)


二维数组写法:

#include <bits/stdc++.h>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 5;

char g[N][N], backup[N][N];

int get(int x, int y) {
    return x * 4 + y;
}

void turn_one(int x, int y) {
    if (g[x][y] == '+') g[x][y] = '-';
    else g[x][y] = '+';
}

void turn_all(int x, int y) {
    for (int i = 0; i < 4; i ++ ) {
        turn_one(x, i);
        turn_one(i, y);
    }

    turn_one(x, y);
}

int main() {
    for (int i = 0; i < 4; i ++ ) cin >> g[i];

    vector<PII> res;
    memcpy(backup, g, sizeof g);                    // 备份
    
    for (int op = 0; op < 1 << 16; op ++ ) {
        vector<PII> temp;
        for (int i = 0; i < 4; i ++ )               // 进行操作,二进制位映射到二维数组
            for (int j = 0; j < 4; j ++ )
                if (op >> get(i, j) & 1) {
                    temp.push_back({i, j});
                    turn_all(i, j);
                }

        bool has_closed = false;                    // 判断所有灯泡是否全亮
        for (int i = 0; i < 4; i ++ )
            for (int j = 0; j < 4; j ++ )
                if (g[i][j] == '+')
                    has_closed = true;

        if (has_closed == false) {					// 答案更新
            if (res.empty() || res.size() > temp.size()) res = temp;
        }

        memcpy(g, backup, sizeof g);                // 使用备份还原
    }

    cout << res.size() << endl;
    for (auto op : res) cout << op.x + 1 << ' ' << op.y + 1 << endl;

    return 0;
}

位运算写法:貌似对棋盘做了优化,看不太懂,也懒得看了。进阶指南的题,也是进阶指南的写法。change[N][N]; 数组每一位存的是行列 7 个数的二进制表示之和。每次进行状态改变的时候,要改变 7 个位置的状态,就只需要 ^change[x][y] 就行了。

  • 从0~(1<<16)枚举到的第一个直接输出也能ac,这是为啥?贪心?
  • 本题比较特殊,可以用代码把所有情况枚举一遍,会发现每种局面的操作方案是唯一的,所以第一次找到的解一定是最优解。 从0~(1<<16)枚举到的第一个直接输出也能ac,这是为啥?贪心?
#include <bits/stdc++.h>

using namespace std;

typedef pair<int,int> PII;

const int N = 4, INF = 100;

int change[N][N];

int get(int x, int y) {
    return x * N + y;
}

int main() {
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j < N; j ++ ) {
            for (int k = 0; k < N; k ++ ) change[i][j] += (1 << get(i, k)) + (1 << get(k, j));
            change[i][j] -= 1 << get(i, j);
        }

    int state = 0;
    for (int i = 0; i < N; i ++ ) {
        string line;
        cin >> line;
        for (int j = 0; j < N; j ++ )
            if (line[j] == '+')
                state += 1 << get(i, j);
    }

    vector<PII> path;
    for (int i = 0; i < 1 << 16; i ++ ) {
        int now = state;
        vector<PII> temp;
        for (int j = 0; j < 16; j ++ )
            if (i >> j & 1) {
                int x = j / 4, y = j % 4;
                now ^= change[x][y];
                temp.push_back({x, y});
            }
        if (!now && (path.empty() || path.size() > temp.size())) path = temp;
    }

    cout << path.size() << endl;
    for (auto &p : path) cout << p.first + 1 << ' ' << p.second + 1 << endl;

    return 0;
}

以上是关于[枚举] aw116. 飞行员兄弟(二维递推+开关问题+二进制枚举)的主要内容,如果未能解决你的问题,请参考以下文章

116. 飞行员兄弟

[递推] aw95. 费解的开关(二维递推+开关问题+二进制枚举)

AcWing 飞行员兄弟 二进制枚举

acwing 116. 飞行员兄弟

[递推] aw3777. 砖块(思维+递推+CF1271B)

飞行员兄弟 DFS+枚举