开关问题
1. 概述
-
此类问题有一个共同的特点,即如果当前位置状态发生变化时,会有一些位置跟着变化。
-
我们需要求解出从初始状态到达终止状态需要的最少步数。
2. 例题
AcWing 1208. 翻硬币
问题描述
- 问题链接:AcWing 1208. 翻硬币
分析
-
因为翻动两次相当于没有翻动,因此每个位置最多翻动一次,因此本题是一个确定性过程,从目标状态的第一个硬币开始考虑,如果不相同则翻动。
-
因为保证有解,直接统计翻动次数,输出即可。
代码
- C++
#include <iostream>
using namespace std;
string a, b;
void turn(int i) {
if (a[i] == '*') a[i] = 'o';
else a[i] = '*';
}
// 递推
int main() {
cin >> a >> b;
int res = 0;
for (int i = 0; i + 1 < a.size(); i++) {
if (a[i] != b[i]) {
res++;
turn(i), turn(i + 1);
}
}
cout << res << endl;
return 0;
}
AcWing 95. 费解的开关
问题描述
- 问题链接:AcWing 95. 费解的开关
分析
-
本题中每个位置有一个灯,当我们按下这个灯的开关后,会改变周围四个灯以及自己的状态,问我们最少按多少次可以让所有灯全部等变亮。
-
因为题目中要求最多按
6
次,多于6
次直接输出-1
即可。因此一种做法是从全亮的状态向前回推(宽搜),使用哈希表记录下所有能到达的状态以及次数,然后判断题目给的状态是否在哈希表中存在,如果不存在,输出-1
,存在输出次数。状态数大于为 2 25 2^{25} 225个,大于三千多万个状态。 -
为了更加保险,这里介绍另一种做法,采用递推求解。
-
本题中每个位置最多被按一次,按多次没有用;另外和按的顺序没有关系。因此可以使用从上到下递推的方式求解。
-
假设第一行的状态已经确定,然后需要操作第二行使得第一行全部变为
1
,可以发现对第二行的操作是唯一的:因为如果第一行某个位置为1
,则第二行对应位置必定不能被按;如果第一行某个位置为0
,则第二行对应位置必定需要被按。 -
然后第二行的状态就确定了,然后需要操作第三行使得第二行全部变为
1
,依次类推到最后一行。最后判断最后一行是否全部为1
即可确定是否有解。 -
为了枚举到所有方式,我们可以枚举第一行的所有状态,一共 2 5 = 32 2^5=32 25=32种方式。这种方式的计算量大约为 32 × 25 × 5 × 500 = 2 × 1 0 6 32 \times 25 \times 5 \times 500=2 \times 10^6 32×25×5×500=2×106(
500
是测试数据数量)。
代码
- C++
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 6;
char g[N][N], bg[N][N]; // 一定要注意这里是char
int dx[5] = {-1, 0, 1, 0, 0}, dy[5] = {0, 1, 0, -1, 0};
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;
// '0'(48): 01001000 <-> '1'(49): 01001001
g[a][b] ^= 1;
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
for (int i = 0; i < 5; i++) scanf("%s", &bg[i]);
int res = 10;
for (int op = 0; op < 1 << 5; op++) {
int cnt = 0;
memcpy(g, bg, sizeof bg);
// 操作第一行的开关
for (int i = 0; i < 5; i++)
if (op >> i & 1) {
turn(0, i);
cnt++;
}
// 递推出后四行的状态
for (int i = 0; i < 4; i++)
for (int j = 0; j < 5; j++)
if (g[i][j] == '0') {
turn(i + 1, j);
cnt++;
}
// 判断最后一行是否全部为1
bool success = true;
for (int i = 0; i < 5; i++)
if (g[4][i] == '0')
success = false;
if (success && res > cnt) res = cnt;
}
if (res > 6) res = -1;
printf("%d\n", res);
}
return 0;
}
AcWing 116. 飞行员兄弟
问题描述
- 问题链接:AcWing 116. 飞行员兄弟
分析
-
暴力求解即可。即枚举所有可能的操作方案,找到使用步数最少的方案。
-
使用二进制表示整个面板状态,记为
state
,给每个格子编号,从左到右,从上到下依次是0~15
。 -
使用二维数组
change[i][j]
表示改变(i, j)
所引起的整个棋盘的变换情况。这样我们当我们改变(i, j)
的状态时,只需要将state
异或上change[i][j]
即可。
代码
- C++
#include <iostream>
#include <cstring>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 4, INF = 100;
int change[N][N]; // change[i][j]记录改变(i, j)状态导致整个棋盘状态的变化
// 二维坐标转化为一维,(0, 0) -> 0
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, tmp;
for (int i = 0; i < 1 << 16; i++) {
int now = state;
tmp.clear();
for (int j = 0; j < 16; j++)
if (i >> j & 1) {
int x = j / 4, y = j % 4;
now ^= change[x][y];
tmp.push_back({x, y});
}
if (!now) { // 说明i对应的操作是一组合法操作
if (path.empty() || path.size() > tmp.size())
path = tmp;
}
}
cout << path.size() << endl;
for (auto &p : path)
cout << p.x + 1 << ' ' << p.y + 1 << endl;
return 0;
}
AcWing 208. 开关问题
问题描述
- 问题链接:AcWing 208. 开关问题
分析
-
这里以一个例子讲解这个题目,有三个开关,分别为
1、2、3
,规则是:如果按1
,则1、2
的状态会改变;如果按2
,则1、2、3
的状态会改变;如果按3
,则3
的状态会改变。初始状态是0、0、0
,转变为1、1、1
存在多少种方案? -
我们使用 x 1 、 x 2 、 x 3 x_1、x_2、x_3 x1、x2、x3 分别表示每个开关有没有发生变化,没变为
0
,变了为1
。根据上述规则,按1、2
会影响1
的状态,因此x1 ^ x2 = 1
,同理我们可以得到如下异或方程组:
{ x 1 ⊕ x 2 = 1 第 一 个 开 关 x 1 ⊕ x 2 = 1 第 二 个 开 关 x 2 ⊕ x 3 = 1 第 三 个 开 关 \begin{cases} x_1 \oplus x_2 = 1 \quad 第一个开关 \\ x_1 \oplus x_2 = 1 \quad 第二个开关 \\ x_2 \oplus x_3 = 1 \quad 第三个开关 \end{cases} ⎩⎪⎨⎪⎧x1⊕x2=1第一个开关x1⊕x2=1第二个开关x2⊕x3=1第三个开关
-
因此此时我们就将问题转化成为了AcWing 884. 高斯消元解异或线性方程组。对增广矩阵进行变换,变为上三角矩阵,最后如果有
k
个自由元(即k
个行全0
),最终的结果就为 2 k 2^k 2k。 -
上述例题存在一个自由元,因此答案是
2
,两个答案分别是 ( x 1 = 0 , x 2 = 1 , x 3 = 0 ) 、 ( x 1 = 1 , x 2 = 0 , x 3 = 1 ) (x_1=0, x_2=1, x_3=0)、(x_1=1, x_2=0, x_3=1) (x1=0,x2=1,x3=0)、(x1=1,x2=0,x3=1)。
代码
- C++
#include <iostream>
#include <cstring>
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; i <= n; i++)
if (a[i][c])
t = i;
if (a[t][c] == 0) continue;
// 交换
for (int i = c; i <= n + 1; i++) swap(a[r][i], a[t][i]);
// 消
for (int i = r + 1; i <= n; i++)
for (int j = n + 1; j >= c; j--)
a[i][j] ^= a[i][c] & 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;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
memset(a, 0, sizeof a);
for (int i = 1; i <= n; i++) scanf("%d", &a[i][n + 1]);
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
a[i][n + 1] ^= x; // a[i][n + 1]存储的是第i个灯的状态是否变化
a[i][i] = 1; // 默认开关可以控制自己
}
int x, y;
while (scanf("%d%d", &x, &y), x || y) a[y][x] = 1; // 影响的是第y个方程
int t = gauss();
if (t == -1) puts("Oh,it's impossible~!!");
else printf("%d\n", t);
}
return 0;
}
POJ 3185. The Water Bowls
问题描述
一共有一行20个数,每个数字都是0或1,每次翻一个数都会将这个数两边的数取反,保证有解,求全部反转为0,最少的反转次数?
分析
-
本题相当于每次翻转3个数,且中间那个数是驱动翻转的那个数,因此从第一个数开始考虑,如果第一个数为1,则必须翻转第二个数,这样前三个数都会被翻转,第一个数可以变为0,这样递推计算即可。
-
本题和AcWing 1208. 翻硬币很类似,都是一个确定性的过程。
代码
- C++
#include <iostream>
using namespace std;
int n = 20, k = 3;
int a[25];
void turn(int i) {
a[i - 1] ^= 1;
a[i] ^= 1;
a[i + 1] ^= 1;
}
int main() {
/*
测试用例:
0 0 1 1 1 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0
*/
for (int i = 0; i < n; i++) cin >> a[i];
int res = 0;
for (int i = 0; i + k - 1 < n; i++)
if (a[i]) {
res++;
turn(i + 1);
}
cout << res << endl;
return 0;
}
POJ 3276. Face The Right Way
问题描述
分析
-
从下到达枚举
k
,计算翻转需要的次数,更新答案即可。 -
在确定
k
之后,如何计算需要翻转的次数呢?从第一个位置开始考虑,如果当前位置是向后,则将从当前位置开始的长度为k
的区间全部翻转。最后检查最后k-1
头牛的朝向是否正确,如果都是朝前,则说明有解,否则对于当前的k
无解。 -
对于每个
k
求解时,如果翻转区间直接暴力枚举,则对于每个k
来说,时间复杂度是 O ( n 2 ) O(n^2) O(n2)的,因此总体时间复杂度为 O ( n 3 ) O(n^3) O(n3),不可取,因此考虑优化这一步。 -
可以使用数组
f
,其中f[i]
表示区间[i~i+k-1]
是否翻转,翻转为1,未反转为0,使用sum
记录当前考察位置翻转次数,这样就可以知道当前考察的位置的朝向了。此时对于每个k
,时间复杂度被优化为 O ( n ) O(n) O(n)的了,因此总体时间复杂度为 O ( n 2 ) O(n^2) O(n2)的。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 5010;
int n;
int d[N]; // 初始朝向, 1表示向前,最终要变为全1
int f[N]; // f[i~i+k-1]是否翻转
int calc(int k) {
memset(f, 0, sizeof f);
int res = 0; // 需要翻转的次数
int sum = 0; // 当前遍历位置被翻转的次数
for (int i = 0; i + k - 1 < n; i++) {
if ((d[i] + sum) % 2 == 0) { // 说明第i头牛面向后, 需要翻转
res++;
f[i] = 1; // 表示f[i~i+k-1]需要翻转
}
sum += f[i];
// f[i-k+1]影响的区间为[i-k+1~i], 不会影响i+1
// 因此考虑下次i时,需要减去
if (i - k + 1 >= 0) {
sum -= f[i - k + 1];
}
}
// 单独检查后面k-1头牛的朝向是否正确
for (int i = n - k + 1; i < n; i++) {
if ((d[i] + sum) % 2 == 0) return -1;
if (i - k + 1 >= 0) sum -= f[i - k + 1];
}
return res;
}
int main() {
/*
测试用例:
7
BBFBFBB
*/
cin >> n;
for (int i = 0; i < n; i++) {
char c;
cin >> c;
if (c == 'F') d[i] = 1;
else d[i] = 0;
}
int res = n, k = 1; // res: 操作次数, k: 翻转的连续数目
for (int i = 1; i <= n; i++) {
int t = calc(i);
if (t != -1 && t < res) {
res = t;
k = i;
}
}
cout << k << ' ' << res << endl;
return 0;
}