算法设计与分析报告

算法分析与设计论文

 

以大学生程序设计竞赛为例

 

 

 

 

姓名:于港添

学    号:20153838

专    业:信息与计算科学

学    校:山东农业大学

授课老师:费玉奎

 

 

 

 

 

前言:

这门课程主要讲了贪心、递归、回溯、分支定界、动态规划等几种算法。

在进行学习之前有做过相关题目,所以在听课的时候感觉好理解了许多。没学这门课的时候总是想因为没学ACM课感到惋惜。

  1.贪心算法

    贪心算法算是DP问题的一个分支了。确定贪心思路,确定贪心标准是其核心。贪心算法的特点在于通过局部最优达到总体最优。也可以理解贪心算法并不是从总体上考虑,它所做出的选择只是在某种意义上的局部最优解算法。

    从全局来看,贪心算法只是每一步都确定当前最优的选择然后进行下一步的选择,并没有回溯过程。所以后面的每一步都是当前看似最佳的选择并不一定能产生整体的最优,这就限制了它只能适用于部分问题的解决。

    贪心算法虽然局限性比较大,但是思虑清晰,运算容易。贪心类的题目,按照一定的贪心标准求解时出现问题则要考虑是否是贪心标准有误。考虑是否缺少某种情况和是否需要更换贪心标准。

    贪心算法可适用于以下几种问题:

      1.0/1背包问题:给定背包容量M,n件物品,物品有属性体积Wi,价值Vi,使得背包所装价值最大。以体积W或者以价值V作为贪心的标准均有缺陷。那么性价比则是最合适的贪心标准。

      2.最优装载问题:  有一批集装箱要装到一艘重量为c的轮船,其中集装箱i的重量为wi,最优装载问题要求确定在装载体积不受限制的情况下,尽可能多的集装箱装上轮船。

      3.连续最值子和问题:(大)给定数组A[ ],求该数组的连续子和的最大值?

      4.单源最短路径问题

      .......

  2.递归算法

    说实话,我感觉递归算法是最高深最有操作的一种算法。为什么这么说?DFS算法也可以看成一种递归。而且递归也可以用来DP,记得Project Euler做过一道题,题意大体是:给定一个整数n,n可以被表示为下述多种情况

        1+1+1+1+.....+1(n个1相加)

        2+1+1+.....+1

        2+2+.....+1

        .......

        (n-1)+1

        n

    也就是说n可以被表示成任意个正整数的和,求可以被表示的情况数。每个阶段都是在拆分n,  n-1和1,n-2和2........n-x和x。拆成n-x和x的时候,计算n-x的情况数和x的情况数。很讨厌的一道题。可不得不说,递归就是牛。还有一道数乘积的问题,也是用递归求解,像这种题目做起来应该会不好受。

    话说回来,递归算法就是在函数体内重复调用该函数。所以一定要先想好思路,递归的出口、满足各种条件该进行的操作。

    递归操作有不少,有许多普通操作和“骚操作“,像一些普通的数学公式、斐波那契数列、阶乘或回溯。这类一眼大体能看出来的就比较容易想,不太会迷糊。确定出口就可以了。所谓的“骚操作”就比如上述的例子。情况多变,求解特别复杂的那种。

    设计思路:

      递归算法的设计思路就是要解决一个规模为n的问题,先看规模为n-1(或者n-k或者n/2,总之是比原问题规模小)的问题是否和原问题有同样的性质(缩小问题规模),如果性质相同,那这个问题应该可以用递归算法解决。

    递归算法可适用以下几种问题:

      1.数学公式(例:阶乘n!)

        int fun(int n)

        {    if(n==1) return 1;

          return n*fun(n-1);

        }

      2.汉诺塔

        汉诺塔问题比较常见,有n个大小不等的盘子放在一个塔A上面,自上而下按照从小到大的顺序排列。要求将所有n个盘子搬到另一个塔C上面,可以借助一个塔B中转,但是要满足任何时刻大盘子不能放在小盘子上面。

        基本思想,当前步未解决的小盘子的塔,中转塔,已解决的塔。先把上面的N-1个盘子经C移到B,然后将最底下的盘子移到C,再讲B上面的N-1个盘子经A移动到C。可以看作每次只考虑当前的未解决的最大盘子。

      3.树形数据结构

        二叉树、线段树、树状数组建树。树的一系列算法这里就不详举了。

      4.“骚操作”系列

    3.回溯算法

      回溯法一直被我理解为深度优先遍历(DFS),也是一种剪枝枚举法,归为暴力。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。用俗语来说就是不到黄河不死心,再加上限界函数让它早点见到黄河。回溯法的灵魂:恢复现场!!!

      回溯法分为递归型和非递归型,但不论哪一种都要在搜索中恢复现场。说到递归型那就很典型了,各种走迷宫。这种题型非常好理解,就是上述说的找黄河嘛。非递归型的例如:给你一串数字123,你要将它分开,分成至多有两个数相连 [["1","2","3"],["12","3"],["1","23"]]。贴上代码:

      

class Solution {

public:

    vector <string> a;

    vector<vector<string> > ans;

    void back(int t,int n,string &s)

