Go教程

经典优化算法之分治法(Divide-and-Conque Algorithm)

本文主要是介绍经典优化算法之分治法(Divide-and-Conque Algorithm),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

欲下载本文相关代码,请在后台回复【分治法】即可下载。

1

目录

1.1  分治法基本介绍

1.2  分治法通俗解释
1.3  分治法严谨定义

1.4  分治法的流程

1.5  分治法的经典例子

1.6  总结

 

2

分治法基本介绍

分治分治,即分而治之。分治,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……直接说就是将一个难以直接解决的大问题,分割成一些规模比较小的相同的小问题,以便各个击破,分而治之。

3

分治法通俗解释

3.1

 栗子

有这样一个非常经典的问题:

有100枚硬币,其中1枚重量与众不同,是假币,略轻一些。如果用天平秤,请问至少称几次一定能找到这枚假币。

 

假如我们用传统的逐枚比较法的话,显然至少需要比较50次。

流程如下:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

而假如我们采用分治法的话,称量流程如下:

 

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),称量3/3,方法同上。

6.将剩下的3(或4)分为1/1/1(或1/1),称量1/1,若平衡,则剩下的一枚是假币,若不平衡,轻的是假币。
若还剩4枚,出现1/1平衡,剩下2枚则称量,显然轻的是假币。

而这种方法只要五次就能解决问题。

 

3.2

 栗子分析

我们来看我们刚刚使用的“分治法”。

 

1.观察可以看到1-2,3-4,5-6步除了硬币的枚数改变了,其他的步骤完全一样。

2.观察发现这是一个子问题的分解过程,100-33-11-3,将一个大问题分解为了容易解决的小问题。

3.可以发现小问题是相互独立的。每一个33枚硬币和其他的并不相互影响。

方便起见,我们用较简单的二分法流程图来具体看一下:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

可以发现这和程序设计中的递归很类似,自顶而下的解决问题。

 

4

分治法严谨定义

4.1

分治算法主定理

分治算法通常遵守一种通用模式:即:在解决规模为n的问题时,总是先递归地求解a个规模为n/b的子问题,然后在

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

时间内将子问题的解合并起来,其中a,b,d>0 a,b,d 是一些特定的整数。分治算法的运行时间可以通过公式来计算。

4.1.1

 分治算法的时间复杂度

一般的,对于a,b,n>0,且有

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

成立,
则时间复杂度为:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

4.1.2 

  举例

我们用比较熟悉的归并排序来分析:

首先有

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=
对于我们熟悉的归并排序符合主定理的第二种情况,

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=


5

分治算法的流程

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

5.1

 流程Divide-and-Conque!

划分问题:整个问题划分成多个无关联的子问题。

递归求解:递归调用求解各个子问题。
合并问题:合并子问题的解,形成原始问题的解。


我们用伪代码来具体分析~

 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条,对于存在公共子问题的问题,使用分治算法会存在重复计算的问题,使用动态规划较为合适。

浅谈分治与动态规划

分治和动态规划有共通也有不同,我i们来看如下两个定义。

 

最优子结构:如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子机构。

 

 

重叠子问题:用来求解原问题的递归算法反复地解同样的子问题,而不是总是在产生新的子问题。对两个子问题来说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,则它们是重叠的。

 

当子问题相互独立时,能且只能使用分治。存在重叠子问题时,动态规划是更好的算法。

In a word, 分治法 —— 各子问题独立;动态规划 —— 各子问题重叠。

 

算法导论: 动态规划要求其子问题既要独立又要重叠,这看上去似乎有些奇怪。虽然这两点要求听起来可能矛盾的,但它们描述了两种不同的概念,而不是同一个问题的两个方面。如果同一个问题的两个子问题不共享资源,则它们就是独立的。对两个子问题俩说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,是重叠的,则它们是重叠的。

 


6

分治算法的经典例子

6.1

归并排序

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

6.1.1

 问题描述

给定n个乱序数字,要求从小到大排序后输出。

 

6.1.2

 思路分析

逐条对应:
1.比较两个数字大小很容易。
2.将n个数字排序和将n/2个数字排序问题相同
3.不存在重叠子问题。因而用分治法是很有效果的。

思路如下:

1.分割:将这个序列分割成一个个已经排好序的子序列(即子序列里只有一个数)

2.比较。
3.归并:将一个个有序的子序列合并成排好序的序列。

 

6.1.3

 Coding 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[i]=a[left+i];//储存左数列
    for(int i=0;i<n2;i++)
        R[i]=a[mid+i];//储存右数列
    L[n1]=R[n2]=INF;
    int i=0,j=0;
    for(int k=left;k<right;k++)//合并
    {
        if(L[i]<=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[i]=a[left+i];
    for(int i=0;i<n2;i++)
        R[i]=a[mid+i];
    L[n1]=R[n2]=INF;
    int i=0,j=0;
    for(int k=left;k<right;k++)
    {
        if(L[i]<=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[i];
    mergesort(a,n,0,n);
    for(int i=0;i<n;i++)
    {
        if(i)
            cout<<" ";
        cout<<a[i];
    }
    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.3

 Coding 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[i]);
    int sumr=a[m+1];t=0;
    for(int i=m+1;i<=r;i++)
        sumr=max(sumr,t+=a[i]);

    return max(Max,suml+sumr);//取两种情况中最大的。


}

 

6.3

汉诺塔问题

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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上n号金片移至C。

3.再把B上从1到n-1号金片移至C。

 

可以看到递归的影子,要解决n个,先解决n-1个,可以分治为n=2的最简单情况并加以解决。

6.3.3

 Coding Time

#include <iostream> 
void move(int,char,char,char); 
int main()
{    
     int m;
     cin>>m; 
     move(m,'A','B','C');
} 
/*XYZ分别代表三根柱子*/

void move(int m,char x,char y,char z)
{    
    if(m==1)    
    {        
        cout<<x<<"->"<<z<<endl";   //如果只有一个金片了,那么我们直接将其从X移至Z即可。        
        return;    
    }    
    move(m-1,x,z,y); //先将m-1个金片从X移至Y。    
    cout<<x<<"->"<<z<<endl);//每次递归时,我们总是将1号金片移至Z。    
    move(m-1,y,x,z); //再将整体代换的m-1个金片从Y移至Z
}

 

 


7

总结

分治法实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。
一定是先找到最小问题规模时的求解方法,然后考虑随着问题规模增大时的求解方法找到求解的递归函数式后(各种规模或因子),设计递归程序即可。

 

生活中也一样,当你面对一个大的问题不知所措的时候,不如静下心来仔细分析,找到问题加以分割解决,是不是会轻松许多呢?

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

---The End---

 

这篇关于经典优化算法之分治法(Divide-and-Conque Algorithm)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!