题目理解:4x4=16个格子,每个格子有两种颜色:黑(b)或白(w),给出初始颜色后,每次可翻转一个格子,即由黑变白,或由白变黑。每翻转一个格子时,其周围的四个格子(即上下左右四个)也要一起翻转。输入为4x4矩阵,表示格子的初始颜色,输出为使得所有格子翻转为同一颜色所需要的翻转的最小格子数目或Impossible
思路变迁:
1、4x4矩阵可对应为4x4二维数组,黑为1,白为0,翻转格子总共有3种情况
a. 矩阵(1,1)、(1,4)、(4,1)、(4,4)位置翻转时,影响周围两个格子,即相当于总共翻转3个格子
b. 矩阵(2,2)、(2,3)、(3,2)、(3,3)位置翻转时,影响周围四个格子,即相当于总共翻转5个格子
c. 矩阵其他位置翻转时,影响周围三个格子,即相当于总共翻转4个格子
假设上面三种格子每种的翻转次数为x, y, z, 起初黑格子数量为m, 白格子数量为n,所以认为3x+5y+4z与m, n可能存在某种关系
2、认识到上面思路的错误:无法附带格子的位置信息,而这个是决定性的
新的发现:
a. 同一个格子翻转奇数次的结果与翻转1次并无差别,同一个格子翻转偶数次的结果与不翻并无差别
b. 若有解,结果肯定小于等于16
c. 翻转顺序对结果没有影响
3、回溯法的引入:定义解空间,每个格子可以进行两种操作:翻转或不翻转,所以搜索次数最大为65536。此时设解空间为集合S,S有65536个元素,每个元素均由16个0或1组成,可认为S={{0...0},......,{1...1}},要做的就是找出满足条件的元素,并找出最小的。此时解空间为子集数,故参考回溯法搜索子集树的模板代码:
void backtrack(int t) { if (t > n) output(x); for (int i = 0; i <= 1; i++) { x[t] = i; if (constraint(t) && bound(t)) backtrack(t + 1); } }
可以看到x[n]数组即解空间,数组每一个位置可为0或1,constraint(t)为约束函数,bound(t)为限界函数,可用其对子集树进行剪枝
结合本题所需的改变:
a. x[n]数组值为1,表示翻转,值为0,表示不翻转
b. 只要翻转为同一颜色即代表成功,所以搜索成功并不只在叶节点
c. 为了表示搜索成功,回溯函数需要另一个参数
d. 需要记录最小翻转次数,所以需要对解空间每个节点即x[n]数组,统计其中为1(表明翻转)的个数
e. 翻转过的节点回溯回来时,再翻转一次,即为原始的值
4、加速你的算法:
a. 4x4矩阵变数字,依次读入,第i位置的权值都为2的15-i次方
b. 翻转变为位运算,使用异或,与0异或保持不变,与1异或相当于取反
c. 空间换时间,提前计算每个位置翻转进行异或所需的数字,用数组保存
综合以上,具体代码如下:
#include<stdio.h> void backtrack(int sum, int t); int find=0; int min=16; int mypow[16]={1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768}; int flip[16]={51200,58368,29184,12544,35968,20032,10016,4880,2248,1252,626,305,140,77,39,19}; int x[16]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; int main() { int i=0; char a; int sum = 0; for(i=0; i<=15; ) { scanf("%c", &a); if(a=='b') { sum += mypow[15-i]; i++; } else if(a == 'w') i++; } backtrack(sum, 0); if(find == 0) printf("Impossible\n"); else printf("%d\n", min); return 0; } void backtrack(int sum, int t) { int i; int result=x[0]; for(i=1; i<=15; i++) { result+=x[i]; } if(sum == 65535 || sum == 0) { find=1; if(result < min) min = result; } if(t == 16) return; for(i=0; i<=1; i++) { x[t] = i; if(i==0) backtrack(sum, t+1); else backtrack(sum^flip[t], t+1); } }
5、使用迭代来代替递归,考虑子集树回溯的迭代模板:
x[n]={-1};//初始化 t=0; while(t>=0) { while(x[t]<=1) { x[t]++; if(x[t]<=1) { if x此时为最终的合法解 记录或输出 else if x为部分解 t=t+1; } } x[t]=-1;或其他恢复操作 t--;//回溯 }
所以上面的递归转换为迭代后,具体代码如下:
#include<stdio.h> using namespace std; bool find=false; int min=16; int mypow[16]={1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768}; int flip[16]={51200,58368,29184,12544,35968,20032,10016,4880,2248,1252,626,305,140,77,39,19}; int x[16]={-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}; int main() { int i=0, j=0; char a; int sum = 0; int result, t; for(i=0; i<=15; ) { scanf("%c", &a); if(a=='b') { sum += mypow[15-i]; i++; } else if(a == 'w') i++; } while(t>=0) { while(x[t]<=1) { x[t]++; if(x[t] == 1) { sum = sum^flip[t]; } if(x[t]<=1) { if(t==15) { for(j=1, result=x[0]; j<=15; j++) result += x[j]; if(sum == 65535 || sum == 0) { find = true; if(result < min) min = result; } } else { t++; } } } x[t] = -1; sum = sum^flip[t]; t--; } if(find) printf("%d\n", min); else printf("Impossible\n"); return 0; }