算法案例之分治法

基于递归思想,将大问题拆解为小问题,再由小问题的解构造原问题的解

  • 设计思路:分三步执行 ------ 分解问题为同类子问题、独立求解各子问题、合并子问题的解得到原问题答案,通常结合递归实现。
  • 适用场合:适用于可拆解为同类子问题,且子问题的解能合并为原问题解的场景。
  • 注意事项
    • 子问题需相互独立
    • 子问题数量要满足递归求解条件
    • 保证子问题解合并的正确性
    • 优化递归深度与重复计算,提升算法效率

【例题1】快速排序

快速排序:选择一个基准元素,将数组分成两部分,使得:

左边所有元素 ≤ 基准

右边所有元素 ≥ 基准

然后递归地对左右两部分排序

分区过程详解(挖坑法):

1.选基准key=a[s],留下"坑"hole=s

2.从右向左找第一个≤key的元素,填入左边的坑

3.从左向右找第一个≥key的元素,填入右边的坑

4.重复直到左右指针相遇

5.将基准放入最后的位置

cpp 复制代码
#include<iostream>
using namespace std;
int Partition(int a[],int s,int e){
	int key=a[s];      // 选择第一个元素作为基准
    int L=s,R=e;       // L:左指针,R:右指针
    int hole=s;        // 空位索引(挖坑法)
	while(L<R){
		while(L<R && a[R]>key){		// 从右向左找第一个≤key的元素
			R--;
		}
		a[hole]=a[R];		// 将找到的元素填到空位
		hole=R;				// 更新空位位置
		while(L<R && a[L]<key){		// 从左向右找第一个≥key的元素
			L++;
		}
		a[hole]=a[L];  // 将找到的元素填到空位
        hole=L;        // 更新空位位置
	}
	a[hole]=key;       // 基准放入最终位置
    return L;          // 返回基准的最终位置
} 
void sort(int a[],int s,int e){ //s=start,e=end
	if(s>=e) return;	// 递归出口:区间为空或只有一个元素
	// 分区,得到基准位置
	int i=Partition(a,s,e);
	//递归解决子问题
	sort(a,s,i-1);  // 排序基准左边的子数组
    sort(a,i+1,e);  // 排序基准右边的子数组 
}
int main(){
	int a[]={2,5,8,3,6,1,4,7,9}; 
	//数组长度 
	int n=sizeof(a)/sizeof(a[0]);
	sort(a,0,n-1);	// 对[0..8]排序
	for(int i=0;i<n;i++){
		cout<<a[i];		// 输出排序结果
	}
	return 0;
}

【例题2】归并排序

自顶向下的二路归并排序算法(代码相对自底向上简单)

归并排序:将数组不断二分,直到每个子数组只有一个元素,然后将这些有序子数组合并成更大的有序数组。

三步走:

1.分解:将数组分成两半

2.解决:递归排序两个子数组

3.合并:合并两个已排序的子数组

cpp 复制代码
#include<iostream>
using namespace std;
void merge(int a[],int s,int mid,int e){
	int i=s, j=mid+1;              // i:左子数组起始,j:右子数组起始
    int temp[e-s+1], k=0;          // 临时数组存放合并结果
    
    // 比较左右子数组,取较小元素放入temp
	while(i<=mid&&j<=e){
		if(a[i]<a[j]){
			temp[k++]=a[i++];
		}else{
			temp[k++]=a[j++];
		}
	} 
    
	// 处理剩余元素(这两个循环只会执行一个) 
	while(i<=mid) temp[k++]=a[i++]; 
	while(j<=e) temp[k++]=a[j++];
    
	// 将临时数组的值还回a数组中
	for(int l=0,r=s;r<=e;l++,r++){
		a[r]=temp[l];
	}
}
void mergesort(int a[],int s,int e){
	if(s >= e) return;           // 递归出口:区间只有一个或零个元素
    int mid = (s+e)/2;           // 计算中点,分解:mid = (s+e)/2将数组分成两半
    mergesort(a,s,mid);          // 递归排序左半部分
    mergesort(a,mid+1,e);        // 递归排序右半部分
    merge(a,s,mid,e);            // 合并两个有序子数组
}
int main(){
	int a[]={2,5,1,7,10,6,9,4,3,8}; 
	int n=sizeof(a)/sizeof(a[0]);
	mergesort(a,0,n-1);		// 对整个数组排序
	for(int i=0;i<n;i++){
		cout<<a[i];
	}
	return 0;
}

【例题3】查找最大和次大元素

问题描述:对于给定的含有n元素的无序序列,求这个序列中最大和次大的两个不同的元素

例如:(2,5,1,4,6,3),最大的元素为6,次大元素为5

a[]:输入数组

s:查找区间的起始索引

e:查找区间的结束索引

max1, max2:引用参数,返回该区间的最大值和第二大值

cpp 复制代码
#include<iostream>
using namespace std;
void find(int a[],int s,int e,int &max1,int &max2){
	//递归出口 
	if(s==e){		// 只有一个元素
		max1=a[s];
		max2=-1;		// 没有第二大值,用-1表示
	}else if(s+1==e){	// 只有两个元素
        //直接比较两个元素,大的为max1,小的为max2
		if(a[s]>=a[e]){		
			max1=a[s];
			max2=a[e];
		}else{
			max1=a[e];
			max2=a[s];
		}
	}else{		// 超过两个元素
	int mid=(s+e)/2;
	int lmax1,lmax2,rmax1,rmax2;
    // 递归解决左半部分
	find(a,s,mid,lmax1,lmax2);
    // 递归解决右半部分
	find(a,mid+1,e,rmax1,rmax2);
	//小问题的解合并为原问题的解,合并结果
	if(lmax1 > rmax1){		// 如果 lmax1 > rmax1,则全局最大是 lmax1
        max1 = lmax1;	
        max2 = (lmax2 > rmax1) ? lmax2 : rmax1;		// 左边次大比右边最大
    } else {				// 否则全局最大是 rmax1
        max1 = rmax1;
        max2 = (rmax2 > lmax1) ? rmax2 : lmax1;		// 右边次大比左边最大
    }
}
	} 
	
