算法设计与分析-《分治法》

一、分治与递归

分治的设计思想是
将一个大问题,分割成一些规模比较小的相同问题,以便各个击破,分而治之。

分治法的流程

  • 分治法产生具有相同性质的较小规模子问题。
  • 反复应用分治手段,可以使子问题规模不断缩小
  • 最终使子问题缩小到很容易直接算出其解
  • 将规模较小的问题的答案逐级向上合并,可得大问题答案。

分治由上到下分解大问题,由下到上合并子问题;这正是递归的过程,所以分治法通常使用递归算法完成。

递归算法的模板

Type abc(参数){
	if(满足边界条件){
		边界条件的处理
	}else{
		非边界条件的处理:
		(1):问题划分为子问题
		(2):对各个子问题递归调用去解决
		(3):合并子问题的解为问题的解
	}
}

例一:阶乘函数
在这里插入图片描述

public int fact(int n){
	if(n==0)return 1;//边界条件
	else return n*fact(n-1)
	//将问题分解成n-1规模的子问题
	//子问题也是个递归调用
	//使用子问题组成问题的解并返回
}

例二:Fibonacci数列

在这里插入图片描述

public int fibonacci(int n){
	if(n<=1) return 1;
	else return fibonacci(n-1)+fibonacci(n-2);
}

上述两个例子也可以使用非递归的方式描述,如下图:
在这里插入图片描述

  • 当问题可以使用非递归的方式解决时,推荐使用非递归的方式去完成。
  • 因为递归解决问题的方式包括一来一回,所以占用的内存空间和时间代价会比较大。
  • 递归的有点在于结构清晰,可读性强,易用数学归纳法来整明算法的正确性。

例三:排列问题
设计一个递归算法生成n个元素 { r 1 , r 2 , ⋯   , r n } \{r_{1},r_{2},\cdots,r_{n}\} {r1,r2,,rn}的全排列。

  • R = { r 1 , r 2 , ⋯   , r n } R = \{r_{1},r_{2},\cdots,r_{n}\} R={r1,r2,,rn} R i = R − { r i } R_{i}=R-\{r_i\} Ri=R{ri}
  • 集合 X X X中的全排列记为 p e r m ( X ) perm(X) perm(X)
  • ( r i ) p e r m ( X ) (r_i)perm(X) (ri)perm(X)表示在全排列perm(X)的每一个排列前加上前缀得到的排列。

该问题的递归定义:

  • 当n==1时, p e r m ( R ) = ( r ) perm(R)=(r) perm(R)=(r),r为集合R中的唯一的元素【边界条件】
  • 当n>1时, p e r m ( R ) perm(R) perm(R) ( r 1 ) p e r m ( R 1 ) , ( r 2 ) p e r m ( R 2 ) , ⋯   , ( r n ) p e r m ( R n ) (r_1)perm(R1),(r_2)perm(R2),\cdots,(r_n)perm(Rn) (r1)perm(R1),(r2)perm(R2),,(rn)perm(Rn)构成【递归函数】

例四:Hanoi塔问题
在这里插入图片描述

递归定义如下:

  • 当n==0,不需要移动
  • 当n>0:
    将A柱上的n-1个圆盘移动到C柱上;再将A最大的圆盘移动到B柱上。
    接下来再将C柱上的n-1个圆盘,通过A柱移动到B柱上
    在这里插入图片描述

假设hanoi(a,b,c,n)表示:
将a上的n个圆盘借助c柱,移动到b柱上。
根据上述的递归定义,可分解成:

  • hanoi(a,c,b,n-1):将a上的n-1个圆盘借助b柱,移动到c柱上
  • 将a柱圆盘移动到b柱
  • hanoi(c,b,a,n-1):将c上的n-1个圆盘借助a柱,移动到b柱上

代码实现:

public class Main{
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		String A="A",B="B",C="C";
		hanoi(A,B,C,3);
	}
	
	public static void hanoi(String A,String B,String C,int n) {
		if(n>0) {
			hanoi(A, C,B, n-1);
			move(A,B);
			hanoi(C,B,A, n-1);
		}
	}
	public static void move(String A,String B) {
		System.out.println(A+"-->"+B);
	}
}

输出结果:

A-->B
A-->C
B-->C
A-->B
C-->A
C-->B
A-->B

二、分治法适应条件与时间复杂度

分治法所能解决的问题一般具有以下几个特征:

  • 该问题的规模缩小到一定程度就可以容易地解决;
  • 该问题可以分解成若干个规模较小的相同问题;(最优子结构性质)
  • 利用该问题分解出的子问题的解可以合并为该问题的解;(最优子结构性质)
  • 该问题所分解出的各个子问题是相互独立的。(无重复子问题)

分治法的基本步骤:

divide-and-conquer(P){
	if(|P|<=n0)adhoc(P);//边界条件,解决小规模问题
	divide P into smaller subinstances P1,P2,……,Pk//分解成若干规模较小的相同问题
	for(i=1;i<=k;i++){
		yi=divide-and-conquer(Pi);//递归求解各子问题
	}
	return merge(y1,y2……,yk);//各子问题的解合并为原问题的解
}

分治法中,将一个问题分成大致相等的k个子问题的处理方式是最有效的,即平衡子问题的思想

分治法的时间复杂度度分析:
分治法将规模为n的问题分解成k个规模为n/m的子问题去解。

  • 当遇到边界条件,即n==1时,规模为1的问题耗费1个时间单位。
  • 将问题分解成k个子问题及用merge将k个子问题的解合并成原问题的解需要f(n)个时间单位。
  • T(n)表示分治法解规模为|P|=n的问题所需要的计算时间

