[leetcode] sudoku solver:暴力还是优化
2013-06-19 01:28
239 查看
1. backtracking
Sudoku是典型的backtracking问题,有关backtracking的问题《The Algorithm Design Manual》 7.1章解释的最详细易懂。Backtracking的定义如下:Backtracking is a systemic way to iterate through all the possible configurations of a search space.
简而言之,backtracking就是通过遍历所有组合,并从中找出符合条件的结果集的一种方法。因此,一种非常直观的backtracking算法可以描述如下:
// array: 保存了[0, step-1]每一步的选择 // step: 当前是第几步 backtracking(array, step) { // 判断前step步的选择是否可以构成一个结果 if (is_a_solution(array, step)) { process_solution(array, step); } else { step++; // 获取下一步的所有选择 condidates = construct_candidates(array, step); foreach c in condidates { // 对每一种选择,继续递归下去 array[step] = c; make_move(array, step); backtracking(array, step); unmake_move(array, step); } } }
其中重要函数有这样几个:
is_a_solution:判断当前组合是否是一个期望的结果
process_solution:处理结果,例如打印出来
construct_candidates:获取下一步的所有选择。注意,这里并没有说明如何选择下一步该如何进行,大多数情况下,这个函数还应当有选择下一步的功能
make_move:向前进一步。通常这意味着在array中填充这一步的所选择的的值
unmake_move:向后退一步。通常这意味着在array中将这一步的值清空
对于数独问题,套用上面的算法,大概步骤如下:
#define DIMENSION 3; bool solve(vector<vector<char> > &board, int step) { // 1. is_a_solution if (step == DIMENSION*DIMENSION*9) return true; // 2. get next square and its all candidates int row = 0, col = 0; // position to move next, start from 0; set<char> possible_values; get_next_square(board, row, col, possible_values, step); for (set<char>::iterator it = possible_values.begin(); it != possible_values.end(); ++it) { board[row][col] = *it; // make_move if (solve(board, step+1)) return true; board[row][col] = EMPTY; // unmake_move } return false; }
2. 如何选择下一步
backtracking主要有两种应用:
获取所有组合。典型问题:Letter Combinations of a Phone Number:给出电话号码,求电话号码对应的所有字符串
Generate Parentheses:求所有n对括号"()"组成的字符串
Combination Sum:给定一组数字和一个target值,求所有和等于target的组合(组合中每个数字可以出现多次)
Combination Sum2:和Combination Sum问题一样,区别是组合中每个数字只能出现一次
Permutations:求一组数字的全排列
Permutations2:和Permutations问题一样,区别是给定的一组数字有重复,并且要求结果集中不能有重复的组合
从所有的组合中找出符合条件的结果集。典型问题:
Sudoku Solver:数独解
第一类问题需要遍历所有解,有些特殊的情况无非是结果集中需要去重,这些都可以通过精细地选择”下一步的值“来做到。例如,在每一步中,可以对”这一步可选的值“做排序,相同的值只选一次,这样可以解决绝大多数”结果集去重“问题(例如Combination Sum2和Permutations2)
第二类问题与第一类问题有着根本不同。第二类问题可以在遍历一组组合的过程中,如果发现当前的组合已经不可能满足条件,则无需遍历完,即可在中途丢提当前的组合,直接跳到下一种组合。
考虑数独问题,首先,如果我们在构造一个组合的过程中,发现某个格子填入任何值都不可能满足条件,那么当前的组合无需再计算下去,必然是之前某些步出错。无需再计算当前还没有填充的其他格子的值,直接丢弃当前解,跳到上一步尝试其他值即可。
说道这里,其实还有一个最关键的问题没有细说,那就是如何选择下一步?
例如sudoku问题,最直观的做法是随机选择一个还是空白的格子,还能再优化吗?考虑这样的情况:假设当前空白的格子中,有一个格子有5种可能的值,有一个格子只有1个可能的值,那么应当先选择哪个格子?显然,选择只有1个可能值的格子更好。填充了这个格子,能够减少其他未填充格子的可选择值,也就降低了unmake_move的次数。
但是这样一定比随机选择更快吗?细心的读者能够发现,这样的选择方式,在每次选择下一步的时候,会花费相当的时间去查找”可选择值最少“的空白格子。每一步,我们对所有空白格子,计算它们的可选择值;计算可选择值的过程是查看当前行、当前列、当前9格。其实,这和随机选择一样,最后都会得到时间代价O(n^4)的算法。
3. 更多的优化
其实这里还有继续优化的空间,这里不详细展开,只说一下大概的思路。使用数组保留每个空白格子的可选值。每当有空白格被填入了数字,重新计算受影响的空白格的可选值(当前行、当前列、当前9格)
不那么严格的选择。例如,我们可以只计算每个9格中的空白格的数量,从空白格最少的9格中,随机选出一个空白格,作为下一步要填充的格子。这是一种不那么严格的选择,好处是每个9格的空白格数量可以快速地计算出,同时保证了unmake_move的次数比随机选择要大大减少。
4. 暴力破解 VS 精细选择
每一步随机选择一个空白格,这其实就是一种暴力破解的方法,这里还有另一种暴力破解的方法,经过计算就会发现,其实两者的算法复杂度基本相当。前文提到的精细选择,如果精细选择的过程没有优化,算法的复杂度其实没有变化,有兴趣的同学可以自己证明和验证。相关文章推荐
- leetcode 493. Reverse Pairs 归并排序统计逆序对数量 + 这个我估计是做不出来的,还是直接暴力吧
- Leetcode: Sudoku Solver
- 优化MySQL,还是使用缓存?
- leetcode 149. Max Points on a Line 计算斜率的问题 + 直接暴力求解即可
- leetcode 587. Erect the Fence 最短围栏 + 凸包优化
- LeetCode 36 Sudoku Solver
- 该暴力的时候还是要暴力滴!
- leetCode Sudoku Solver
- LeetCode-65-Valid Number 脑残暴力
- BSOJ 4852:比赛 暴力优化
- 查询优化里, 是按CPU,查还是按Duration 查,哪个更好?
- leetcode第一刷_Sudoku Solver
- LeetCode - Sudoku Solver
- [LeetCode] Sudoku Solver
- 算法分析与设计课程(8):【leetcode】Sudoku Solver
- LeetCode-Sudoku Solver
- leetcode 328. Odd Even Linked List 奇偶序列的调整 + 暴力做法真棒
- SQL优化工作, 不能太激动。记录失败的优化经历,优化从 70分钟优化到 30秒, 再到1s但还是失败了
- codeforces 716D. Complete The Graph 堆优化dij+暴力
- leetcode 459. Repeated Substring Pattern 暴力拆分即可