分治算法详细讲解(含经典例题分析)

分治法思路:

将整个问题分解成若干小问题后再分而治之。如果分解得到的子问题相对来说还是太大,则可反复使用分治策略将这些子问题分成更小的同类型子问题,直至产生方便求解的子问题,必要时逐步合并这些子问题的解,从而得到问题的解。
分治算法可以由递归过程来表示,因为分治法就是一种找大规模问题与小规模问题关系的方法,是递归设计的一种具体策略。

步骤

1.分解
将原问题分解为若干规模较小,相互独立,与原问题相同的子问题。
2.解决
若干子问题较小而容易被解决则直接解决,否则再继续分解为更小的子问题,直到容易解决。
3.合并
将已求解的各个子问题的解,逐步合并为原问题的解。

有的问题分解后不需要合并子问题的解,此时就不需要再做第3步了。多数问题需要子问题的解,按照题意使用恰当的方法合并成为整个问题的解。需要具体问题具体分析。

算法框架

分治法的一般算法设计模式如下:

Divide-and-Conquer(int n){
     
	if(n<=n0){
     //n为问题规模 ,n0为可解子问题的规模
		解子问题;
		return(子问题的解);
	}
	for(i=1;i<=k;i++){
     //分解成较小的子问题p1,p2,...,pk
		yi=Divide-and-Conquer(|Pi|);//递归解决
	}
	T=MERGE(y1,y2,...yk);//合并子问题
	return(T);//返回问题的解
}

典型二分法

(一)

在算法设计中,每次一个问题分解成的子问题个数一般是固定的,每个子问题的规模也是平均分配的。当每次都将问题分解为原问题规模的一半时,称为二分法。
二分法是分治法较常用的分解策略,数据结构课程中的折半查找、归并排序等算法都是采用此策略实现的。

问题:金块问题

老板有一袋金块(共n块),最优秀的雇员得到其中最重的一块,最差的雇员得到其中最轻的一块,假设有一台比较重量的仪器,我们希望用最少的比较次数找出最重的金块。

方法1:逐个查找比较

最简单的方就是逐个的比较查找,算法类似于选择排序。先拿两块进行比较,留下最重的与下一块比较,直到全部比较完毕就找到了最重的金子

完整代码如下:

//金块算法1:逐个查找比较
#include
int main(){
     
	void maxmin(float a[],int n);//定义查找比较的函数
	float a[5]={
     5,3,1,9,0};
	int i;
	maxmin(a,5);
	return 0;
} 
void maxmin(float a[],int n){
     
	float max,min;
	int i;
	int count=0;
	max=min=a[0];
	for(i=1;i<n;i++){
     
		if(max<a[i]) {
     //记录每次比较重量最大的金块
			max=a[i];
			count++;
		}
		if(min>a[i]){
     //记录每次比较重量最小的金块
			min=a[i];
			count++;
		} 
	}
	printf("max=%.2f\nmin=%.2f\ncount=%d\n",max,min,count);//输出结果
}
方法2:分治法(二分法)

1.将数据等分为两组(两组数据可能差1),目的是分别选取其中最大值(最小值)。
2.递归分解直到每组元素的个数不大于2,可简单的找到最大值(最小值)。
3.回溯时将分解的两组解大者取大,小者取小

用分治法可以用较少的比较次数解决问题。

完整代码如下:

//金块算法2:运用递归实现分半查询 
#include
float a[5]={
     3,6,9,2,5};
