算法复习笔记(分治法、动态规划、贪心算法)

分治法

  分治法的基本思想是将一个规模为n的问题分解为k个规模较小的问题,这些子问题互相独立且与原问题相同(所以可以递归)。递归地解这些子问题,然后将各个子问题的解合并得到原问题的解。它的一般算法设计模式如下:

divide-and-conquer(P)
{
//|P|表示问题的规模,n0表示阈值,当规模不超过n0时,问题容易解出,不必分解
    if(|P|<=n0)   
        adhoc(P);
    //将P分解成子问题
    divide P into smaller subinstances P1,P2...Pk;
    //对子问题逐个求解
    for(i=0;i<=k;i++)
        yi=divide-and-conquer(Pi);
    //将各个子问题的解合并得到原问题的解 
    return merge(y1,y2,...,yk);
}

  分治法的示例有很多,比如数据结构中的二路归并排序。还有大整数乘法,Strassen矩阵乘法等。这边我们解释一个简单的实例,寻找逆序对。
  设A[1…n]是一个包含n个不同数的数组。如果在i小于j的情况下,有A[i]大于A[j],则(i,j)就成为A中的一个逆序对(inversion)。
  要确定一个数组中的逆序对的个数,可以采取分治法。将A分为两部分A1和A2,则A中逆序对的数目等于A1中逆序对的数目、A2中逆序对的数目和A1,A2合并时A1中比A2中元素大的数目。
  代码如下所示:

#define Maxsize 4
#include <IOSTREAM>
using namespace std;

int number[Maxsize]={8,3,4,1};

int Merge(int start,int mid,int end)
{
    int count=0;
    int temp[Maxsize];
    int i,j,k=0;
    i=start;
    j=mid+1;
    while (i<=mid&&j<=end)
    {
        if (number[i]>number[j])
        {
            count=count+mid-i+1;  //这句话是重点
            temp[k]=number[j];
            j++;
        }
        else
        {
            temp[k]=number[i];
            i++;
        }
        k++;
    }
    if(i<=mid)
    {
        for(;i<=mid;i++)
        {  
            temp[k]=number[i];  
            k++;  
        }
    }
    if(j<=end)
    {
        for(;j<=end;j++)
        {  
            temp[k]=number[j];  
            k++;  
        }
    }
    for(k=0;k<end-start+1;k++)  
        number[start+k]=temp[k];
    return count;
}

int findReverseOrder(int start,int end)
{
    if (start<end)
    {
        int mid;
        int num1;
        int num2;
        mid=(end+start)/2;
        num1=MSort(start,mid);
        num2=MSort(mid+1,end);
        return num1+num2+Merge(start,mid,end);
    }
    else
        return 0;
}

int main()
{
    cout<<"共有"<<findReverseOrder(0,3)<<"对逆序对。";  
    return 0;
}

动态规划

  动态规划算法和分治法相似的地方是它也是将待求解问题分成若干子问题,然后从这些子问题的解得到原问题的解。但与分治法不同的是,适合动态规划法解的题,经分解得到的子问题往往不是相互独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
  能用动态规划解的问题有如下三个性质:
  1. 最优子结构性质
  当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。在动态规划算法中,利用问题的最优子结构性质,以自底向上的方式从子问题的最优解逐步构造出整个问题的最优解。
  2. 无后效性
  将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
  3. 重叠子问题
  在用递归算法自顶向下解此问题时,每次产生的子问题并不总是新的子问题,有些子问题被反复计算过多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其保存在一个表格中,当再次需要解此问题时,只是简单地用常数时间看一下结果。
  动态规划的解题过程往往是一个填表的过程。又十分类似于数学学过的归纳法。我将解题过程归纳成两步:1.找边界(找0或者1这些特殊情况时的边界)2.找出递推式(这一步就是将大问题分解成小问题,Sn=?Sn-1)。另外,由于是填表过程(给数组赋值),有这样的规律数组的内容往往是结果,数组的下标往往是结果的条件
  关于动态规划的示例我看到作者为Hawstein写的这样一篇文章,受益匪浅。http://www.hawstein.com/posts/dp-novice-to-advanced.html。在这里我讲一下01背包问题的动态规划。
  01背包是在M件物品取出若干件放在空间为W的背包里,每件物品的体积为W1,W2……Wn,与之相对应的价值为P1,P2……Pn。有4个体积是2,3,5和6,价值为3,4,5和7的物品,背包容量是11。求背包能放的最大价值。
  01背包的状态转换方程 f[i,j] = Max{ f[i-1,j-Wi]+Pi( j >= Wi ), f[i-1,j] }
  f[i,j]表示在前i件物品中选择若干件放在承重为 j 的背包中,可以取得的最大价值。Pi表示第i件物品的价值。

//0-1背包问题
#include <IOSTREAM>
using namespace std;

