算法竞赛进阶指南-29.飞行员兄弟

题目链接

算法竞赛进阶指南-29.飞行员兄弟

“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有 1616 个把手的冰箱。

已知每个把手可以处于以下两种状态之一:打开或关闭。

只有当所有把手都打开时,冰箱才会打开。

把手可以表示为一个 4×44 \times 4 的矩阵,您可以改变任何一个位置 [i,j][i,j] 上把手的状态。

但是,这也会使得第 ii 行和第 jj 列上的所有把手的状态也随着改变。

请你求出打开冰箱所需的切换把手的次数最小值是多少。

输入格式

输入一共包含四行,每行包含四个把手的初始状态。

符号 + 表示把手处于闭合状态,而符号 - 表示把手处于打开状态。

至少一个手柄的初始状态是关闭的。

输出格式

第一行输出一个整数 NN,表示所需的最小切换把手次数。

接下来 NN 行描述切换顺序,每行输出两个整数,代表被切换状态的把手的行号和列号,数字之间用空格隔开。

注意:如果存在多种打开冰箱的方式,则按照优先级整体从上到下,同行从左到右打开。

数据范围

1i,j41 \le i,j \le 4

输入样例:

-+--
----
----
-+--

输出样例:

6
1 1
1 3
1 4
4 1
4 3
4 4

Method : 位运算+枚举

这道题比较像第8题费解的开关,而本题的枚举状态更是简单。

解题要领:

  • 按题目逻辑模拟实现开关函数turn(int x, int y)

  • 这种开关问题,如果按同一个开关两次,会恢复成原来的状态,因此要想按的次数最少,显然每个开关最多按一次,因此可以把状态进行二进制压缩:用一个int型数存储所有可能的开关方式,其二进制数表示开关的位置,i * n + j位数的1或0,映射到二维坐标[i][j]上,决定开或关

    • state存储答案,(state >> n & 1 == 1)来判断state从低到高的第n位是否为1
    • 每次枚举前都要memcpy备份,枚举后都要memcpy还原
#include <iostream>
#include <cstring>

using namespace std;

const int n = 4;
const int INF = 0x3f3f3f3f;

char graph[n][n];

void change(char &c) {
    if (c == '+') c = '-';
    else c = '+';
}

void turn(int x, int y) {
    change(graph[x][y]);
    for (int i = 0; i < n; i ++) {
        change(graph[i][y]);
    }
    for (int j = 0; j < n; j ++) {
        change(graph[x][j]);
    }
}

int main() {
    for (int i = 0; i < n; i ++) {
        cin >> graph[i];
    }

    int res = INF;
    int state;
    // 枚举所有开关打开的可能性, 1对应的位置选择turn
    for (int op = 0; op < (1 << n * n); op ++) {
        char backup[n][n];
        memcpy(backup, graph, sizeof graph);

        int cnt = 0;
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < n; j ++) {
                if ((op >> (i * n + j)) & 1) {
                    turn(i, j);
                    cnt ++;
                }
            }
        }

        bool flag = true;
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < n; j ++) {
                if (graph[i][j] == '+') {
                    flag = false;
                    break;
                }
            }
        }
        if (flag && cnt < res) {
            res = cnt;
            state = op;
        }
        memcpy(graph, backup, sizeof backup);
    }

    cout << res << endl;
    for (int i = 0; i < n; i ++) {
        for (int j = 0; j < n; j ++) {
            if ((state >> (i * n + j)) & 1) {
                printf("%d %d\n", i + 1, j + 1);
            }
        }
    }

    return 0;
}

复杂度分析

  • 时间复杂度:O(2(nn)(nn)n){O(2^{(n * n)} * (n * n) * n)}, 第一个(nn){(n * n)}opop状态的枚举,第二个(nn){(n * n)}是内部双重循环,最后一个n{n}turn()turn()函数。

  • 空间复杂度:O(nn){O(n * n)},存储输入的图为(nn){(n * n)}