8.分治、回溯的实现与特性
前言
分治与回溯,其实本质上就是递归,只不过它是递归的其中一个细分类。你可以认为分治和回溯最后就是一种特殊的递归,或者是较为复杂的递归即可。
做这一类题目最后到达效果:碰到一个题目你就找它的重复性。重复性有最近的重复性,或者是有所谓的最优重复性,那么最优重复性就是动态规划。最近重复性是根据重复性怎么构造以及怎么分解就有说明分治,或者是最后要回溯,或者是其它的各种办法。但本质上其实就是一种递归,而本质上的话其实就是找它的重复性。
分治 Divide & Conquer
分治的思想
分治本质就是一种递归,只不过在它的递归的状态树的时候,对一个问题它要化解成好几个子问题。
递归状态树
什么地方不需要化解成子问题?
基本上是没有。
如果非要举个例子,你可以认为之前求阶乘的题目,每次你要求n的阶乘,你就转换成n
再乘以n-1
,它的子问题其实就只有一个。所以它的子问题没有那么多,但它必然也有子问题。
而其它的比如说Fibonacci
数列,比如说爬楼梯、比如说抛硬币问题这种都是分解成多个子问题。所以这些递归其实多多少少都需要分治
为什么很多的问题都需要分治?
其实也很简单,因为它一个大的问题之所以为大问题,肯定是有很多细的子问题组成,不然的话它就不算一个大问题,或者不算一个复杂的问题了。那有人可能会问,那是不是没有这么乘,也有可能假设没有这么乘的话,其实也不需要递归了。所以既然这个问题看似比较复杂,或者是相对来说能进入比较高级的面试,要用高级的算法,那它来说是相对复杂的,要相对复杂的话,那要这么用程序解决。其实就是找重复性,找到重复性之后你会发现一个大问题它肯定是重复的话,那就能分成若干个子问题,最后就变成分治这样一种算法了。这样理解和这样的一个逻辑的话,希望大家让大家可以透过这些现象看到问题的本质,就是重复性。
这样说来的话整个分治就比较好理解了,不需要大家去打开百度百科之类的东西,把分治法或者是像其它的算法书上分治是干嘛干嘛的,或者是什么去背,毫无意义的,我个人觉得。
所以不管是递归、分治或者回溯,或者是其它的办法的话,其实最后本质上就是找重复性以及分解问题和最后组合每个子问题的结果。都是这一个思路。而为什么是这个思路?我们用饿的程序语言都是非常查的语言,它只支持if else for loop recursion
,所以的话最后就变成是它的重复性的一种不管的反复迭代。
分治的解法
假设:有这么一个问题:一个字符串 abcdefgth
一直到 j
,要把它转成大写的字符串怎么做。正常情况下你写一个循环或者调用一个函数,一下子把所有的字符全部都转过去。
分治是什么,先拆分成子问题,每个子问题就是相应每个字符,那么你要做的事情就是对每一个字符都转成大写,然后最后在拼回去,变成一个大写。
分治代码模版
Python
# Python
def divide_conquer(problem, param1, param2, ...):
# recursion terminator
if problem is None:
print_result
return
# prepare data
data = prepare_data(problem)
subproblems = split_problem(problem, data)
# conquer subproblems
subresult1 = self.divide_conquer(subproblems[0], p1, ...)
subresult2 = self.divide_conquer(subproblems[1], p1, ...)
subresult3 = self.divide_conquer(subproblems[2], p1, ...)
…
# process and generate the final result
result = process_result(subresult1, subresult2, subresult3, …)
# revert the current level states
Java
// Java
private static int divide_conquer(Problem problem, ) {
if (problem == NULL) {
int res = process_last_result();
return res;
}
subProblems = split_problem(problem)
res0 = divide_conquer(subProblems[0])
res1 = divide_conquer(subProblems[1])
result = process_result(res0, res1);
return result;
}
C/C++
// C/C++
int divide_conquer(Problem *problem, int params) {
// recursion terminator
if (problem == nullptr) {
process_result
return return_result;
}
// process current problem
subproblems = split_problem(problem, data)
subresult1 = divide_conquer(subproblem[0], p1)
subresult2 = divide_conquer(subproblem[1], p1)
subresult3 = divide_conquer(subproblem[2], p1)
...
// merge
result = process_result(subresult1, subresult2, subresult3)
// revert the current level status
return 0;
}
JavaScript
// JavaScript
const divide_conquer = (problem, params) => {
// recursion terminator
if (problem == null) {
process_result
return
}
// process current problem
subproblems = split_problem(problem, data)
subresult1 = divide_conquer(subproblem[0], p1)
subresult2 = divide_conquer(subproblem[1], p1)
subresult3 = divide_conquer(subproblem[2], p1)
...
// merge
result = process_result(subresult1, subresult2, subresult3)
// revert the current level status
}
回溯 Backtracking
回溯,我个人觉得:你就把它理解成一种递归的情况,或者递归的一种问题。
百度百科解释:
回溯法采用试错的思想,它尝试分布的去解决一个问题。在分布解决问题的过程中,当它通过尝试发现有的分布答案不能得到有效的正确的解答的时候,它将取消上一步甚至上几步的计算,再通过其它的可能的分布解答再次尝试寻找问题的答案。
回溯法通常最简单的递归方法实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确答案;
- 在尝试了所有可能的分步方法后宣告该问题没有答案。
在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。
高频题目
部分图片来源于网络,版权归原作者,侵删。