    {

        if(!s.size()){

          ans.push_back(a);

          return ;

        }

        if(t>=s.size())    

          return ;

        else{

          if(n==1){

            string c="";

            c=c+s[t];

            a.push_back(c);

            if(t+1==s.size()){

              ans.push_back(a);

            }

            t++;

            back(t,1,s);

            t++;

            back(t,2,s);

            t-=2;

            a.erase(a.end()-1);

        }

          else{

            string c="";

            c=c+s[t-1]+s[t];

            a.push_back(c);

            if(t+1==s.size()){

               ans.push_back(a);

            }

            t++;

            back(t,1,s);

            t++;

            back(t,2,s);

            t-=2;

            a.erase(a.end()-1);

            }

        }

    return ;   

    }

    vector<vector<string> > splitString(string& s) {

        back(0,1,s);

        if(s.size()>1)

            back(1,2,s);

        return ans;

    }

};

      回溯法无疑可以得到正确答案,可其缺点就是复杂度太高,很多题型数据一大就不能用了。  //注意爆栈

  4.分支定界法

    分支定界 (branch and bound) 算法是一种在问题的解空间树上搜索问题的解的方法。但与回溯算法不同,分支定界算法采用广度优先或最小耗费优先的方法搜索解空间树,并且,在分支定界算法中,每一个活结点只有一次机会成为扩展结点。

利用分支定界算法对问题的解空间树进行搜索,它的搜索策略是:

      1 .产生当前扩展结点的所有孩子结点;

      2 .在产生的孩子结点中,抛弃那些不可能产生可行解(或最优解)的结点;

      3 .将其余的孩子结点加入活结点表;

      4 .从活结点表中选择下一个活结点作为新的扩展结点。

    如此循环,直到找到问题的可行解(最优解)或活结点表为空。

    从活结点表中选择下一个活结点作为新的扩展结点,根据选择方式的不同,分支定界算法通常可以分为两种形式:

      1 . FIFO(First In First Out) 分支定界算法:按照先进先出原则选择下一个活结点作为扩展结点,即从活结点表中取出结点的顺序与加入结点的顺序相同。

      2 .最小耗费或最大收益分支定界算法:在这种情况下,每个结点都有一个耗费或收益。如果要查找一个具有最小耗费的解,那么要选择的下一个扩展结点就是活结点表中具有最小耗费的活结点;如果要查找一个具有最大收益的解,那么要选择的下一个扩展结点就是活结点表中具有最大收益的活结点。

     分支定界法的典型问题也是迷宫求最小步数等等,有时也可以用来对数据进行预处理。

    相信刚开始接触搜索算法的人,都做过类似迷宫这样的题目吧。我们在“走迷宫”的时候,一般回溯法思路是这样的:   

      1、这个方向有路可走,我没走过  
      2、往这个方向前进  
      3、是死胡同,往回走,回到上一个路口  
      4、重复第一步,直到找着出口  
    这样的思路很好理解,编程起来也比较容易。但是当迷宫的规模很大时,回溯法的缺点便暴露无遗:搜索耗时极巨,无法忍受。  

    回溯法与分支定界法都属搜索算法,也都经常用到剪枝函数,剪枝函数用来减少非最优或者不正确结果的搜索来提高效率。剪枝的原则: 
      1、正确性  
      正如上文所述,枝条不是爱剪就能剪的。如果随便剪枝,把带有最优解的那一分支也剪掉了的话,剪枝也就失去了意义。所以,剪枝的前提是一定要保证不丢失正确的结果。  
      2、准确性  
      在保证了正确性的基础上,我们应该根据具体问题具体分析,采用合适的判断手段,使不包含最优解的枝条尽可能多的被剪去,以达到程序最优化的目的。可以说,剪枝的准确性,是衡量一个优化算法好坏的标准。  
      3、高效性   设计优化程序的根本目的,是要减少搜索的次数,使程序运行的时间减少。但为了使搜索次数尽可能的减少,我们又必须花工夫设计出一个准确性较高的优化算法,而当算法的准确性升高,其判断的次数必定增多,从而又导致耗时的增多,这便引出了矛盾。  
    因此,如何在优化与效率之间寻找一个平衡点,使得程序的时间复杂度尽可能降低,同样是非常重要的。倘若一个剪枝的判断效果非常好,但是它却需要耗费大量的时间来判断、比较,结果整个程序运行起来也跟没有优化过的没什么区别,这样就太得不偿失了。   
    综上所述,我们可以把剪枝优化的主要原则归结为六个字:正确、准确、高效。   
    5.动态规划

    动态规划算法的基本思想是:自顶向下(多阶段决策)将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果,与贪心算法不同的是,在贪心算法中,每采用一次贪心标准,便做出一个不可撤回的决策;而在动态规划算法中,还要考察每个最优决策序列中是否包含一个最优决策子序列,即问题是否具有最优子结构性质。 

    动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

    在我的理解中,其实就是有动态判断条件(而不是只有终止条件)的递归或循环。就比如典型的N皇后问题,在N*N的棋盘上,放置N个皇后,使其不同行不同列,不同对角。他的思想是每扩展棋盘一圈(N增大1)都根据前一状态N来计算,所以当然也是可以用DP的,但是会有很多重复的情况,所以更适合于用回溯,把不合格的情况提前掐掉。

    另外一个典型的问题就是上楼梯,有N级台阶,每次只能上一步或者两步,求到第N阶台阶有多少种策略。当前解取决于上一步和上两步的解之和。

    其实根据这两道题目就可以发现,动态规划是一种解决问题的思想,而不是像搜索等算法有固定解题方法。

 

 

posted @ 2017-11-02 20:01  会飞的雅蠛蝶  阅读(2359)  评论(0编辑  收藏  举报