报纸和杂志通常具有以下形式的隐式算术难题:
SEND
+ MORE
--------
MONEY
--------
此处的目标是为每个字母分配一个0到9之间的数字,以便正确计算该算术。规则是,所有出现的字母必须分配一个相同的数字,并且不能将一个数字分配给一个以上的字母。
- 首先,创建所有需要分配以传递给Solve的字符的列表
- 如果分配了所有字符,则解决难题后返回true,否则返回false
- 否则,请考虑第一个未分配的字符
- 用于(在未使用的数字中的所有可能的选择)
做出选择,然后递归尝试分配其余字符
如果递归成功,则返回true
如果成功,请取消分配并尝试输入其他数字 - 如果尝试了所有数字并且没有任何效果,则返回false触发回溯
/* ExhaustiveSolve
* ---------------
* This is the "not-very-smart" version of cryptarithmetic solver. It takes
* the puzzle itself (with the 3 strings for the two addends and sum) and a
* string of letters as yet unassigned. If no more letters to assign
* then we've hit a base-case, if the current letter-to-digit mapping solves
* the puzzle, we're done, otherwise we return false to trigger backtracking
* If we have letters to assign, we take the first letter from that list, and
* try assigning it the digits from 0 to 9 and then recursively working
* through solving puzzle from here. If we manage to make a good assignment
* that works, we've succeeded, else we need to unassign that choice and try
* another digit. This version is easy to write, since it uses a simple
* approach (quite similar to permutations if you think about it) but it is
* not so smart because it doesn't take into account the structure of the
* puzzle constraints (for example, once the two digits for the addends have
* been assigned, there is no reason to try anything other than the correct
* digit for the sum) yet it tries a lot of useless combos regardless
*/
bool ExhaustiveSolve(puzzleT puzzle, string lettersToAssign)
{
if (lettersToAssign.empty()) // no more choices to make
return PuzzleSolved(puzzle); // checks arithmetic to see if works
for (int digit = 0; digit <= 9; digit++) // try all digits
{
if (AssignLetterToDigit(lettersToAssign[0], digit))
{
if (ExhaustiveSolve(puzzle, lettersToAssign.substr(1)))
return true;
UnassignLetterFromDigit(lettersToAssign[0], digit);
}
}
return false; // nothing worked, need to backtrack
}
上面的算法实际上与置换算法有很多共通之处,它几乎只是创建了从字符到数字的映射的所有排列,并尝试每一种直到成功完成一项或全部尝试为止。对于大难题,这可能需要一段时间。
一种更智能的算法可以考虑难题的结构,并避免沿死胡同走下去。例如,如果我们从每个人的位置开始分配字符,然后向左移动,则在每个阶段,我们都可以在继续进行之前验证我们到目前为止所拥有字符的正确性。这肯定会使代码复杂化,但会极大地提高效率,从而使解决大型难题更为可行。
在这种情况下,伪代码下方具有更多特殊情况,但总体设计相同
- 首先检查最上面一行的最右边数字,进位为0
- 如果我们超出了难题的最左位,如果没有进位,则返回true,否则返回false
- 如果我们当前正在尝试在加数之一中分配一个字符
如果已经分配了char,则只需在该行下面的行上重复进行,即可将总和添加值
如果未分配,则- 用于(在未使用的数字中的所有可能的选择)
做出选择,然后在该选择的下一行,如果成功,则返回true
如果成功,请取消分配并尝试输入其他数字 - 如果没有分配触发回溯,则返回false
- 用于(在未使用的数字中的所有可能的选择)
- 否则,如果尝试在总和中分配一个字符
- 如果分配了字符且匹配正确,
在进位左侧的下一列重复,如果成功返回true, - 如果分配的字符不匹配,则返回false
- 如果未分配char和正确的数字,则返回false
- 如果未分配char和未使用正确的数字,
分配它,并在进位的下一列上重复出现,如果成功返回true - 返回false触发回溯
来源:
http://see.stanford.edu/materials/icspacs106b/H19-RecBacktrackExamples.pdf