首页 > 代码库 > 五大算法

五大算法

                                 五大算法

1.贪心法( Greedy algorithm),又称贪心算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。


1. 基本概念

贪心算法与动态规划的不同在于它每对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。

所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。

贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。


2. 适用范围

贪心策略适用的前提是:局部最优策略能导致产生全局最优解
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断


3. 贪心法的步骤

1. 建立数学模型来描述问题。
2. 把求解的问题分成若干个子问题。
3. 对每一子问题求解,得到子问题的局部最优解。

4. 把子问题的解局部最优解合成原来解问题的一个解。


2.动态规划(英语:Dynamic programming,DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。


1. 基本概念

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。


2. 适用范围

基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。


3. 动态规划的步骤

1. 最优子结构性质。

   如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。 
2. 子问题重叠性质。

  子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。


4. 典型问题

(1)钢条切割

(2)最长公共子序列

(3)最优二叉搜索树

(4)最长回文字串

(5)0-1背包问题

斐波那契数列(Fibonacci polynomial)[编辑]

计算斐波那契数列(Fibonacci polynomial)的一个最基础的算法是,直接按照定义计算:

   function fib(n)
       if n = 0 or n = 1
           return 1
       return fib(n ? 1) + fib(n ? 2)

当n=5时,fib(5)的计算过程如下:

  1. fib(5)
  2. fib(4) + fib(3)
  3. (fib(3) + fib(2)) + (fib(2) + fib(1))
  4. ((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
  5. (((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

由上面可以看出,这种算法对于相似的子问题进行了重复的计算,因此不是一种高效的算法。实际上,该算法的运算时间是指数级增长的。 改进的方法是,我们可以通过保存已经算出的子问题的解来避免重复计算:

array map [0...n] = { 0 => 0, 1 => 1 }
fib(n)
    if(map m does not contain key n)
        m[n] := fib(n ? 1) + fib(n ? 2)
    return m[n]

将前n个已经算出的数保存在数组map中,这样在后面的计算中可以直接应用前面的结果,从而避免了重复计算。算法的运算时间变为O(n)

背包问题[编辑]

背包问题作为NP完全问题,暂时不存在多项式时间算法。动态规划属于背包问题求解最优解的可行方法之一。此外,求解背包问题最优解还有搜索法等,近似解还有贪心法等,分数背包问题有最优贪心解等。 背包问题具有最优子结构和重叠子问题。动态规划一般用于求解背包问题中的整数背包问题(即每种物品所选的个数必须是整数)。 解整数背包问题: 设有n件物品,每件价值记为Pi,每件体积记为Vi,用一个最大容积为Vmax的背包,求装入物品的最大价值。 用一个数组f[i,j]表示取i件商品填充一个容积为j的背包的最大价值,显然问题的解就是f[n,Vmax].

f[i,j]=

      f[i-1,j] {j<Vi}
      max{f[i-1,j],f[i,j-Vi]+Pi} {j>=Vi}
      0 {i=0 OR j=0}

对于特例01背包问题(即每件物品最多放1件,否则不放入)的问题,状态转移方程:

f[i,j]=

      f[i-1,j] {j<Vi}
      max{f[i-1,j],f[i-1,j-Vi]+Pi} {j>=Vi}
      0 {i=0 OR j=0}

3.分治法(Divide and conquer的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

1. 基本概念

字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如快速排序,归并排序等。


2. 适用范围

分治法所能解决的问题一般具有以下几个特征:

    1) 该问题的规模缩小到一定的程度就可以容易地解决

    2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

    3) 利用该问题分解出的子问题的解可以合并为该问题的解;

    4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好


3. 分治法的步骤

分治法在每一层递归上都有三个步骤:

    step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

    step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题

    step3 合并:将各个子问题的解合并为原问题的解。

它的一般的算法设计模式如下:

 Divide-and-Conquer(P)

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  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的解。


4. 复杂度分析

参见http://blog.sina.com.cn/s/blog_48258fbe0100gcy8.html


5. 典型问题

(1)二分搜索

(2)大整数乘法

 (3)Strassen矩阵乘法

(4)棋盘覆盖

(5)合并排序

(6)快速排序

(7)线性时间选择

(8)最接近点对问题

(9)循环赛日程表

(10)HANOI塔

4.回溯法(英语:backtracking)是暴力搜寻法中的一种

回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
1.找到一个可能存在的正确的答案。
2.在尝试了所有可能的分步方法后宣告该问题没有答案。

在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。


1. 基本概念

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。


2. 适用范围

在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。

若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
若用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。


3. 回溯法的步骤

(1)针对所给问题,确定问题的解空间:首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。 


4. 典型问题

八皇后问题

一个实现:http://blog.sina.com.cn/s/blog_3fe961ae0100zaph.html


5. 基本框架

设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,…..,n)之间满足某种条件,记为f(ai)。

非递归实现

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int a[n],i;  
  2. 初始化数组a[];  
  3. i = 1;  
  4. while (i>0(有路可走)   and  (未达到目标))  // 还未回溯到头  
  5. {  
  6.     if(i > n)                                              // 搜索到叶结点  
  7.     {     
  8.           搜索到一个解,输出;  
  9.     }  
  10.     else                                                   // 处理第i个元素  
  11.     {   
  12.          a[i]第一个可能的值;  
  13.          while(a[i]在不满足约束条件且在搜索空间内)  
  14.          {  
  15.               a[i]下一个可能的值;  
  16.          }  
  17.         if(a[i]在搜索空间内)  
  18.         {  
  19.                i = i+1;                              // 扩展下一个结点  
  20.         }  
  21.         else   
  22.         {  
  23.              清理所占的状态空间;            // 回溯  
  24.               i = i –1;   
  25.         }  
  26. }  

5.分枝界限法(Branch and Bound Method是由三栖学者查理德·卡普(Richard M.Karp)在20世纪60年代发明,成功求解含有65个城市的旅行商问题,创当时的记录。“分枝界限法”把问题的可行解展开如树的分枝,再经由各个分枝中寻找最佳解。

1. 基本概念

描述:采用广度优先产生状态空间树的结点,并使用剪枝函数的方法称为分枝限界法。
所谓“分支”是采用广度优先的策略,依次生成扩展结点的所有分支(即:儿子结点)。
所谓“限界”是在结点扩展过程中,计算结点的上界(或下界),边搜索边减掉搜索树的某些分支,从而提高搜索效率。


2. 算法的步骤

步骤一:如果问题的目标为最小化,则设定目前最优解的值Z=∞
步骤二:根据分枝法则(Branching rule),从尚未被洞悉(Fathomed)节点(局部解)中选择一个节点,并在此节点的下一阶层中分为几个新的节点。
步骤三:计算每一个新分枝出来的节点的下限值(Lower bound,LB)
步骤四:对每一节点进行洞悉条件测试,若节点满足以下任意一个条件,则此节点可洞悉而不再被考虑:
此节点的下限值大于等于Z值。
已找到在此节点中,具最小下限值的可行解;若此条件成立,则需比较此可行解与Z值,若前者较小,则需更新Z值,以此为可行解的值。
此节点不可能包含可行解。
步骤五:判断是否仍有尚未被洞悉的节点,如果有,则进行步骤二,如果已无尚未被洞悉的节点,则演算停止,并得到最优解。


3. 分支限界法与回溯法

回溯法深度优先搜索堆栈活结点的所有可行子结点被遍历后才被从栈中弹出找出满足约束条件的所有解。
分支限界法广度优先或最小消耗优先搜索队列、优先队列每个结点只有一次成为活结点的机会找出满足约束条件的一个解或特定意义下的最优解。

(1)求解目标:回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。 
(2)搜索方式的不同:回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。


4. 典型问题

(1)货郎担问题

(2)旅行推销员问题


五大算法