前几天我妈给我发来一道题,说是我表弟(他读三年级)的一道数学题,问我会不会:
我大概看了一下,有几个格子很好填出来,比如最中间的4,还有几个格子是可以知道里面的数的组合(具体的排列情况可能要根据每行每列是否冲突来进行调整),至于在这之后, 我想到的办法就是一个一个地去挑格子去试,一旦不对就回到前几步,除此之外我没想到什么更好的办法。
既然都用这种笨办法一个个去试了,何不用计算机去试呢2333,毕竟干傻事人脑可干不过电脑呀。回顾我刚才手算的策略,这不就是典型的试探——回溯的算法嘛。只不过我自己做的时候,倾向于在整张表格里先挑明显看得出来是由哪几个数组成的格子,然后尝试不同的排列方式,一个个去试过去,像是那种“先在一个根据地扎根,然后一步步拓宽解放区”的策略。至于计算机来的话,就不需要这么复杂了,这题的数据量不大,直接枚举所有可能的7*7数独组合,然后用原题里“规则”下面的约束剪枝就可以(不剪枝会死得很难看,枚举全排列的话是 O(7^49) ,枚举数独排列(每行每列不重不漏)的话,也是阶乘级别的复杂度的)。按照这个思路,这题就是道经典的dfs暴力搜索题了(不过就我自己而言是还没学到图论和图算法这一块的,只是之前做过一道应用dfs的题目,所以大概有点点概念的那种)。我采用一维数组,按从左到右、从上到下的顺序来存储这个表格。
话不多说,代码伺候:
#include <cstdio>
#include <cstring>
int a[49] = {0};
inline int sum(int a, int b) { return a + b; }
inline int sum(int a, int b, int c) { return a + b + c; }
inline int dif(int a, int b) { return a > b ? (a - b) : (b - a); }
inline bool isLegal(int *a, int index)
{ //这里判断的条件就是对应原题“规则”下面的第3,第4,第5条的要求
bool f[49];
memset(f, true, sizeof(f));
//l1
f[2] = (sum(a[0], a[1], a[2]) == 10);
f[4] = (sum(a[3], a[4]) == 13) || (dif(a[3], a[4]) == 13);
f[13] = (sum(a[5], a[6], a[13]) == 8);
f[14] = (sum(a[7], a[14]) == 5) || (dif(a[7], a[14]) == 5);
//l2-l3
f[9] = (sum(a[8], a[9]) == 3) || (dif(a[8], a[9]) == 13);
f[17] = (sum(a[10], a[17]) == 7) || (dif(a[10], a[17]) == 7);
f[12] = (sum(a[11], a[12]) == 13) || (dif(a[11], a[12]) == 13);
f[16] = (sum(a[15], a[16]) == 11) || (dif(a[15], a[16]) == 11);
f[25] = (sum(a[18], a[25]) == 2) || (dif(a[18], a[25]) == 2);
f[20] = (sum(a[19], a[20]) == 11) || (dif(a[19], a[20]) == 11);
//l4-l7
f[35] = (sum(a[21], a[28], a[35]) == 12);
f[29] = (sum(a[22], a[29]) == 11) || (dif(a[22], a[29]) == 11);
f[31] = (sum(a[23], a[30], a[31]) == 7);
f[24] = (a[24] == 4);
f[33] = (sum(a[26], a[33]) == 9) || (dif(a[26], a[33]) == 9);
f[34] = (sum(a[27], a[34]) == 4) || (dif(a[27], a[34]) == 4);
f[37] = (sum(a[36], a[37]) == 13) || (dif(a[36], a[37]) == 13);
f[45] = (sum(a[38], a[44], a[45]) == 14);
f[46] = (sum(a[32], a[39], a[46]) == 7);
f[41] = (sum(a[40], a[41]) == 4) || (dif(a[40], a[41]) == 4);
f[48] = (sum(a[47], a[48]) == 7) || (dif(a[47], a[48]) == 7);
f[43] = (sum(a[42], a[43]) == 9) || (dif(a[42], a[43]) == 9);
//下面的for循环用来给dfs剪枝,一旦发现当前已有的组合不满足条件,立马返回false
for (int i = 0; i < index; ++i)
{
if (f[i] == false) return false;
}
return true;
}
inline bool isInterupt(int *a, int index, int cp)
{ //这个函数也是拿来剪枝的。它用来判断生成当前的组合每行每列是否有重复的元素(这个条件等价于“规则”里的第2条)
int current_line = index / 7 * 7;
int current_column = index % 7;
for (int i = current_line; i < current_line + 7; ++i)
{
if (i != index && cp == a[i])
return true;
}
for (int j = current_column; j < 49; j += 7)
{
if (j != index && cp == a[j])
return true;
}
return false;
}
inline void display(int *a)
{ //打印最后的答案
for (int i = 0; i < 49; ++i)
{
if (a[i] == 0)
return;
}
for (int i = 0; i < 49; i++)
{
printf("%d ", a[i]);
if (i % 7 == 6)
printf("\n");
}
}
void dfs(int *a, int cur)
{
if (!isLegal(a, cur)) return;
if (cur == 49)
{
display(a);
return;
}
//这一步是一个试探——回溯的过程,先对当前位从1-7试探,如果不冲突,则设置数位以后转到下一位;
for (int i = 1; i <= 7; i++)
{
if (!isInterupt(a, cur, i)) //按照当前排列是否满足
{
a[cur] = i;
dfs(a, cur + 1);
}
a[cur] = 0; //如果冲突(意味着1-7全都试过一遍了),则把当前一位置0然后return回上一个数位继续试探(否则影响上一个数位的isInterupt判定)
}
}
int main()
{
printf("start...\n");
dfs(a, 0);
return 0;
}
最后此题答案是:
5 2 3 6 7 1 4
4 1 2 5 6 7 3
1 5 6 2 3 4 7
3 7 1 4 5 6 2
7 4 5 1 2 3 6
2 6 7 3 4 5 1
6 3 4 7 1 2 5
这个算法写的时候,注意回溯前把当前位置0,然后剪枝剪好就行。