int main(){
     
	void maxmin(int i,int j,float &fmax,float &fmin,int &count);//fmax的地址,fmin的地址 
	float fmax=0,fmin=0;
	int count=0;
	maxmin(0,4,fmax,fmin,count);
	//传递地址是为了能够把值带出来 
	printf("fmax=%f\nfmin=%f\ncount=%d\n",fmax,fmin,count); 
	return 0;
}
void maxmin(int i,int j,float &fmax,float &fmin,int &count){
     
	int mid;
	int k;
	float lmax,lmin,rmax,rmin;
	count++;
	//递归结束条件(子问题的解)
	//当分到只剩一个数
	if(i==j){
     
		fmax=a[i];fmin=a[i];
	} 
	else if(i==j-1){
     //(i,j为下标,在分解过程中,i总在左,j总在右 )假如只剩两个数 
		if(a[i]>a[j]){
     
			fmax=a[i];
			fmin=a[j];
		} 
		else if(a[j]>a[i]){
     
			fmax=a[j];
			fmin=a[i];
		}
	}
	else{
     //其他情况(还剩很多数),则继续递归分解 
		mid=(i+j)/2;//继续分半
		maxmin(i,mid,lmax,lmin);
		maxmin(mid+1,j,rmax,rmin);
		if(lmax>rmax) fmax=lmax;
		else fmax=rmax;
		if(lmin<rmin) fmin=rmin;
		else fmin=rmin;
	}
}

(二)二分法不相似情况

需要构造与原问题相似子问题。

问题:残缺棋盘
残缺棋盘是一个有2k*2k(k>0)个方格的棋盘,其中恰有一个方格残缺,其中残缺的方格用蓝色阴影表示。

①号
分治算法详细讲解(含经典例题分析)_第1张图片
②号
分治算法详细讲解(含经典例题分析)_第2张图片
③号
分治算法详细讲解(含经典例题分析)_第3张图片
④号
分治算法详细讲解(含经典例题分析)_第4张图片
这样的棋盘我们成为“三格板”,残缺棋盘问题就是要用这四种三格板覆盖更大的残缺棋盘。在此覆盖中要求:
(1)两个三格板不能重叠。
(2)三格板不能覆盖残缺方格,但必须要覆盖其他所有的方格。

解决:
以k=2为例(此时共有16个小方格),用二分法进行分解,得到的是由4个k=1的棋盘组成。
分治算法详细讲解(含经典例题分析)_第5张图片

注意这四个棋盘,并不都是与原问题相似的子问题,只有残缺方格在左上角的棋盘时,左上角1号子棋盘与原问题相似。
既然不是与原问题相似的子问题,自然不能够独立求解,所以我们要用巧妙的方法,将2,3,4号子棋盘也转化成与原问题相似的子问题。
分治算法详细讲解(含经典例题分析)_第6张图片
我们把紫色阴影覆盖的方格,也看作是残缺方格(我们称之为“伪”残缺方格),这样,每一个子残缺棋盘就都转化为与原问题相似的子问题了。
这样处理过后:
1号子棋盘可以用③号三格板覆盖
2号子棋盘可以用③号三格板覆盖
3号子棋盘可以用②号三格板覆盖
4号子棋盘可以用①号三格板覆盖

这样就覆盖了所有棋盘,满足题意。
如下图所示,粉色阴影是用题中给出的三格板覆盖的位置。
分治算法详细讲解(含经典例题分析)_第7张图片
解决了k=2的情况,相当于解决了所有的情况,因为分治算法就是把大规模问题逐步分解成独立且相似的子问题。
思路清晰了之后,我们来解决一般情况

代码分析:
1.数据结构设计 :我们用一个二维数组Board[][]来模拟棋盘,覆盖残缺棋盘所需要的三格板数目为(size2-1)/3
(size为棋盘的行数或列数)
2.棋盘识别
我们用左上角方格所在行和列来唯一确定一个棋盘,残缺方格或“伪”残缺方格用行号和列号记录。
tr:棋盘左上角方格所在行
td:棋盘左上角方格所在列
dr:残缺方块所在行
dl:残缺方块所在列
3.棋盘填充
将三格板编号1-(size
2-1)/3,将每个方格板的编号填充在代表棋盘的数组中,以这种方式表示棋盘被方格板覆盖。
完整代码如下:

#include
using namespace std;
int count=0;
int size=1;
int Board[8][8];
int main(){
     
	void Cover(int tr,int tc,int dr,int dc,int size);//填充棋盘的函数 
	void OutputBoard(int size);
	int x,y;
	int i;
	int k;
	//输入棋盘规模
	cout<<"请确认棋盘规模,输入k大小(2k×2k):"<<endl ;
	cin>>k;
	for(i=1;i<=k;i++) size=size*2;
	cout<<"请输入残缺棋子位置(行号,列号):"<<endl;
	cin>>x>>y;
	Cover(0,0,x,y,size);
	OutputBoard(size); 	
} 
	void Cover(int tr,int tc,int dr,int dc,int size){
     
		if(size<2) return;
		int t=count++;
		int s=size/2;
		//如果残缺在左上角
		if(dr<tr+s&&dc<tc+s){
     
			Cover(tr,tc,dr,dc,s);
			Board[tr+s-1][tc+s]=t;
			Board[tr+s][tc+s-1]=t;
			Board[tr+s][tc+s]=t;
			//返回,填充其他部分 
			Cover(tr,tc+s,tr+s-1,tc+s,s);
			Cover(tr+s,tc,tr+s,tc+s-1,s);
			Cover(tr+s,tc+s,tr+s,tc+s,s);
		}
		//右上角 
		else if(dr<tr+s&&dc>=tc+s) {
     
			Cover(tr,tc+s,dr,dc,s);
			Board[tr+s-1][tc+s-1]=t;
			Board[tr+s][tc+s-1]=t;
			Board[tr+s][tc+s]=t;
			//返回,填充其他部分 
			Cover(tr,tc,tr+s-1,tc+s-1,s);
			Cover(tr+s,tc,tr+s,tc+s-1,s);
			Cover(tr+s,tc+s,tr+s,tc+s,s);
		}
		else if(dr>=tr+s&&dc<tc+s){
     
			Cover(tr+s,tc,dr,dc,s);
			Board[tr+s-1][tc+s-1]=t;
			Board[tr+s-1][tc+s]=t;
			Board[tr+s][tc+s]=t;
			//返回,填充其他部分 
			Cover(tr,tc,tr+s-1,tc+s-1,s);
			Cover(tr,tc+s,tr+s-1,tc+s,s);
			Cover(tr+s,tc+s,tr+s,tc+s,s);
		}
		else if(dr>=tr+s&&dc>=tc+s){
     
			Cover(tr+s,tc+s,dr,dc,s);
			Board[tr+s-1][tc+s-1]=t;
			Board[tr+s-1][tc+s]=t;
			Board[tr+s][tc+s-1]=t;
			//返回,填充其他部分 
			Cover(tr,tc,tr+s-1,tc+s-1,s);
			Cover(tr,tc+s,tr+s-1,tc+s,s);
			Cover(tr+s,tc,tr+s,tc+s-1,s);
		}
		
}
void OutputBoard(int size){
     //输出棋盘的内容
			for(int i=0;i<size;i++){
     
				for(int j=0;j<size;j++){
     
					cout<<Board[i][j];
					
				}
				cout<<endl;
			}
		}
	

(三)二分法不独立情况

上面的例题,经过二分法分解或经过简单处理后,就得到了相似且相互独立的子问题。对于分解后不独立的情况,主要表现在子问题之间包含公共子问题。
问题:求数列的最大字段和
给定n个元素的整数列(可能为负整数)(斜体标注的都为下标)
a1,a2,…,an.
求形如:ai,ai+1,…aj(i/j=1…n,i<=j)的子段
使其和为最大。当所有整数为负整数时定义其最大字段和为0。
例如当(a1,a2,a3,a4,a5,a6)=(-2,11,-1,13,-5,-2)
最大字段和为i=2,j=4(下标从1开始).
解决:
用二分法将数据分为两组,找出两个子问题的解,两个子问题的解不能简单地得到原问题的解,因为两个子问题的中间还有公共子问题,也就是说两个子问题不能真正的分开。此时再想使用二分法,则需要对公共子问题单独处理。
代码思路:
使用二分法将原问题分解,虽然分解得到的子问题并不相互独立,但是对公共子问题进行单独的处理。
将原问题分解成三部分
(从中间分开)
左边部分
右边部分

