算法设计策略

一、分治

1.1 基本思想

将一个难以直接解决的大问题,分割成 n 个规模较小的子问题,这些子问题相互独立,且与原问题相同,然后各个击破,分而治之

能用分治法的基本特征:

  1. 问题缩小到一定规模容易解决
  2. 分解成的子问题是相同种类的子问题,即该问题具有最优子结构性质(递归思想)
  3. 分解而成的小问题在解决之后要可以合并
  4. 子问题是相互独立的,即子问题之间没有公共的子问题

第 3 条是能分治的关键。解决子问题之后如果不能合并从而解决大问题的话,那么凉凉,如果满足一、二,不满足三,即具有最优子结构的话,可以考虑贪心或者 dp。

如果不满足第 4 条的话,也可以用分治。但是在分治的过程中,有大量的重复子问题被多次的计算,拖慢了算法效率,这样的问题可以考虑 dp(大量重复子问题)。

分治法常常与递归结合使用:通过反复应用分治,可以使子问题与原问题类型一致而规模不断缩小,最终使子问题缩小到很容易求出其解,这和递归算法的思路一致。

根据分治法的分割原则,应把原问题分割成多少个子问题才比较适宜?每个子问题是否规模相同或怎样才为适当?这些问题很难给出肯定的回答。但人们从大量实践中发现,在使用分治法时,最好均匀划分,且在很多问题中可以取 k = 2。这种使子问题规模大致相等的做法源自一种平衡子问题的思想,它几乎总是比使子问题规模不等的做法好。

1.2 分治步骤

  1. 分解成很多子问题
  2. 解决这些子问题
  3. 将解决的子问题合并从而解决整个大问题

化成一颗问题树的话,最底下的就是很多小问题,最上面的就是要解决的大问题,自底向上的方式求解问题。

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

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)

1.3 分类

根据如何由分解出的子问题得出原始问题的解,分治策略可分为两种情形:

  1. 原始问题的解只存在于分解出的某一个(或某几个)子问题中,则只需要在这一(或这几个)子问题中求解即可;
  2. 原始问题的解需要由各个子问题的解再经过综合处理得到。

1.4 效果

适当运用分治策略往往可以较快地缩小问题求解的范围,从而加快问题求解的速度。子问题最好规模相同;然后对子问题求解;最后合并这些子问题的解,得到原始问题的解。

分治策略运用于计算机算法时,往往会出现分解出来的子问题与原始问题类型相同的现象;而与原始问题相比,各个子问题的尺寸变小了。这刚好符合递归的特性

因此,计算机算法中的分治策略往往与递归联系在一起。

1.5 算法的典型应用

  1. MAXMIN问题
  2. 二分搜索
  3. 归并排序
  4. 寻找第 K 小的元素
  5. 大整数的乘法
  6. Strassen 矩阵乘法
  7. 快速排序
  8. 二叉树遍历
  9. 棋盘覆盖
  10. 线性时间选择
  11. 最接近点对问题
  12. 循环赛日程表
  13. 汉诺塔。。。

1.6 依据分治法设计程序时的思维过程

实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。

  1. 一定是先找到最小问题规模时的求解方法;
  2. 然后考虑随着问题规模增大时的求解方法;
  3. 找到求解的递归函数式后(各种规模或因子),设计递归程序即可。

文章:分治算法详解

二、减治

2.1 基本思想

减治技术利用了一种关系:一个问题给定实例的解和同样问题较小实例的解之间的关系。(利用解之间的关系,也就是说可以减少相应的计算,也可以说是一种时空平衡)

有了这种关系,我们可以自顶向下地递归求解,也可以自底向上地迭代实现,从较小实例开始求解这一角度来看减治也叫增量法

一旦建立了这样一种关系,既可以递归地,也可以非递归地地来运用减治技术。

2.2 分类

减治法有 3 种主要的变种:

  1. 减去一个常量
  2. 减去一个常数因子
  3. 减去的规模是可变的

2.3 减去一个常量

每次算法迭代总是从实例规模中减去一个规模相同的常量。一般来说,这个常量为 1。

函数 f(n) = an 可以用一递归定义来计算

f(n) = f(n-1) * a   如果 n > 1

f(n) = a        如果 n = 1

虽然时间复杂度和蛮力法一致,但是体现的思想却不一样!

2.4 减去常量因子

每次算法迭代总是从实例的规模中减去一个相同的常数因子。在的多数应用中,这样的常数因子为 2。

计算 an 的值是规模为 n 的实例,规模减半(常数因子等于 2)的实例计算就是 an/2 的值;它们之间有着明显的关系: an = (an/2)2