int main(){
	int a[]={2,5,1,4,6,3}; 
	int n=sizeof(a)/sizeof(a[0]);
	int max1,max2;
	find(a,0,n-1,max1,max2);
	cout<<max1<<","<<max2;
	return 0;
}

【例题4】折半查找(二分查找)

a[]:已排序的数组(必须有序)

l:查找范围的左边界索引

r:查找范围的右边界索引

num:要查找的目标值

cpp 复制代码
#include<iostream>
using  namespace std;
int binsearch(int a[],int l,int r,int num){		// 二分查找函数
	int mid;
	while(l<r){		// 只要左边界 l小于右边界 r,就继续查找
		mid = (l+r)/2;
		if(a[mid]==num) return mid;		// 找到目标
		else if(a[mid]<num) l=mid+1;		// 目标在右侧
		else if(a[mid]>num) r=mid-1;		// 目标在左侧
	}
	return -1;			// 返回值:找到则返回索引,未找到返回 -1
}
int main(){
	int a[]={1,2,3,4,5,6,7,8,9}; 
	int n=sizeof(a)/sizeof(a[0]);
	int index = binsearch(a,0,n-1,8);    //查找数字为8
	cout<<index;
	return 0;
}

扩展:

查找数字第一次出现的位置

cpp 复制代码
#include<iostream>
using  namespace std;
int binsearch(int a[],int l,int r,int num){
	int mid;
	while(l+1!=r){
		mid = (l+r)/2;
		if(a[mid]<num) l=mid;
		else if(a[mid]>=num) r=mid;
	}
	return r;
}
int main(){
	int a[]={1,2,3,4,5,6,7,8,8,8,8,8,8,8,9}; 
	int n=sizeof(a)/sizeof(a[0]);
	int index = binsearch(a,0,n-1,8);
	cout<<index;
	return 0;
}

查找数字最后一次出现的位置

cpp 复制代码
#include<iostream>
using  namespace std;
int binsearch(int a[],int l,int r,int num){
	int mid;
	while(l+1!=r){
		mid = (l+r)/2;
		if(a[mid]<=num) l=mid;
		else if(a[mid]>num) r=mid;
	}
	return l;
}
int main(){
	int a[]={1,2,3,4,5,6,7,8,8,8,8,8,8,8,9}; 
	int n=sizeof(a)/sizeof(a[0]);
	int index = binsearch(a,0,n-1,8);
	cout<<index;
	return 0;
}

注意:没有重复的也可以查找到相应的数字,但推荐使用第一个代码进行二分查找

【例题5】求解循环日程安排问题

设有n=2^k个选手要进行网球循环赛,要求设计一个满足以下要求的比赛日程表(分治法)

1.每个选手必须与其他n-1个选手各赛一次

2.每个选手一天只能赛一次

3.循环赛在n-1天之内结束

cpp 复制代码
#include<iostream>
using namespace std;
int k=4;		// 2^k 个选手,这里k=4表示16个选手
int a[101][101];	// 日程表,a[i][j]表示选手i在第j天的对手
void plan(int k){
	a[0][0]=1 ,a[0][1]=2;	// 选手1:编号1,第1天对手是2
	a[1][0]=2 ,a[1][1]=1;	// 选手2:编号2,第1天对手是1
	//构造复杂问题
	int n=2; 	// 从2个选手开始
	for(int s=1;s<=k;s++){	// s表示当前扩展次数
		int temp=n; 	// 保存当前规模
		n=n*2;			// 规模翻倍
		//构造左下角
		for(int i=temp;i<n;i++){
			for(int j=0;j<temp;j++){
				a[i][j] = a[i-temp][j] + temp;
			}
		}
		//构造右下角
		for(int i=temp;i<n;i++){
			for(int j=temp;j<n;j++){
				a[i][j] = a[i-temp][j-temp];
			}
		} 
		//构造右上角
		for(int i=0;i<temp;i++){
			for(int j=temp;j<n;j++){
				a[i][j] = a[i][j-temp]+temp;
			}
		}  
	} 
}
int main(){
	plan(k);
	for(int i=0;i<1<<k;i++){
		for(int j=0;j<1<<k;j++){
			cout<<a[i][j]<< " ";
		}
		cout<<endl;
	}
	return 0;
} 
相关推荐
小屁猪qAq2 小时前
强符号和弱符号及应用场景
c++·弱符号·链接·编译
wen__xvn2 小时前
代码随想录算法训练营DAY25第七章 回溯算法 part04
算法·leetcode·深度优先
亲爱的非洲野猪2 小时前
动态规划进阶:序列DP深度解析
算法·动态规划
头发还没掉光光2 小时前
HTTP协议从基础到实战全解析
linux·服务器·网络·c++·网络协议·http
数字游名Tomda2 小时前
阿里上新 AI 平台「呜哩」,生图生视频免费开放!
经验分享
不染尘.2 小时前
双指针算法
算法
2501_901147832 小时前
题解:有效的正方形
算法·面试·职场和发展·求职招聘
你撅嘴真丑2 小时前
习题与总结
算法
亲爱的非洲野猪2 小时前
动态规划进阶:状态机DP深度解析
算法·动态规划