int n=4;
int C=11;
int V[4+1][11+1];  //0号元素均不用
int p[4+1]={0,3,4,5,7};
int w[4+1]={0,2,3,5,6};

int X[11+1];

int Knapsack()  
{
    int i,j;
    for (i=0;i<=n;i++)
    {
        V[i][0]=0;
    }
    for (j=0;j<=C;j++)
    {
        V[0][j]=0;
    }
    for (i=1;i<=n;i++)
    {
        for (j=1;j<=C;j++)
        {
            V[i][j]=V[i-1][j];
            if (w[i]<=j)
            {
                V[i][j]=(V[i-1][j]>(V[i-1][j-w[i]]+p[i])?V[i-1][j]:(V[i-1][j-w[i]]+p[i]));
            }
        }
    }
    return V[n][C];
}

int main()
{   
    int bestp=Knapsack();
    cout<<"选总价值为"<<bestp<<"的物品。"<<endl;
    return 0;
}

  这是一个二维的动态规划解法,根据状态方程和上面的程序可以画出如下的表:
  《算法复习笔记(分治法、动态规划、贪心算法)》
  那么如何修改这个算法,使其只需C的空间呢?(改变成1维的解法)
 

int Knapsack()
{
    for (int i = 0; i <= C; ++i)
    {
        X[i] = 0;
    }
    for (int j = 1; j <= n; ++j)
    {
        for (int i = C; i >= 1; --i)//注意必须是倒着来算
        {
            if (i >= w[j] && X[i - w[j]] + p[j] > X[i])
            {
                X[i] = X[i - w[j]] + p[j];
            }
        }
    }
    return X[C];
}

  这个解法是不是和Hawstein博客中第一个解法特别像。算法思想是:因为填表时只需上下两行就可以填,所以填下一行的时候可以直接填在上一行中,但会出现上一行中前面(左边)的信息因为更新了,被覆盖了(计算V[i-1][j-w[i]]要用到上一行当前位置左边的元素,但此元素有可能已经被更新了)导致计算错误。所以填表要从表的右边开始填,这样就不会影响(因为程序计算时不需要上一行当前位置左边的元素,所以左边元素更新无所谓)。因此i从C到1。

贪心算法

  贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
  贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
  因此贪心算法具有最优子结构性质和贪心选择性质。
  1.最优子结构(略)
  2.贪心选择性质
   所谓贪心的选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这也是贪心算法和动态规划算法的主要区别。在动态规划算法中,每步所做的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能做出选择。而在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择。正是由于这种区别,动态规划算法通常以自底向上的方式解决个子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式做出相继的贪心选择,每做一次贪心选择就将所求问题简化为规模更小的子问题。
   贪心算法的示例也有很多,比如数据结构中的Prim算法、Dijkstra算法和Huffman算法等。我们以活动安排问题为例。
   问题描述:设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si 小于fi。如果选择了活动i,则它在半开时间区间[si, fi)内占用资源。若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的。也就是说,当si≥fj或sj≥fi时,活动i与活动j相容。活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合。
   算法思想:
   定义两个数组start[]和finish[],按结束时间的非递增顺序拍好后,分别存放开始时间和结束时间,第一个活动选,从第二个活动开始比较是否开始时间超过前一个被选活动的结束时间,超过则相容,此活动被选,依次进行。
   算法代码:

//活动安排问题:start[i]={0,1,3,0,5,3,5,6,8,8,2,12},finish[i]={0,4,5,6,7,8,9,10,11,12,13,14}
#define maxsize 12
#include <IOSTREAM>
using namespace std;

int n=11;
int x[maxsize]={0};      //用来判断活动是否被选
int start[maxsize]={0,1,3,0,5,3,5,6,8,8,2,12}; //各个活动开始时间,0号元素不用
int finish[maxsize]={0,4,5,6,7,8,9,10,11,12,13,14};  //各个活动的结束时间,0号元素不用


void GreedySelect()  
{
    int i=1,j=1;
    x[j]=1;
    for (i=2;i<=n;i++)
    {
        if (start[i]>finish[j])
        {
            x[i]=1;
            j=i;
        }
    }
}

int main()
{   

    cout<<"活动安排问题:start[i]={1,3,0,5,3,5,6,8,8,2,12},finish[i]={4,5,6,7,8,9,10,11,12,13,14}"<<endl<<endl;
    GreedySelect();
    cout<<"所选的活动是:"<<endl;
    int sum=1;
    for (int i=1;i<=n;i++)
    {
        if (x[i]==1)
        {
            cout<<"("<<sum<<")"<<"开始时间为"<<start[i]<<",结束时间为"<<finish[i]<<"的活动。"<<endl;
            sum++;
        }

    }
    return 0;
}

  贪心算法的第一步往往就是排序。
  

    原文作者:贪心算法
    原文地址: https://blog.csdn.net/king_like_coding/article/details/51865450
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