经典例题:POJ3279
给你一个01矩阵,矩阵大小为M x N。(1 <= M , N <= 15)
每次操作选择一个格子,使得该格子与上下左右四个格子的值翻转。
至少多少次操作可以使得矩阵中所有的值变为0?
请输出翻转方案,若没有方案,输出"IMPOSSIBLE” 。
若有多种方案符合题意,请首先输出翻转次数最少的方案;若方案个数仍不唯一,则输出字典序最小的方案。输入:
4 4
1 0 0 1
0 1 1 0
0 1 1 0
1 0 0 1输出:
0 0 0 0
1 0 0 1
1 0 0 1
0 0 0 0
此类问题有一种固定的问法,不管题意有多复杂,不管最终是让你输出翻转的最小次数还是以一个矩阵的形式输出每行每一个块,核心问题总是:你可以翻转每一块,目的是使每一块看上去完全相同,那么怎么翻(手动黑人问号?)。
一、位运算:首先位运算是一种简化技巧,直接借助位运算解决的题目很少(但是难题居多),但是很多算法是离不开位运算的(状态压缩,ST)。而且因为位运算直接和二进制打交道,效率不知道要比普通计算快多少。所以,合理的利用位运算是一种很好的技巧。
举几个例子:
1、a^=1代表将a所代表的二进制每一位状态翻转,0变为1,1变为0。
2、一个偶数和1异或等于这个数加一,一个奇数和1异或等于这个数减一。
3、二分经常用的(r+l)>>1,相当于除以2。
4、1<<5相当于2的5次方,其原理也很简单,就是把1对应的二进制数左移5位,后面补0,这种运算比pow快很多。
当然位运算有些情况下无法处理精度问题,比如用a^=b;b^=a;a^=b;代替swap无法处理double类型。
二进制状态压缩,即将一个长度为m的bool数组用一个m位的二进制数来表示和储存
二、方向数组
int x[4] = {0, 0, -1, 1};//四个方向
int y[4] = { -1, 1, 0, 0};//四个方向
for(int k = 0; k < 4; ++k)//向四个方向寻找,找到就翻转,这里使用了异或
if(i + x[k] > -1 && j + y[k] > -1)
t[i + x[k]][j + y[k]] ^= 1;
}
借助方向数组我们可以遍历到上下左右四个方向,本题只需要遍历这四个方向。有些题目需要更为复杂的方向数组。
三、memcpy
memcpy(t, g, sizeof(t));对数组进行复制,t是副本,g是原本
让我们回到这类问题,我们的目的是把每一块都变为相同的颜色,那么按照常规思路,我们可以先把第一行变成相同的颜色,因为第一行是上边界,只能通过翻转第二行来改变。方法是翻转第二行的某些块。这时,我们可以以此类推,若要把第二行变成相同的颜色,只需要翻转第三行...所以每次当我们要改变某一块的颜色,只需要翻转这一块下面的那块(翻转上面的就乱了)。那么,我们要把整个矩阵都变为相同的颜色,第一行的状态如何改变其实就决定了整个矩阵能否实现同化,如果翻到最后一行发现最后一行还有不同的颜色,那么说明这种方案是不可取的。题目一般还要我们输出最少翻转的次数。要想找到最优的步骤,我们需要对第一行的所有状态进行枚举。
一些全局变量
const int N = 16;
int g[N][N], t[N][N], f[N][N];
int cnt, n, m;
int x[4] = {0, 0, -1, 1};//四个方向
int y[4] = { -1, 1, 0, 0};//四个方向
模块化编程(函数)首先翻转操作如下:
借助异或操作,0变为1,1变为0
void flip(int i, int j)//翻转
{
++cnt, f[i][j] = 1;//步数加1,记录翻转了哪个瓷砖
t[i][j] = !t[i][j];//首先翻转自己
for(int k = 0; k < 4; ++k)//向四个方向寻找,找到就翻转,这里使用了异或
if(i + x[k] > -1 && j + y[k] > -1)
t[i + x[k]][j + y[k]] ^= 1;
}
对每一行进行操作,先把第一行的情况找出来
bool ok(int k)//对于第一行的每一种情况,判断是否能够产生最终的结果
{
cnt = 0;//初始化步数
memcpy(t, g, sizeof(t));//初始化临时数组,作为原始数组的副本
for(int j = 0; j < m; ++j)
if(k & (1 << ((m - 1) - j)))
flip(0, j);//如果某一列不为0,就翻转第一行的这个位置
//-------------------------------------------------------------------------------------//
//-------------------------------以上为枚举第一行---------------------------------------//
for(int i = 1; i < n; ++i)
for(int j = 0; j < m; ++j)
if(t[i - 1][j]) flip(i, j);//如果该列上一个位置是1,那么这个位置需要翻,否则不需要翻
for(int j = 0; j < m; ++j)//因为最后一行没有下一行,所以我们应该枚举判断最后一行的每一个元素,出现1就return false
if(t[n - 1][j]) return false;
return true;
}
主函数中主要是对第一行二进制表示所有情况的枚举,然后找到最小步骤
int main()
{
int ans, p;
while(~scanf("%d%d", &n, &m))
{
for(int i = 0; i < n; ++i)//数据输入
for(int j = 0; j < m; ++j)
scanf("%d", &g[i][j]);
//-------------------------------------------------------------------------------
ans = n * m + 1, p = -1;//初始化
//-------------------------------------------------------------------------------
for(int i = 0; i < (1 << m); ++i){//i表示一个二进制数,用来枚举第一行的各种不同翻法,如0001就是只翻最后一个
if(ok(i) && cnt < ans) //如果找到一种可能并且所用的步数更少的话,记下这种翻法
ans = cnt, p = i;
}
memset(f, 0, sizeof(f));
//--------------------------------------------------------------------------------
if(p >= 0)//最后找到的就是最少的翻法,模拟一遍,然后输出
{
ok(p);
for(int i = 0; i < n; ++i)
for(int j = 0; j < m; ++j)
printf("%d%c", f[i][j], j < m - 1 ? ' ' : '\n');
}
else puts("IMPOSSIBLE");//自带换行
}
}
部分代码来自colorfulshark