|
欲下载本文相关代码,请关注微信公众号“数据魔术师”,在后台回复关键字“分治法”。
1.目录
1.1分治法基本介绍
1.2分治法通俗解释
1.3分治法严谨定义
1.4分治法的流程
1.5分治法的经典例子
1.6总结
2.分治法基本介绍
分治分治,即分而治之。分治,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
直接说就是将一个难以直接解决的大问题,分割成一些规模比较小的相同的小问题,以便各个击破,分而治之。
3.分治法通俗解释
3.1栗子
有这样一个非常经典的问题 .
问题:有100枚硬币,其中1枚重量与众不同,是假币,略轻一些。如果用天平秤,请问至少称几次一定能找到这枚假币。
假如我们用传统的逐枚比较法的话,显然至少需要比较50次。 流程如下:
而假如我们采用分治法的话,称量流程如下:
1.将100硬币分成3份,33,33,34。
2.称量1、2份,若天平平衡,则假币必在另外34枚中。若不平衡,假币在轻的那33枚里。
3.将34枚分为11/11/12枚(或将33枚分成11*3)。
4.称量两组11枚的硬币,若平衡,假币在12枚里(或另外的11枚)若不平衡,假币在轻的11里。
5.将11(或12)枚分成3/4/4(或4/4/4),称量4/4,方法同上。
6.将剩下的3(或4)分为1/1/1(或1/1),称量1/1,若平衡,则剩下的一枚是假币,若不平衡,轻的是假币。 若还剩4枚,出现1/1平衡,剩下2枚则称量,显然轻的是假币。
这种方法只需要5次就能解决这个问题。
3.2栗子分析
我们来看我们刚刚使用的“分治法”。
1.观察可以看到1-2,3-4,5-6步除了硬币的枚数改变了,其他的步骤完全一样。
2.观察发现这是一个子问题的分解过程,100—33-11-3,将一个大问题分解为了容易解决的小问题。
3.可以发现小问题是相互独立的。每一个33枚硬币和其他的并不相互影响。
方便起见,我们用较简单的二分法流程图来具体看一下:
可以发现这和程序设计中的递归很类似,自顶而下的解决问题;
4.分治法严谨定义
4.1分治算法主定理
分治算法通常遵守一种通用模式:即:在解决规模为n的问题时,总是先递归地求解a个规模为n/b的子问题,然后在
时间内将子问题的解合并起来,其中a,b,d>0a,b,d 是一些特定的整数。分治算法的运行时间可以通过公式:
来计算。
4.1.1分治算法的时间复杂度
一般的,对于a,b,d>0且有
成立,
则:
4.1.2 举例
我们用比较熟悉的归并排序来分析:
首先有
对于我们熟悉的归并排序符合主定理的第二种情况,有
。
5.分治算法的流程
5.1流程Divide-and-Conquer!
划分问题:整个问题划分成多个无关联的子问题。
递归求解:递归调用求解各个子问题。
合并问题:合并子问题的解,形成原始问题的解。 我们用伪代码来具体分析~
Divide-and-Conquer(P)
1. if |P|≤n0
2. then return(ADHOC(P))
3. 将P分解为较小的子问题 P1 ,P2 ,…,Pk
4. for i←1 to k
5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi
6. T ← MERGE(y1,y2,…,yk) △ 合并子问题
7. return(T)
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。5.2使用条件
1.该问题的规模缩小到一定的程度就可以容易地解决
2.该问题可以分解为若干个规模较小的相同问题。
3.利用该问题分解出的子问题的解可以合并为该问题的解;
4.该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。(非必需)
5.2.1对使用条件的分析
第1条随着问题规模的减少,问题自然会容易解决。条件2,3是分治的前提。即Divide-and-Conquer的必要条件。 第4条,对于存在公共子问题的问题,使用分治算法会存在重复计算的问题,使用动态规划较为合适。
5.2.1.1浅谈分治与动态规划
分治和动态规划有共通也有不同,我i们来看如下两个定义。
最优子结构:如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子机构。
重叠子问题:用来求解原问题的递归算法反复地解同样的子问题,而不是总是在产生新的子问题。对两个子问题来说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,则它们是重叠的。 当子问题相互独立时,能且只能使用分治。存在重叠子问题时,动态规划是更好的算法。 In a word, 分治法 —— 各子问题独立;动态规划 —— 各子问题重叠。
算法导论: 动态规划要求其子问题既要独立又要重叠,这看上去似乎有些奇怪。虽然这两点要求听起来可能矛盾的,但它们描述了两种不同的概念,而不是同一个问题的两个方面。如果同一个问题的两个子问题不共享资源,则它们就是独立的。对两个子问题俩说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,是重叠的,则它们是重叠的。
6.分治算法的经典例子
6.1 归并排序
6.1.1问题描述:给定n个乱序数字,要求从小到大排序后输出。
6.1.2思路分析:
逐条对应:
1.比较两个数字大小很容易。
2.将n个数字排序和将n/2个数字排序问题相同
3.不存在重叠子问题。
因而用分治法是很有效果的。 思路如下:
1.分割:将这个序列分割成一个个已经排好序的子序列(即子序列里只有一个数)
2.比较。
3.归并:将一个个有序的子序列合并成排好序的序列。
6.1.3coding time
我们用递归的方法来解决。
//首先来看函数主体,实现了分割的部分
void mergesort(int a[],int n,int left,int right)
{
if(left+1<right)
{
int mid=(left+right)/2;
mergesort(a,n,left,mid);
mergesort(a,n,mid,right);
merge(a,n,left,mid,right);
}
}
//下面来写自己的merge函数!
//L,R是辅助数组!
void merge(int a[],int n,int left,int mid,int right)
{
int n1=mid-left,n2=right-mid;
for(int i=0;i<n1;i++)
L=a[left+i];//储存左数列
for(int i=0;i<n2;i++)
R=a[mid+i];//储存右数列
L[n1]=R[n2]=INF;
int i=0,j=0;
for(int k=left;k<right;k++)//合并
{
if(L<=R[j])
a[k]=L[i++];
else
a[k]=R[j++];
}
}代码如下
#include<bits/stdc++>
using namespace std;
const int maxn=500000,INF=0x3f3f3f3f;
int L[maxn],R[maxn];
void merge(int a[],int n,int left,int mid,int right)
{
int n1=mid-left,n2=right-mid;
for(int i=0;i<n1;i++)
L=a[left+i];
for(int i=0;i<n2;i++)
R=a[mid+i];
L[n1]=R[n2]=INF;
int i=0,j=0;
for(int k=left;k<right;k++)
{
if(L<=R[j])
a[k]=L[i++];
else
a[k]=R[j++];
}
}
void mergesort(int a[],int n,int left,int right)
{
if(left+1<right)
{
int mid=(left+right)/2;
mergesort(a,n,left,mid);
mergesort(a,n,mid,right);
merge(a,n,left,mid,right);
}
}
int main()
{
int a[maxn],n;
cin>>n;
for(int i=0;i<n;i++)
cin>>a;
mergesort(a,n,0,n);
for(int i=0;i<n;i++)
{
if(i)
cout<<&#34; &#34;;
cout<<a;
}
cout<<endl;
return 0;
}6.1.4优缺点分析
优点:用分治算法主定理可得时间复杂度为O(nlogn),相同元素的顺序不会颠倒,是稳定排序。
缺点:需要辅助数组,所需空间复杂度为O(n)。
6.2求最大连续和
6.2.1问题描述:
给出一个长度为n的序列A1,A2,A3·····An,求最大连续和。如序列(6,-1 , 5, 4,-7), 该序列中的最大和是6 +( - 1)+ 5 + 4 = 14。
6.2.2思路分析:
基本思路是使用枚举法,三重嵌套循环,时间复杂度为n的三次方
我们来用分治法解决这个问题
1.划分问题:将序列分成元素个数尽可能相等的两半。
2.递归求解:分别求出位于左半和右半的最佳序列。
3.合并问题:求出起点位于左半,终点位于右半的最大连续和序列,和子问题最优解比较。
6.2.3coding time!
int maxsum(int l,int r)
{
if(l==r)return a[l];
int m=(l+r)/2;
int Max=max(maxsum(l,m),maxsum(m+1,r));//(分解)情况1:完全在左区间,或者完全在右区间
//(合并)情况2:横跨左右两个区间
int suml=a[m],t=0;
for(int i=m;i>=l;i--)
suml=max(suml,t+=a);
int sumr=a[m+1];t=0;
for(int i=m+1;i<=r;i++)
sumr=max(sumr,t+=a);
return max(Max,suml+sumr);//取两种情况中最大的。
}6.3汉诺塔问题
6.3.1问题描述:
如下图所示,从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面,求移动的步骤和移动的次数。
6.3.2思路分析:
考虑最简单情况n=2,三根柱子分别为A、B、C,此时,在A上有2片金片(假设从小到大依次为1,2)。那么我们的解决步骤可以归结为如下:
1,先把A上1号金片移至B。
2,再把A上2号金片移至C。
3,再把B上1号金片移至C。 考虑n增大的情况: 1,先把A上从1到n-1号金片移至B。
2,再把A上64号金片移至C。
3,再把B上从1到n-1号金片移至C。
可以看到递归的影子,要解决n个,先解决n-1个,可以分治为n=2的最简单情况并加以解决。
6.3.3coding time!
#include <iostream>
void move(int,char,char,char);
int main()
{
int m;
cin>>m;
move(m,&#39;A&#39;,&#39;B&#39;,&#39;C&#39;);
}
/*XYZ分别代表三根柱子*/
void move(int m,char x,char y,char z)
{
if(m==1)
{
cout<<x<<&#34;->&#34;<<z<<endl&#34;; //如果只有一个金片了,那么我们直接将其从X移至Z即可。
return;
}
move(m-1,x,z,y); //先将m-1个金片从X移至Y。
cout<<x<<&#34;->&#34;<<z<<endl);//每次递归时,我们总是将1号金片移至Z。
move(m-1,y,x,z); //再将整体代换的m-1个金片从Y移至Z
}7.总结
分治法实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。 一定是先找到最小问题规模时的求解方法,然后考虑随着问题规模增大时的求解方法找到求解的递归函数式后(各种规模或因子),设计递归程序即可。
<hr/>文案 && 代码:李博骁
审核:邓发珩 && 贺兴
指导老师: 秦时明岳(华中科技大学管理学院)
如对文中内容有疑问,欢迎交流。PS:部分资料来自网络。
如有需求,可以联系:
秦虎老师(professor.qin@qq.com)
李博骁 (华中科技大学管理学院本科一年级 : 1642940680@qq.com)
邓发珩 (华中科技大学管理学院本科二年级:2638512393@qq.com、个人公众号:程序猿声)
贺兴 (华中科技大学管理学院本科四年级: hexing15@gmail.com) |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|