数独的优化回朔算法(三)
RecoverCellCandidate
既然有移除候选数的方法,同样也需要有恢复候选数的方法。这个方法需要保证,如果我们为某个单元格赋了某一个值后发现无论如何都不能成功生成数独,那么我们必须要完整的恢复候选数到赋值前的状态。上代码!
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/// 恢复单元格的候选数
/// </summary>
/// <param name="table">表</param>
/// <param name="i">行</param>
/// <param name="j">列</param>
/// <param name="index">索引</param>
/// <returns>返回结果</returns>
private bool RecoverCellCandidate(Cell[,] table, int i, int j, int index)
{
int value = table[index / 9, index % 9].Value;
bool flag = true;
if (table[i, j].DuplicateDel.ContainsKey(value))
{//如果在重复删除的字典里有此数,则重复删除字典此数对应的键值的值-1
if (--table[i, j].DuplicateDel[value] == 0)
{
table[i, j].DuplicateDel.Remove(value);
}
}
else if (!table[i, j].Candidate.Contains(value))
{//如果单元格候选数没有此数,添加之
table[i, j].Candidate.Add(value);
}
return flag;
}
其实这里恢复候选数在上一篇文章中已经提过了,如果是发现DuplicateDel字典里有这个值对应的主键,那么就把这个主键对应的字典里的值减去1,如果发现减去1后值为0,那么便将其从字典中移除。如果是DuplicateDel字典里没有这个值对应的主键,那说明没有重复删除过此候选数,便将其添加至候选数列表中。
DealRelativeCell
写好了移除候选数和恢复候选数的方法之后,我们将开始考虑整个程序了。
1.当我们为某一个单元格A赋值的时候,便需要更新A的相关单元格的候选数列表,从列表中移除这个值的候选数,也就是对A的所有相关单元格调用RemoveCellCandidate方法;
2.当我们发现为A赋某个值不能生成数独时,便会为A赋另外一个值去尝试,此时也需要更新A的相关单元格的候选数列表,从列表中恢复之前那个值的的候选数,也就是对A的所有相关单元格调用RecoverCellCandidate方法;
大家有没有发现,我们的这两个方法都是在为A赋值(或者重新选择另一个数赋值)时,所有A的相关单元格需要调用的方法。简单点说,就是这两个方法每次的作用范围都相同,都是A的相关单元格。那么这很容易便想到用C#的代理来实现,我们只需要写一个找寻A的相关单元格的方法DealRelativeCell,然后传入一个方法,这个方法可以是RemoveCellCandidate或者RecoverCellCandidate,然后在DealRelativeCell方法里调用
传入的方法即可!代码如下!
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/// 处理相关单元格
/// </summary>
/// <param name="cellMethod">调用方法</param>
/// <param name="table">表</param>
/// <param name="index">索引</param>
/// <returns>返回结果</returns>
private bool DealRelativeCell(RelativeCellMethod cellMethod, Cell[,] table, int index)
{
bool flag = true;
Cell cell = table[index / 9, index % 9];
for (int i = 0; i < 9; i++)
{
//同列单元格
if (i != index / 9)
{//不能等于本单元格
flag &= cellMethod(table, i, index % 9, index);
}
//同行单元格
if (i != index % 9)
{//不能等于本单元格
flag &= cellMethod(table, index / 9, i, index);
}
}
//同九宫格单元格的剩余四个单元格
for (int i = nineCells[index / 9]; i < nineCells[index / 9] + 3; i++)
{
for (int j = nineCells[index % 9]; j < nineCells[index % 9] + 3; j++)
{
if (i != index / 9 && j != index % 9)
{
flag &= cellMethod(table, i, j, index);
}
}
}
if (cellMethod == RecoverCellCandidate)
{
cell.Value = 0;
}
return flag;
}
一般的方法传入的参数都是用来计算或者赋值,这个DealRelativeCell方法接受的参数中包括一个叫做RelativeCellMethod的代理:
private delegate bool RelativeCellMethod(Cell[,] table, int i, int j, int index);
这个代理表示的是另一个方法的引用,于是DealRelativeCell方法里就可以直接调用传入的代理来处理我们的相关单元格。这个方法的本质就是为我们找寻与A相关的单元格,而具体要做什么操作,他并不管,操作由RelativeCellMethod类型的cellMehod来完成。这里还传入了数独表table和单元格A的索引。
举个简单的例子:假设A是二年级一班的班主任,张三,李四是二年级一般的学生。某一天,A老师要为班上的所有同学一人发一个苹果,那么他要做两件事:1.找到所有二年级一班的学生,也就是张三、李四;2.为上一步找到的学生发苹果。如果第二天,A老师又要为班上所有同学一人发一个橘子,那么他又要做相似的两件事:1.找到所有二年级一班的学生,也就是张三、李四;2.为上一步找到的学生发橘子。只不过是把苹果换成橘子而已。显然,这两天中我们做的第一件事都是重复的,只有第二件事不同。因此我们很自然的想把第一件事抽离出来。用DealRelativeCell方法来解释:我们定义一个DealRelativeCell(Method,teacher)的方法,这个方法只负责找这个老师班上对应的所有学生,但并不知道是发苹果还是橘子什么的,然后再定义两个方法SendApple(学生),SendOrange(学生)。这两个方法负责给传入的参数学生发苹果或者橘子。然后第一天,我们就可以描述成:DealRelativeCell(SendApple,A老师)这样,第二天描述成:DealRelativeCell(SendOrange,A老师)这样。DealRelativeCell方法里面直接写Method(学生)方法即可。应用到数独这个程序,老师就相当于是单元格A,学生就是A的所有相关单元格。发苹果这个动作可以比作移除候选数这个方法,发橘子这个动作可比作恢复候选数。DealRelativeCell方法只负责用来找A所对应的相关单元格。这个程序就好理解了。
另外还要注意一点,假设A有相关单元格B、C、D(实际上A应该有20个相关单元格,这里只是举个例子),当我们为A赋值1,便需要移除B、C、D的候选数1,但是假如当我们移除C的候选数1的时候,发现某些操作失败,需要回滚,这时我们不能立即退出这个方法,而是需要把后面D的候选数1也一并移除之后再回滚。为什么呢,因为我们做回滚操作的时候肯定需要恢复相关单元格的候选数,如果在移除相关单元格候选数时中途退出,也就是说有一部分相关单元格没有移除,那我们恢复候选数的时候就不好找到哪些单元格是移除过的,哪些是没移除过的。所以最好的方法就是打包处理,只要是移除或者恢复,就算中途发现返回结果是回滚,也等到把所有相关单元格的都一并处理完之后再回滚。
这里还需要注意最后一个细节就是最后几行代码,判断如果传入的操作是恢复候选数RecoverCellCandidate,那么要多做一个操作:即把单元格A的值重新置为0,以便重新赋值。
还要说明一点,就是处理同在九宫格的相关单元格,这里借鉴的是生鱼片那篇文章的方法,先定义了一个
private int[] nineCells = new int[9] { 0, 0, 0, 3, 3, 3, 6, 6, 6 };//处理九宫格的约束
这样便能有效地找到A所在的九宫格了,方法很巧妙!如果还不理解可以自己运行程序跟踪就知道了。
SetUniqueCellCandidate
这里我们还可以对程序进一步优化!试想如果你在填数独的时候,填着填着,突然,发现有一个空白单元格的候选数只有一个了,你会怎么办?你的大脑会立刻告诉你,先别管其他,快去把那个单元格填了,因为那个单元格只能填那唯一的一个数,没有其他选择。但计算机没有人脑,所以需要我们去告诉他!因此SetUniqueCellCandidate方法就诞生了!
首先我们讨论一下这个方法应该放在什么地方。问:我们一般什么时候去关心候选数呢?答:是为单元格赋值的时候,我们需要移除候选数,还有回滚的时候,我们需要恢复候选数。那么什么时候最有可能是第一次发现某单元格候选数个数为一的时候呢?当然应该是移除候选数的时候,因为恢复候选数只能使候选数的个数增加,只有在移除候选数时才会第一时间发现单元格候选数个数为一。因此,我们应该把这个SetUniqueCellCandidate方法放在RemoveCellCandidate方法后面,上代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/// 为候选数个数为一的单元格赋值
/// </summary>
/// <param name="table">表</param>
/// <param name="i">行</param>
/// <param name="j">列</param>
/// <param name="index">索引</param>
/// <returns>返回结果</returns>
private bool SetUniqueCellCandidate(Cell[,] table, int i, int j, int index)
{
RelativeCellMethod removeCandidateMethod = new RelativeCellMethod(RemoveCellCandidate);
RelativeCellMethod recoverCandidateMethod = new RelativeCellMethod(RecoverCellCandidate);
bool flag = true;
if (table[i, j].Value == 0 && table[i, j].Candidate.Count == 1)
{//如果单元格移除此候选数之后,候选数量为1,则直接为此单元格赋值剩下的候选数
int oldValue = table[i, j].Candidate[0];//保存该唯一候选数,以便如果失败恢复
flag &= SetValue(table[i, j]);
flag &= DealRelativeCell(removeCandidateMethod, table, i * 9 + j);
if (!flag)
{//如果移除候选数失败,则恢复候选数,回滚
DealRelativeCell(recoverCandidateMethod, table, i * 9 + j);
table[i, j].Candidate.Add(oldValue);
flag &= false;
}
}
return flag;
}
解释下上面的代码:
1.定义两个代理方法,一个是移除候选数,一个是恢复候选数;
2.先保存那个唯一的候选数,如果为这个单元格赋值之后发现不能正确生成数独,回滚的时候,就需要把这个候选数再还原恢复到这个单元格中;
3.为单元格赋值;
4.移除相关单元格的候选数;
5.如果移除操作后发现生成数独失败,回滚,那么便恢复相关单元格的候选数,同时再把这个单元格的候选数中从新添加上之前保存的那个值。
下面我们再来看,这个方法的作用范围是什么呢?有人可能会说是整个数独表,但是其实可以把范围缩的更小些,那就是赋值单元格A时,所有A的相关单元格,因为此时只有A的相关单元格候选数有变动,其他单元格不会有变化。那你肯定就会想到,这个作用范围岂不是和之前移除候选数、恢复候选数的作用范围一样了?是的,那我们自然又理所当然的会想用DealRelativeCell方法来帮我们找相关单元了!
因此我们对之前的DealRelativeCell方法做一些改动,得到一个更优化更完整的方法:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/// 处理相关单元格
/// </summary>
/// <param name="cellMethod">调用方法</param>
/// <param name="table">表</param>
/// <param name="index">索引</param>
/// <returns>返回结果</returns>
private bool DealRelativeCell(RelativeCellMethod cellMethod, Cell[,] table, int index)
{
bool isSetUniqueCellMethod = false;
if (cellMethod == SetUniqueCellCandidate)
isSetUniqueCellMethod = true;
bool flag = true;
Cell cell = table[index / 9, index % 9];
for (int i = 0; i < 9; i++)
{
//同列单元格
if (i != index / 9)
{//不能等于本单元格
//如果任意一个赋值失败则直接跳出循环,以下同理
if (isSetUniqueCellMethod && !flag) break;
flag &= cellMethod(table, i, index % 9, index);
}
//同行单元格
if (i != index % 9)
{//不能等于本单元格
if (isSetUniqueCellMethod && !flag) break;
flag &= cellMethod(table, index / 9, i, index);
}
}
//同九宫格单元格的剩余四个单元格
for (int i = nineCells[index / 9]; i < nineCells[index / 9] + 3; i++)
{
for (int j = nineCells[index % 9]; j < nineCells[index % 9] + 3; j++)
{
if (i != index / 9 && j != index % 9)
{
if (isSetUniqueCellMethod && !flag) break;
flag &= cellMethod(table, i, j, index);
}
}
}
if (cellMethod == RemoveCellCandidate && flag)
{//如果都移除候选数成功,则判断有没有只剩一个候选数的为赋值的单元格,有则赋值上
RelativeCellMethod setCandidateMethod = new RelativeCellMethod(SetUniqueCellCandidate);
flag &= DealRelativeCell(setCandidateMethod, table, index);
}
if (cellMethod == RecoverCellCandidate)
{
cell.Value = 0;
}
return flag;
}
这个代码应该不难理解,但有一点要注意,如果我们这个DealRelativeCell方法发现传入的方法参数是SetUniqueCellCandidate方法,那么在为相关单元格找唯一候选数赋值时,如果发现有某个单元格满足条件,赋值之后但是生成数独不成功,便会回滚,这个时候我们将不会继续试探后面的相关单元格,因为这个操作不需要所有相关单元格统一,只要发现一个不满足,那就全部回滚,继续往下试探也没有意义了。因此你会发现,每次调用cellMethod方法上面都有一个if语句来判断是不是要执行后面的方法。但对于移除候选数和恢复候选数是不能中途回滚的,所以这里是单独判断处理。
以上就是处理数独单元格的基本方法,在下一篇中我们将讨论最核心的递归方法,如果上面的都能理解,那么后面的就真不难了!