1-3、算法设计常用思想之分治法
文章内容来自王晓华老师
参考书籍 算法的乐趣
分治法的设计思想是将无法着手解决的大问题分解成一系列规模较小的相同问题,然后逐个解决小问题,即所谓分而治之。
分治法产生的子问题与原始问题相同,只是规模减小,反复使用分治方法,可以使得子问题的规模不断减小,直到能够被直接求解为止。
应用分治法,一般出于两个目的,其一是通过分解问题,使得无法着手解决的大问题变成容易解决的小问题,其二是通过减小问题的规模,降低解决问题的复杂度(或计算量)。
在很多情况下,分治法都会使用递归的方式对问题逐级分解,但是在每个子问题的层面上,分治法基本上可以归纳为三个步骤。
• 分解:将问题分解为若干个规模较小,相互独立且与原问题形式相同的子问题,确保各个子问题的解具有相同的子结构。
• 解决:如果上一步分解得到的子问题可以解决,则解决这些子问题,否则,对每个子问题使用和上一步相同的方法再次分解,然后求解分解后的子问题,这个过程可能是一个递归的过程。
• 合并:将上一步解决的各个子问题的解通过某种规则合并起来,得到原问题的解。
采用递归方式的算法模式可以用伪代码描述为: T DivideAndConquer(P) { if(P 可以直接解决) { T <- P 的结果; return T; } 将 P 分解为子问题{P1, P2,..., Pn}; for_each(Pi : {P1, P2,..., Pn}) { ti <- DivideAndConquer(Pi); //递归解决子问题 Pi } T <- Merge(t1, t2,...,tn); //合并子问题的解 return T; }
分治法最难也最灵活的部分就是对问题的分解和结果的合并,对于一个未知的问题,只要能找到对子问题的分解方式和结果的合并方式,应用分治法就可以迎刃而解。
而在数学上,只要能用数学归纳法证明的问题,一般也都可以应用分治法解决,这也是一个应用分治法的强烈信号。
以快速排序为例,如果把待排序的序列作为问题的话,那么子问题的规模就可以定义为子序列在原始序列中的起始位置。
对此一般化之后,原始问题和子问题的描述就统一了,都是原始序列 + 起始位置,原始问题的起始位置就是 [1,n],子问题的起始位置就是 [1,n] 中的某一个子区间,
由此一来,递归的接口就明确了:
void quick_sort(int *arElem, int p, int r) { if(p < r) { int mid = partion(arElem, p, r); quick_sort(arElem, p, mid - 1); quick_sort(arElem, mid + 1, r); } } int intArray[] = {12, 56, 22, 78, 102, 6, 90, 57, 29}; quick_sort(0, 8); //原始问题:对数组中的1-9号元素排序
分治法的例子:字符串全排列问题
我们的问题是:
给定一个没有重复字母的字符串,输出该字符串中字符的所有排列。(解决这个问题我们的常用策略是每次选择固定一个字符,然后对剩下的两个字符进行排列。)
• 一个方法是用字符串的开始位置和字符串的长度表示一个子字符串,对于一个长度为 n 的字符串,用这种方法定义的子问题就是“从位置 i 开始,长度为 m 的字符串,其中, 1⩽i<n1⩽i<n,0<m⩽n0<m⩽n”,原始问题就是从位置 1 开始,长度为 n 的字符串。
• 另一个方法是用字符串的位置区间来表示一个子字符串,同样对于一个长度为 n 的字符串,用这种方法定义的子问题就是“从位置 i 开始,到位置 j 结束的字符串,其中,1⩽i<n,i⩽j⩽n1⩽i<n,i⩽j⩽n”,原始问题就是从位置 1 开始到位置 n 结束的字符串。考虑到很多编程语言中索引位置都是从 0 开始,上述描述中的索引位置要做 -1 修正,读者应该能够理解,接下来的例子用 lua 实现算法,就会体现这一点。
-- lua实现的字符串全排列问题
local str = "223456" local list = {} for i = 1, #str do list[i] = string.sub (str, i, i) end function swap(str_list, pos1, pos2) if (pos1 ~= pos2) then local tmp = str_list[pos1]; str_list[pos1] = str_list[pos2]; str_list[pos2] = tmp; end end function Permutation(str_list, begin_pos, end_pos) if (begin_pos == end_pos) then --就剩一个字符了,不需要排列了,直接输出当前的结果 local str = "" for k, v in next, str_list do str = str .. v end print("===str", str) end for i = begin_pos, end_pos do swap(str_list, begin_pos, i) Permutation(str_list, begin_pos + 1, end_pos) swap(str_list, begin_pos, i) end end Permutation(list, 1, #list)
分治法有很多典型的应用,比如二分查找、Karatsuba 大整数乘法、棋盘覆盖问题、快速排序、合并排序等等
附Lua实现的二分查找 local arr = {1,3,5,9,13,19,35,40,45} function search(list, begin_pos, end_pos, val) if (begin_pos > end_pos) then return false end local mid = math.floor((end_pos + begin_pos) / 2) if(list[mid] == val) then return mid elseif (list[mid] < val) then begin_pos = mid + 1 return search(list, begin_pos, end_pos, val) else end_pos = mid - 1 return search(list, begin_pos, end_pos, val) end end print(search(arr, 1, #arr, 19))