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

分治算法详细讲解(含经典例题分析)分治法思路 将整个问题分解成若干小问题后再分而治之 如果分解得到的子问题相对来说还是太大 则可反复使用分治策略将这些子问题分成更小的同类型子问题 直至产生方便求解的子问题 必要时逐步合并这些子问题的解 从而得到问题的解 分治算法可以由递归过程来表示 因为分治法就是一种找大规模问题与小规模问题关系的方法 是递归设计的一种具体策略 步骤 1 分解将原问题分解为若干规模较小 相互独立 与原问题相同的子问题 2 解决若干子问题较小而容易被解决则直接解决 否则再继续分解为更小的子问题 直到容易解决 3

分治法思路:

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

步骤

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)个方格的棋盘,其中恰有一个方格残缺,其中残缺的方格用蓝色阴影表示。

解决:
以k=2为例(此时共有16个小方格),用二分法进行分解,得到的是由4个k=1的棋盘组成。
在这里插入图片描述




#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这个答案下标也会变化)


















#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 ̄)づ╭❤ ~拜托啦

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/200109.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月20日 上午11:30
下一篇 2026年3月20日 上午11:31


相关推荐

  • sendgrid html text,用sendgrid发邮件

    sendgrid html text,用sendgrid发邮件sendgrid 是发送邮件的服务提供商 它提供两种发送邮件的方式 通过 smtp 或者他们自己专有的 API 来发送 我们用 go 来发送一个 首先下载 sendgrid go 软件包 通过 goget 命令来下载 getgetgithub com sendgrid sendgrid go 下载完后我们来看第一个例子 packagemaini fmt log os gi

    2026年3月19日
    3
  • 如何建立爬虫代理ip池「建议收藏」

    如何建立爬虫代理ip池「建议收藏」目录一、为什么需要建立爬虫代理ip池二、如何建立一个爬虫代理ip池原文地址:https://www.cnblogs.com/TurboWay/p/8172246.html一、为什么需要建立爬虫代理ip池在众多的网站防爬措施中,有一种是根据ip的访问频率进行限制的,在某段时间内,当某个ip的访问量达到一定的阀值时,该ip会被拉黑、在一段时…

    2022年5月15日
    41
  • mj绘画中文站使用教程

    mj绘画中文站使用教程

    2026年3月15日
    1
  • influxdb基本操作_一个数据库只有一个内模式

    influxdb基本操作_一个数据库只有一个内模式安装aptinstallinfluxdbinfluxdb-client<ip>:8083#web<ip>:8086#httpapi复制代码influxDB中独有的一些概念Point由时间戳(time)、数据(field)和标签(tags)组成time:每条数据记录的时间,也是数据库自动生成的主索引fields:各种记录的值…

    2025年6月20日
    6
  • Promise.all的深入理解「建议收藏」

    Promise.all的深入理解「建议收藏」异步之PromisePromise.allPromise.all接收的promise数组是按顺序执行的还是一起执行的,也就是说返回的结果是顺序固定的吗?目前有两种答案:应该是同步执行的,但是这样就有效率问题了,如果想改成异步执行怎么办呢?有些人认为结果是按顺序执行的,有些人认为结果顺序不确定。那么我们根据实现来解密:环境为:vscode1.20….

    2022年5月24日
    61
  • 没有网线情况下使用树莓派连接WiFi

    没有网线情况下使用树莓派连接WiFi没有网线情况下使用树莓派连接WiFi烧录系统后,在boot/文件夹下创建wpa_supplicant.conf文件添加代码:country=CNctrl_interface=DIR=/var/run/wpa_supplicantGROUP=netdevupdate_config=1在尾部添加network={ssid=”你无线的名字”//无线名称psk=”你无线的密码”//无线密码}树莓派开机,可自动连接WiFi…

    2022年5月6日
    100

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号