an = (an/2)2                       n 是正偶数

an = (a(n-1)/2)2 * a     n 是大于 1 的奇数,先提出一个 a 来再减半

an = a               n = 1

上式递归根据所做的乘法次数来度量效率,该算法属于 O(log n);

因为每次迭代的时候,以不超过两次乘法为代价,问题的规模至少会减小一半。

2.5 减可变规模

每次算法迭代时,规模减小的模式都是不同的。例如:欧几里德算法

2.6 算法思想的典型应用

减去一个常量:

  1. 插入排序
  2. 深度优先查找
  3. 广度优先查找
  4. 拓扑排序(源删除法对无环有向图进行拓扑排序)
  5. 生成排列
  6. 生成子集

减去常量因子:

  1. 折半查找
  2. 假币问题
  3. 俄式乘法
  4. 约瑟夫斯问题

减可变规模

  1. 插值查找、二叉查找树
  2. 欧几里得算法,随着不断求余,n 越来越小

三、分治减治区别

分治法:求解多个子问题(每个子问题都需要求解),合并子问题的解。

减治法:求解一个子问题(子问题只需要求解一次),扩展子问题的解。

求 an

分治法:a(n/2) * a(n/2) 则左右两个 a(n/2) 均需要求解,所以时间复杂度为 O(n)。

减治法:(a(n/2))2 则只需要求 a(n/2) 再平方即可,所以时间复杂度为 O(lgn)。

也就是说分治法是分解的部分需要进行分开的单独计算(需要计算两遍),而减治法则利用了“一个问题给定实例的解和同样问题较小实例的解之间的关系”从而减少了计算量

四、变治

基于变换的方法,首先把问题的实例变得容易求解,然后进行求解。根据对问题实例的变换方式,变治思想有 3 种主要类型:

  • 变换为同样问题的一个更简单或者更方便的实例:实例化简
  • 变换为同样实例的不同表现:改变表现
  • 变换为另一个问题的实例, 这种问题的算法是已知的:问题化简

基于这种思想的算法也有很多,如:预排序(把无序变为有序,然后处理)。

4.1 实例化简

  1. 检验数组中元素的唯一性(预排序)
  2. 模式计算(预排序)
  3. AVL 树

4.2 改变表现

  1. 2-3 树、2-3-4 树(二叉排序树)
  2. 堆和堆排序(利用最大/小堆总是找到最大/小值)
  3. 霍纳法则(多项式的计算)
  4. 高斯消去法(把方程组经过初等变换,得到具有特殊性质的方程组)

4.3 问题化简

  1. 背包问题(线性规划)

五、动态规划

将原问题分解成若干个子问题。与分治法不同的是,其分解出的子问题往往不是相互独立的。这种情况下若用分治法会对一些子问题进行多次求解,这显然是不必要的。动态规划法在求解过程中把所有已解决的子问题的答案保存起来,从而避免对子问题重复求解。

动态规划常用于解决最优化问题。对一个最优化问题可否应用动态规划法,取决于该问题是否具有如下两个性质:

  1. 最优子结构性质

    当问题的最优解包含其子问题的最优解时,称该问题具有最优子结构性质。

    要证明原问题具有最优子结构性质,通常采用反证法。假设由问题的最优解导出的子问题的解不是最优的,然后再设法说明在该假设下可构造出比原问题的最优解更好的解,从而导致矛盾。

  2. 子问题重叠性质

    子问题重叠性质是指由原问题分解出的子问题不是相互独立的,存在重叠现象。

用动态规划法解题过程中,应当先找出最优解的结构特征,即原问题的最优解与其子问题的最优解的关联。然后有如下两种程序设计方法:

  1. 自底向上递归法

    利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。

  2. 自顶向下递归法(即备忘录法)

    利用问题的最优子结构性质,用与直接递归法相同的控制结构自顶向下地进行递归求解。初始时在表格中为每个子问题存入一个标识解。在求解过程中,对每个待求子问题,首先查看表格中相应的记录项。若记录项为初始时的标识值,则表示该子问题是初次遇到,此时应利用问题的最优子结构性质进行递归求解,并将结果存入表格,以备以后查看。否则则说明该问题已被求解过,直接返回表格中相应的值即可,不必重新计算。

    当一个问题的所有子问题都要求解时,应当用自底向上递归法。当子问题空间中的部分子问题可不必求解时,自底向上递归法会进行多余的计算,此时应采用自顶向下递归法。

文章:动态规划:从新手到专家

六、贪心