中间部分(中间部分单独处理时进行再分)
对左边部分和右边部分:
递归二分,直到分解到只剩一个元素。
对中间部分:
中间部分包含的序列可能是整个序列,把整个序列分为左右两部分,两边各向两侧扩展,最后两边的值相加为中间部分最大字段和。
从中间数据开始向一侧扩展,如果加上元素大于原来的和,则加上此元素,否则跳过这个元素继续扩展。
完整代码如下:

#include
int len=5;
int main(){
     
	double max_sum(double a[],int left,int right);//left,right是数组下标范围 这是一个递归函数
	double max=0;
	double a[5]={
     -1,-2,0,3,6};
	max=max_sum(a,1,5);
	printf("%.2f\n",max); 
	return 0;         
}
double max_sum(double a[],int left,int right){
     
	int i;
	double s1=0,s2=0;//中间部分左边为s1,中间部分右边为s2 
	double lefts=0,rights=0;//临时变量标示左边部分的和,和右边部分的和 
	double left_sum=0;
	double right_sum=0;
	double center_sum=0;
	if(left==right){
     //如果这个数为正数就返回,如果为负数就返回0 
		if(a[left]>0) return (a[left]);
		else return (0);
	}
	else{
     
		//递归求左边部分和右边部分最大和
		int center=(left+right)/2; 
		left_sum=max_sum(a,left,center);
		right_sum=max_sum(a,center+1,right);
		//对公共子问题进行单独求解
		//求中间部分最大和
		for(i=0;i<center;i++){
     
			lefts=lefts+a[center-i];
			if(lefts>s1) s1=lefts;
			rights=rights+a[center+i+1];
			if(rights>s2) s2=rights;
		}
		center_sum=s1+s2;
		//比较三部分大小
		if(left_sum>right_sum){
     
			if(left_sum>center_sum) return (left_sum);
			else return (center_sum);
		} 
		else{
     
			if(right_sum>center_sum) return (right_sum);
			else return (center_sum);	
		}
	}
}         

非等分分治

问题:选择第k小的数
对于给定的n个元素的数组a[0,n-1],要求从中找出第k小的数。
解决:
通过改写快速排序算法来解决选择问题。
快速排序算法回忆:首先选择第一个数作为分界数据,将比它小的数据存储在它的左边,将比它大的数据存储在它的右边,它存储在左右两个子集中间。这样左右两个子集就是原问题分解后的独立子问题,再用同样的方法解决这些子问题,直到每个数据只有一个数据,就自然有序了,也就完成了全部数据的排序工作。
代码思路:
一趟排序中分解出的左子集中元素个数nleft,有以下几种情况:
1. nleft=k-1,则分界数据就是选择问题的答案。
2. nleft>k-1,则选择问题的答案在左子集中找,问题规模变小了。
3. 则选择问题的答案在右子集中找,问题变为寻找第k-nleft-1小的数了,问题规模变小了。(因为随着子区间下标不同,k这个答案下标也会变化)

如果直接用快速排序算法的话,需要把数组全部排序后,再找第k小的数。而分治算法是边排边找,节省时间效率。
下面,将通过快速排序的算法和分治算法的比较,来观察两种方法的不同。
完整代码如下:

