可以分为三个步骤:
1.分解:将问题分解为若干个相互独立,规模较小,与原问题形式相同的子问题,确保每个小问题的解法结构,形式结构相同。
2.解决:若可以对子问题进行解决,则进行对子问题的解决,若不可以,则继续拆分,可能是一个递归的过程。
3.合并
递归过程的伪代码:
T DivideAndConquer(P){
if(P 可以直接进行解决){
T<--P的解决结果
return T
}
将P分解为子问题{P1,P2,P3.......Pn}
for_each(Pi:{P1,P2,P3.......Pn}){
ti<--DivideAndConquer(Pi) //递归解决子问题Pi
}
T<--Merge(t1,t2.....tn) //合并子问题的解
returnT
}
能使用分治法解决的问题一般都具有两个显著的特点,
第一个特点是
问题可以分解为若干个规模较小的相同问题,并且这个分解关系可以用递归或递推的方式逐级分解,直到问题的规模小到可以直接求解的程度。这里说的相同问题,并不是说分解后的子问题与原问题完全一样,这里说的相同只是问题的结构相同,比如原问题有四个属性,分解后规模较小的子问题也应该具有四个相同的属性,不同的只是各个属性的范围和规模。
第二个特点是
子问题的解可以用某种方式合并出原始问题的解。这很容易理解,如果不能合并出原始问题的解,那么子问题的划分和求解就没有意义了。
分治法的难点是如何将子问题分解,并且将子问题的解合并出原始问题的解,针对不同的问题,通常有不同的分解与合并方式。先来看看快速排序算法,快速排序算法的分解思想是选择一个标兵数,将待排序的序列分成两个子序列,其中一个子序列中的数都小于标兵数,另一个子序列中的数都大于标兵数,然后分别对这两个子序列排序,其合并思想就是将两个已经排序的子序列一前一后拼接在标兵数前后,组成一个完整的有序序列
分治法的例子:字符串全排列问题
我们的问题是:给定一个没有重复字母的字符串,输出该字符串中字符的所有排列。假如给定的字符串是“abc”,则应该输出“abc”、“acb”、“bac”、“bca”、“cab”和“cba”六种结果。
首先分析这是一个全排列问题,解决这个问题我们的常用策略是每次选择固定一个字符,然后对剩下的两个字符进行排列。比如这个三个字母的字符串,我们首先选择固定 a,然后对 bc 进行排列,可以得到“abc”和“acb”两个结果;然后选择固定 b,对 ac 进行排列,可以得到“bac”和“bca”两个结果;最后选择固定 c,对 ab 排列,可以得到“cab”和“cba”两个结果。
不知道大家有没有意识到,这其实就是使用了分治法的思想在解决问题。三个字符排列,我们人脑可能处理不过来,但是我们固定一个字母后,把问题的规模减小为两个字符的排列,两个字符的排列只有两种结果,是可以解决的问题;然后我们将小问题的结果与固定的字母组合在一起,就可以得到原始问题,即三个字符的排列结果。分治法分解子问题,并不是一定要用某种方式均匀分解原始问题,哪怕是每次只能将原始问题的规模变小一点,也是一种分解子问题的方法。
回到我们这个问题上,对字符串类问题分解子问题,通常考虑的方法有两个。
-一个方法是用字符串的开始位置和字符串的长度表示一个子字符串,对于一个长度为 n 的字符串,用这种方法定义的子问题就是“从位置 i 开始,长度为 m 的字符串,原始问题就是从位置 1 开始,长度为 n 的字符串。
-另一个方法是用字符串的位置区间来表示一个子字符串,同样对于一个长度为 n 的字符串,用这种方法定义的子问题就是“从位置 i 开始,到位置 j 结束的字符串,原始问题就是从位置 1 开始到位置 n 结束的字符串。
对于这个问题,我们选择用区间的方法定义子问题,即用字符位置索引区间 [begin, end] 表示子问题,选好子问题的表达方式,接下来就要考虑如何分解子问题。根据之前的分析,我们采用每次固定一个字符,然后将剩下的字符串作为一个子问题进行全排列的方式分解子问题。因为每个字符都要被“固定”一次,所以算法实现的方法是用一个循环对子问题 [begin, end] 区间上的每个字符都选择一次。
我们采用的方法是将问题区间 [begin, end] 中的 begin 位置作为选中的固定字符位置,将除了这个位置之外的问题区间 [begin+1, end] 作为子问题进一步处理。如果被选中的固定字符不在 begin 位置,则交换两个字符的位置,使得被选中的固定字符位于 begin 位置。
解决了子问题的分解,接下来要考虑子问题的求解。分解的目的是为了减小问题的规模,直到问题能够求解,对于这个字符串排列问题,当子问题的规模减小到只有一个字符的时候,子问题就可以求解了。因为我们处理方式是从前向后,每次固定 begin 位置的字符,然后将区间 [begin+1, end] 作为子问题进一步处理,所以当 begin 位置和 end 位置相同的时候,就说明字符串只有一个字符了,这时就不需要再分解子问题了。
void swap (String[] std,int begin,int i){
if (begin!= i)
{
int tmp = std[begin];
std[begin] = std[i];
std[i] = tmp;
}
}
void solution(String[] std,int begin,int end)
{
if(begin==end){
syso(std)
}
for(int i=begin;i<=end;i++){
swap(std,begin,i);//把第 i 个字符换到 begin 位置
solution(std,begin+1,end);//子问题
swap(std,begin,i);//在挑选下一个固定字符之前,需要换回来
}
}
二分查找
//循环实现二分算法
public static int binSearch_1(int key, int[] array) {
int low = 0; //第一个下标
int high = array.length - 1;//最后一个下标
int middle = 0; //防越界
if (key < array[low] || key > array[high] || low > high) {
return -1;
} while (low <= high) {
middle = (low + high) / 2;
if (middle == key) {
return array[middle];
} else if (middle < key) {
low = middle + 1;
} else {
high = middle - 1;
}
}
return -1;
}
/* *递归实现二分算法 */
public static int binSearch_2(int key,int[] array,int low,int high){
//防越界
if (key < array[low] || key > array[high] || low > high) {
return -1;
}
int middle = (low+high)/2;
if(array[middle]>key){ //大于关键字
return binSearch_2(key,array,low,middle-1);
}else if(array[middle]<key){ //小于关键字
return binSearch_2(key,array,middle+1,high);
}else{
return array[middle];
}
}
}
二分查找中中间值的计算:
这是一个经典的话题,如何计算二分查找中的中值?大家一般给出了两种计算方法:
算法一: mid = (low + high) / 2
算法二: mid = low + (high – low)/2
乍看起来,算法一简洁,算法二提取之后,跟算法一没有什么区别。但是实际上,区别是存在的。算法一的做法,在极端情况下,(low + high)存在着溢出的风险,进而得到错误的mid结果,导致程序错误。而算法二能够保证计算出来的mid,一定大于low,小于high,不存在溢出的问题。
网友评论