当一个问题具有最优子结构性质时,可用动态规划法求解。但有时会有比动态规划更简单更直接效率更高的算法:贪心法。

贪心法总是做出在当前看来最好的选择,也就是说贪心法并不从整体最优考虑,它所做出的选择只是在某种意义上的局部最优选择。虽然贪心法并不能对所有问题都得到整体最优解,但是对许多问题它能产生整体最优解。有些情况下,贪心法虽然不能得到整体最优解,但其最终结果却是最优解的很好的近似。

贪心法常用于解决最优化问题。对一个最优化问题可否应用贪心法,取决于该问题是否具有如下两个性质:

  1. 贪心选择性质

    贪心选择性质是指原问题总有一个整体最优解可通过当前的局部最优选择,即贪心选择来达到。

    对于一个具体问题,要确定它是否具有贪心选择性质,通常可考察问题的一个整体最优解,并证明可修改这个最优解,使其以贪心选择开始。由此证明该问题总有一个最优解可通过贪心选择得到,即具有贪心选择性质。

  2. 最优子结构性质

    这一点与动态规划相同。做出贪心选择后,由于最优子结构性质,原问题简化为规模更小的类似子问题。如果将子问题的最优解和之前所做的贪心选择合并,则可得到原问题的一个最优解。

贪心问题的整体最优解可通过一系列局部的最优选择,即贪心选择来达到。这也是贪心法与动态规划的主要区别。在动态规划中,每一步所做出的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能做出选择。而在贪心法中,仅做出当前状态下的最好选择,即局部最优选择。然后再去解做出这个选择之后产生的相应的子问题。贪心法所做出的贪心选择可以依赖于以往所做过的选择,但绝不依赖于将来所做的选择,也不依赖于子问题的解。正是由于这种差别,动态规划通常以自顶向上的方式解各子问题,而贪心法通常以自顶向下的方式进行,以迭代的方式做出相继的贪心选择,每做出一次贪心选择就将所求问题简化为规模更小的子问题。

七、回溯

回溯法是对问题的解空间树进行深度优先搜索 ,但是在对每个节点进行 DFS 之前,要先判断该节点是否有可能包含问题的解。如果肯定不包含,则跳过对以该节点为根的子树的搜索,逐层向其祖先节点回溯。如果有可能包含,则进入该子树,进行 DFS。

回溯法通常的解题步骤如下:

  1. 定义问题的解空间。
  2. 将解空间组织成便于进行 DFS 的结构,通常采用树或图的形式。
  3. 对解空间进行 DFS,并在搜索过程中用剪枝函数避免无效搜索。

用回溯法解题时并不需要显式地存储整个解空间,而是在 DFS 过程中动态地产生问题的解空间。在任何时刻,算法只保存从根节点到当前节点的路径。如果解空间树的高度为 h,则回溯法的空间复杂度通常为 O(h)

用回溯法解题时,常会遇到以下两类典型的解空间树:

  1. 当所给的问题是从 n 个元素的集合 S 中找出 S 满足某种性质的子集时,相应的解空间树称为子集树,例如 0-1背包问题
  2. 当所给的问题是找出 n 个元素满足某种性质的排列时,相应的解空间树称为排列树,例如 回溯法解旅行推销员问题

回溯法中的剪枝函数通常分为两类:

  1. 用约束函数在指定节点处剪去不满足约束的子树,例如 0-1背包问题
  2. 用限界函数在指定节点处剪去得不到最优解的子树,例如回溯法解旅行推销员问题

八、分支限界

回溯法是对解空间进行深度优先搜索,事实上任何搜索遍整个解空间的算法均可解决问题。所以采用通用图搜索(树可抽象为特殊的图)的任何实现作为搜索策略均可解决问题,只要做到穷举即可。除了深度优先搜索之外,我们还可采用广度优先搜索,而分支限界法则是对解空间进行优先级优先搜索。

分支限界法的搜索策略是,在当前节点处,先生成其所有的子节点(分支),并为每个满足约束条件的子节点计算一个函数值(限界),再将满足约束条件的子节点全部加入解空间树的活结点优先队列。然后再从当前的活节点优先队列中选择优先级最大的节点(节点的优先级由其限界函数的值来确定) 作为新的当前节点。重复这一过程,直到到达一个叶节点为止。所到达的叶节点就是最优解。

九、内容来源

姚来飞的博客 & 常见的算法设计策略
算法设计之五大常用算法设计方法总结
减治、分治与变治

posted @ 2020-03-15 13:50  和风细羽  阅读(918)  评论(0编辑  收藏  举报