一、基本概念
字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)
二、基本思想及策略
分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
三、分治法适用的情况
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
四、分治法的基本步骤
分治法在每一层递归上都有三个步骤:
1.分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
2.解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
== 3.合并:将各个子问题的解合并为原问题的 。==
它的一般的算法设计模式如下:
Divide-and-Conquer(P){
if |P| ≤ N{
then return(ADHOC(P))
}else{
将P分解为较小的子问题 P1 ,P2 ,...,Pk
for i←1 to k
do yi ← Divide-and-Conquer(Pi) // 递归解决Pi
T ← MERGE(y1,y2,...,yk) // 合并子问题
}
return(T)
}
其中|P|表示问题P的规模;
N为一阈值,表示当问题P的规模不超过N时,问题已容易直接解出,不必再继续分解。
ADHOC§是该分治法中的基本子算法,用于直接解小规模的问题P。
因此,当P的规模不超过N时直接用算法ADHOC§求解。
算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。
五、分治法的复杂性分析
一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值N=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:T(n)= k T(n/m)+f(n)
通过迭代法求得方程的解:
递归方程及其解只给出n等于m的方幂时T(n)的值,但是如果认为T(n)足够平滑,那么由n等于m的方幂时T(n)的值可以估计T(n)的增长速度。通常假定T(n)是单调上升的,从而当mi≤n<mi+1时,T(mi)≤T(n)<T(mi+1)。
六、依据分治法设计程序时的思维过程
实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。
1、一定是先找到最小问题规模时的求解方法
2、然后考虑随着问题规模增大时的求解方法
3、找到求解的递归函数式后(各种规模或因子),设计递归程序即可。
七、可使用分治法求解的一些经典问题
(1)二分搜索
(2)大整数乘法
(3)Strassen矩阵乘法
(4)棋盘覆盖
(5)合并排序
(6)快速排序
(7)线性时间选择
(8)最接近点对问题
(9)循环赛日程表
(10)汉诺塔
(1)二分搜索
public static int binSearch(int[] arr,int k,int left,int right){
if(left<=right) {
int mid = (right + left)/2;
if(arr[mid] == k){
return mid;
}else if(arr[mid] < k){
return binSearch(arr,k,mid+1,right);
}else{
return binSearch(arr,k,left,mid);
}
}
return -1;
}
(2)大整数乘法
大整数相乘:A、B两个整数,A有n位(123456……n),B有m位(123456……m),求A*B的结果
我们把整数A由规模n分为n1和n2,把整数B由规模m分为m1和m2,如下图:
则A分为n1位的A1和n2位,B分为m1位的B1和m2位的B2,如下式所示:
以此类推,我们可以把A1、A2、B1、B2继续划分,直至最小单位。(这里在编程时需要用递归来实现)
那么A和B的相乘就可以表示为:
现在是要计算四个大整数相乘,我们可以通过变换使得上式变成三个大整数相乘,如下式:
import java.math.BigInteger;
import java.util.Scanner;
import org.stone.stack.MyBigAdd;
/**
* @ClassName_MyBigIntegerMutiply
* @author_Stone6762
* @CreationTime_2016年10月22日 下午3:39:57
* @Description_ 大整数乘法A x B AB可以为不同的位数
* 采用的是: 分治的思想,二分
*/
public class MyBigIntegerMutiply1 {
/**
* @Description:未优化的大数相乘
* @param a
* @param b
* @return a*b={a1*10^(n1/2)+a0}*{b1*10^(n2/2)+b0}
*/
public static String Mutiply1(String a, String b)// 用字符串读入2个大整数
{
String result = "";
if (a.length() == 1 || b.length() == 1)// 递归结束的条件
//其中一个长度为1,另一个不一定
result = "" + Long.valueOf(a) * Long.valueOf(b);
else// 如果2个字符串的长度都 >= 2
{
//1.分割成 a1 a0 b1 b0
int lengthA0 = a.length() / 2;
int lengthA1=a.length()-lengthA0;
String a1 = a.substring(0, lengthA1); // 截取前一半的字符串(较短的一半)
String a0 = a.substring(lengthA1, a.length()); // 截取后一半的字符串
int lengthB0 = b.length() / 2;
int lengthB1=b.length()-lengthB0;
String b1 = b.substring(0, lengthB1);
String b0 = b.substring(lengthB1, b.length());
// * a*b=
// * (a1*b1)* 10^[(n1+n2)/2 ]
// * +(a1*b0)*10^(n1/2)
// * +(a0*b1)*10^(n2/2)
// * +(a0*b0)
//2.计算展开式中的乘法
String a1b1 = Mutiply1(a1, b1);
String a1b0 = Mutiply1(a1, b0);
String a0b1 = Mutiply1(a0, b1);
String a0b0 = Mutiply1(a0, b0);
//3.模拟移位
String resulta1b1 = a1b1;
for (int i = 0; i < lengthA0+lengthB0; i++) {
resulta1b1 += "0";
}
String resulta1b0 = a1b0;
for (int i = 0; i <lengthA0; i++) {
resulta1b0 += "0";
}
String resulta0b1 = a0b1;
for (int i = 0; i < lengthB0; i++) {
resulta0b1 += "0";
}
//4.大数相加
result = MyBigAdd.add(resulta1b1, resulta1b0);
result = MyBigAdd.add(result, resulta0b1);
result = MyBigAdd.add(result, a0b0);
}
return result;
}
/**
* @Description拿BigInteger自身大数相乘来判断自身算法的正确与否
* @param args
*/
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
String aString=scan.next();
String bString=scan.next();
BigInteger aBigInteger=new BigInteger(aString);
BigInteger bBigInteger=new BigInteger(bString);
String reslut1=aBigInteger.multiply(bBigInteger).toString();
String result2=Mutiply1(aString, bString);
System.out.println("标准答案: "+reslut1);
System.out.println("计算结果: "+result2);
System.out.println("结果是否正确: "+reslut1.equals(result2));
}
}
}
(3)汉诺塔问题
public class Hanoi {
//使用递归法求解含有n个不同大小盘子的汉诺塔移动路径,参数n为盘子数,把A塔上盘子全部移动到C塔上,B为过渡塔
public static void recursionHanoi(int n,char A,char B,char C){
if(n == 1){
System.out.print(A+"——>"+C+"\n");
}else{
recursionHanoi(n-1,A,C,B); //使用递归先把A塔最上面的n-1个盘子移动到B塔上,C为过渡塔
System.out.print(A+"——>"+C+"\n"); //把A塔中底下最大的圆盘,移动到C塔上
recursionHanoi(n-1,B,A,C); //使用递归把B塔上n-1个盘子移动到C塔上,A为过渡塔
}
}
public static void main(String[] args){
System.out.println("请输入盘子总数n:");
Scanner in = new Scanner(System.in);
int n = in.nextInt();
recursionHanoi(n,'A','B','C');
}
}