T ( n ) = { O ( 1 ) , n =  1 k T ( n / m ) + f ( n ) , n > 1 T(n)=\begin{cases} O(1), & \text{n\ = \ 1} \\ kT(n/m)+f(n), & \text{n\ >\ 1} \\ \end{cases} T(n)={O(1),kT(n/m)+f(n),n =  1n > 1

即当n>1时,时间复杂度为k个子问题的时间复杂度与合并k个子问题时间复杂度之和。

例一:二分搜索技术
给定按升序排好序的n个元素 a [ 0 : n − 1 ] a[0:n-1] a[0:n1],现要在这n个元素中找出一特定元素x。

分析:

  • 该问题的规模缩小到1时,可以直接给出答案
  • 该问题可以分解成若干个规模较小的子问题。(x要么在左半边,要么在右半边,可分解成在左/右半边寻找的子问题)
  • 分解出的子问题的解可以合并为原问题的解;(子问题的解就是原问题的解)
  • 分解出的各个子问题是相互独立的。

结论:可以使用分治法

思路:
设在a[l:r]中找x,m=(l+r)/2
(0) 如果x==a[m],则找到
(1)如果x<a[m],则在左半边a[l:m]中找x即可
(2)如果x>a[m],则在右半边a[m+1:r]中找x即可
子问题的答案就是大问题的答案

则将一个较大规模的问题分解成了一个较小规模的子问题

代码:

int BiSearch(int a[],int x,int l,int r){
	if(r>=l){
		int m = (l+r)/2;
		if(x==a[m])return m;
		else if(x<a[m]) return BiSearch(a,x,l,m-1);
		else return BiSearch(a,x,m+1,r);
	}else return -1;
}

时间复杂度:
T ( n ) = { O ( 1 ) , n =  1 T ( n / 2 ) + 1 , n > 1 T(n)=\begin{cases} O(1), & \text{n\ = \ 1} \\ T(n/2)+1, & \text{n\ >\ 1} \\ \end{cases} T(n)={O(1),T(n/2)+1,n =  1n > 1
规模为n的问题分解成了原问题规模一半的解,所以分解及解决子问题的时间复杂度为T(n/2);子问题的解就是原问题的解,所以合并子问题的解时间复杂度为1。

三、快速幂算法

给定实数a和正整数n,用分治法设计求 a n a^{n} an的快速算法(递归算法)。

分析

a n = { 0 , a =  0 1 , n =  0 ( a n 2 ) 2 , n >  0,n为偶数 ( a n 2 ) 2 ∗ a , n >  0,n为奇数 a^{n}=\begin{cases} 0, & \text{a\ = \ 0} \\ 1, & \text{n\ = \ 0} \\ (a^{\frac{n}{2}})^{2}, & \text{n\ > \ 0,n为偶数} \\ (a^{\frac{n}{2}})^{2}*a, & \text{n\ > \ 0,n为奇数} \\ \end{cases} an= 0,1,(a2n)2,(a2n)2a,a =  0n =  0n >  0,n为偶数n >  0,n为奇数

该问题满足分治的四个条件,估可使用分治法解决。

代码

public double exp2(double a,int n){
	if(a==0)return 0;
	if(n<=0)return 1;
	else{
		double x = exp2(a,n/2);
		if(n%2==1)return a*x*x;
		else return x*x;
	}
}

四、Strassen矩阵乘法

问题:
两个 n × n n\times n n×n矩阵A和B的乘积矩阵C,C中有 n × n n\times n n×n个元素,C中元素 C [ i , j ] C[i,j] C[i,j]定义为:
在这里插入图片描述
C中的一个元素 C [ i ] [ j ] C[i][j] C[i][j]需要做n次乘法和n-1次加法,因此算出矩阵C的 n × n n\times n n×n个元素所需要的计算时间复杂性为 O ( n 3 ) O(n^3) O(n3).

尝试使用分治法优化该问题:
将矩阵A,B和C中每一矩阵都分块成4个大小相等的子矩阵。由此可将方程C=AB重写为: .
在这里插入图片描述

时间复杂度分析:
T ( n ) = { O ( 1 ) , n =  2 8 T ( n / 2 ) + O ( n 2 ) , n >2 T(n)=\begin{cases} O(1), & \text{n\ = \ 2} \\ \\ 8T(n/2)+O(n^2), & \text{n\ >2} \\ \end{cases} T(n)= O(1),8T(n/2)+O(n2),n =  2n >2

  • T ( n / 2 ) T(n/2) T(n/2):
    将原问题分解成了8个问题规模为 n / 2 n/2 n/2的子问题
  • O ( n 2 ) O(n^2) O(n2):
    C 11 C_{11} C11问题的合并相加需要进行 ( n / 2 ) 2 (n/2)^2 (n/2)2次加法,总的问题合并规模则为 O ( n 2 ) O(n^2) O(n2)

该方法的复杂度依然是 O ( n 3 ) O(n^3) O(n3),没有得到改进。

为了降低复杂度,必须减少乘法的次数。进一步改进算法,如下图:

在这里插入图片描述

复杂度分析:

在这里插入图片描述

在这里插入图片描述


五、合并排序

基本思想:
将待排序元素分成大小大致相同的2个集合,分别对2个子集进行排序,最终将排好序的子集合并成为所要求的排好序的集合。

在这里插入图片描述

实例举例:
在这里插入图片描述

  • 最坏时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
  • 平均时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
  • 辅助空间: O ( n ) O(n) O(n)
  • 稳定的排序算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值