#include
#include
#include
void swap(int a[],int x,int y);//交换函数
int main(){
     
	//比较两种算法的时间复杂度
	//快速排序算法函数
	int a[7]={
     -1,3,-2,7,6,0,9};
	int len=7;//数组长度  
	int k;
	int n=20,i=0;
	double time;
	int num;
	clock_t start,end;
	void kuaisupaixu(int a[],int left,int right); 
	//非等分分治算法
	int feidengfenfenzhi(int a[],int len,int k);
	//输入k 
	printf("查找数组中第k小的数,请输入k(0~7):");
	scanf("%d",&k);
	/*------------------------------*/
	//调用快速排序算法 
	start=clock();
	for(;i<n;i++){
     
		kuaisupaixu(a,0,len-1);//一次运行时间太短,运行100次取平均值 
	}
	end=clock();
	time=(double)(end-start)/CLOCKS_PER_SEC/n;
	printf("您要查找的第k小的元素为:%d\n",a[k-1]);
	printf("函数调用时间为:%.30lf\n",time);
	/*------------------------------*/
	//调用非等分分治 
	start=clock();
	for(;i<n;i++){
     
		num=feidengfenfenzhi(a,len,k);
	}
	end=clock();
	time=(double)(end-start)/CLOCKS_PER_SEC/n;
	printf("您要查找的第k小的元素为:%d\n",num);
	printf("函数调用时间为:%.30lf\n",time);
	/*------------------------------*/
	return 0; 
}
void kuaisupaixu(int a[],int left,int right){
     
	int temp=a[left];//把第一个变量设置为分界变量 
	int i=left;//左指针赋初值 
	int j=right;//右指针赋初值 
	if(left==right){
     
		return;
	}
	if(left==right-1){
     
		if(a[left]>a[right]) swap(a,left,right);
		return;
	}
	//把比分界变量小的数放前面,把比分界变量大的数放在后面 (利用递归把数从小到大排列)
	//实现一次排序 
	while(1&&j>=left&&i<=right){
     
		//右边扫描 (找出小于分界变量的数 )
		while(a[j]>=temp&&j>=left){
     
				if(j>i) j=j-1;
				else break;
		}
		//左边扫描(找出大于分界变量的数 )
		while(a[i]<=temp&&i<=right){
     
			if(i==j) break;
			else i=i+1;
		}
		if(i==j){
     //每轮探测以i==j结束 
			swap(a,i,left);
			break;
		}
		if(i>=j){
     
			break; 
		}
		swap(a,i,j);
	}
	//左边递归
	if(i!=left){
     
		digui(left,i-1);
	} 
	//右边递归
	if(i!=right){
     
		digui(i+1,right); 
	}
		
}

int feidengfenfenzhi(int a[],int len,int k){
     
	int digui(int a[],int left,int right,int k); 
	int i;
	if(k<0||k>=len){
     
		printf("您查找的数字不在数组中!\n");
	}
	else{
     
		return digui(a,0,len-1,k);
	}
} 
int digui(int a[],int left,int right,int k){
     
	//利用快速排序思路,找到一个基准数(选第一个),小于基准数的放左边,大于基准数的放右边 
	int i,j;
	int p;
	int temp;
	if(left>=right) return a[left];
	i=left,j=right+1;//指定左右指针(因为用do  while循环所以j先加一) 
	temp=a[left];//指定基准数
	//把小数放前面,大数放后面(从小到大排列) 
	//右边循环
	//指针指向小于基准数的下标,等待交换 
	while(1){
     	
		do{
     
			i=i+1;	
		}while(a[i]<temp);//指针向后移动,直到碰到大于temp的数 
		do{
     
			j=j-1;
		}while(a[j]>temp); //指针向前移动,直到碰到小于temp的数 
		//左边循环 
		//指针指向大于基准数的下标,等待交换 
		if(i>=j) break;
		swap(a,i,j);
	}

	//从循环里出来的时候i==j,且以此时指针指向的数为分界点,划分新的区间进行递归 
	if(j-left+1==k) {
     

		return temp;
	}
	a[left]=a[j];
	a[j]=temp;
	if(j-left+1<k){
     //查找的数在右边 
		return digui(a,j+1,right,k-j-1+left);//k的值改变 
	}
	else{
     //查找的数在左边
		return digui(a,left,j-1,k);
	}
}

void swap(int a[],int x,int y){
     //传入的x,y为下标 
	int t;//借助额外变量实现两个数的交换 
	t=a[x];
	a[x]=a[y];
	a[y]=t;
}

最后

原创不易,如果对您有帮助,请点个赞再走呀(づ ̄3 ̄)づ╭❤ ~拜托啦

你可能感兴趣的:(算法分析,分治算法,c算法)