数据结构与算法之深入解析常用的七大算法设计策略

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析常用的七大算法设计策略相关的知识,希望对你有一定的参考价值。

一、分治

① 基本思想

  • 在计算机科学中,分治法是一种很重要的算法,字面上的解释是“分而治之”,就是将一个难以直接解决的大问题,分割成 n 个规模较小的子问题,这些子问题相互独立,且与原问题相同,然后各个击破,分而治之。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)等。
  • 能用分治法的基本特征:
    • 问题缩小到一定规模容易解决;
    • 分解成的子问题是相同种类的子问题,即该问题具有最优子结构性质(递归思想);
    • 分解而成的小问题在解决之后要可以合并;
    • 子问题是相互独立的,即子问题之间没有公共的子问题。
  • “分解而成的小问题在解决之后要可以合并”是能分治的关键,解决子问题之后如果不能合并从而解决大问题的话,那么凉凉。如果满足前两个条件,不满足“解决之后合并”,即具有最优子结构的话,可以考虑贪心或者 dp。
  • 如果不满足“子问题是相互独立的,即子问题之间没有公共的子问题”的话,也可以用分治。但是在分治的过程中,有大量的重复子问题被多次的计算,拖慢了算法效率,这样的问题可以考虑 dp(大量重复子问题)。
  • 分治法常常与递归结合使用:通过反复应用分治,可以使子问题与原问题类型一致而规模不断缩小,最终使子问题缩小到很容易求出其解,这和递归算法的思路一致。
  • 根据分治法的分割原则,应把原问题分割成多少个子问题才比较适宜?每个子问题是否规模相同或怎样才为适当?这些问题很难给出肯定的回答。但人们从大量实践中发现,在使用分治法时,最好均匀划分,且在很多问题中可以取 k = 2。这种使子问题规模大致相等的做法源自一种平衡子问题的思想,它几乎总是比使子问题规模不等的做法好。

② 分治步骤

  • 分治法在每一层递归上都有三个步骤:
    • 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
    • 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
    • 合并:将各个子问题的解合并为原问题的解。
  • 化成一颗问题树的话,最底下的就是很多小问题,最上面的就是要解决的大问题,自底向上的方式求解问题。
  • 一般的算法设计模式如下:Divide-and-Conquer( P )
    • if |P|≤n0
    • then return(ADHOC§)
    • 将 P 分解为较小的子问题 P1, P2, …, Pk
    • for i←1 to k
    • do yi ← Divide-and-Conquer(Pi) △ 递归解决 Pi
    • T ← MERGE(y1,y2,…,yk) △ 合并子问题
    • return(T)
  • 说明:
    • |P| 表示问题 P 的规模,n0 为一阈值,表示当问题 P 的规模不超过 n0 时,问题已容易直接解出,不必再继续分解;
    • ADHOC§ 是该分治法中的基本子算法,用于直接解小规模的问题 P,因此当 P 的规模不超过 n0 时直接用算法 ADHOC§ 求解;
    • 算法 MERGE(y1,y2,…,yk) 是该分治法中的合并子算法,用于将 P 的子问题 P1 ,P2 ,…,Pk 的相应的解 y1,y2,…,yk 合并为 P 的解。

③ 分类

  • 根据如何由分解出的子问题得出原始问题的解,分治策略可分为两种情形:
    • 原始问题的解只存在于分解出的某一个(或某几个)子问题中,则只需要在这一(或这几个)子问题中求解即可;
    • 原始问题的解需要由各个子问题的解再经过综合处理得到。

④ 效果

  • 适当运用分治策略往往可以较快地缩小问题求解的范围,从而加快问题求解的速度。子问题最好规模相同;然后对子问题求解;最后合并这些子问题的解,得到原始问题的解。
  • 分治策略运用于计算机算法时,往往会出现分解出来的子问题与原始问题类型相同的现象;而与原始问题相比,各个子问题的尺寸变小了。这刚好符合递归的特性。
  • 因此,计算机算法中的分治策略往往与递归联系在一起。

⑤ 分治的典型应用

  • MAXMIN 问题
  • 二分搜索
  • 归并排序
  • 寻找第 K 小的元素
  • 大整数的乘法
  • Strassen 矩阵乘法
  • 快速排序
  • 二叉树遍历
  • 棋盘覆盖
  • 线性时间选择
  • 最接近点对问题
  • 循环赛日程表
  • 汉诺塔

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

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

⑦ 分治法的复杂性分析

  • 一个分治法将规模为 n 的问题分成 k 个规模为 n/m 的子问题去解:
    • 设分解阀值 n0=1,且 adhoc 解规模为 1 的问题耗费 1 个单位时间;
    • 再设将原问题分解为 k 个子问题以及用 merge 将 k 个子问题的解合并为原问题的解需用 f(n) 个单位时间;
    • 用 T(n) 表示该分治法解规模为 |P|=n 的问题所需的计算时间;
  • 则有:

  • 通过迭代法求得方程的解:

  • 递归方程及其解只给出 n 等于 m 的方幂时 T(n) 的值,但是如果认为 T(n) 足够平滑,那么由 n 等于 m 的方幂时 T(n) 的值可以估计 T(n) 的增长速度;通常假定 T(n) 是单调上升的,从而当 mi≤n<mi+1 时,则 T(mi)≤T(n)<T(mi+1)。

二、减治

① 基本思想

  • 减治技术利用了一种关系:一个问题给定实例的解和同样问题较小实例的解之间的关系(利用解之间的关系,也就是说可以减少相应的计算,也可以说是一种时空平衡)。
  • 有了这种关系,我们可以自顶向下地递归求解,也可以自底向上地迭代实现,从较小实例开始求解这一角度来看减治也叫增量法。
  • 一旦建立了这样一种关系,既可以递归地,也可以非递归地地来运用减治技术。

② 分类

  • 减治法有 3 种主要的变种:
    • 减去一个常量;
    • 减去一个常数因子;
    • 减去的规模是可变的。

③ 减去一个常量

  • 每次算法迭代总是从实例规模中减去一个规模相同的常量,一般来说,这个常量为 1。
  • 函数 f(n) = an 可以用一递归定义来计算:
    • 如果 n > 1,f(n) = f(n-1) * a
    • 如果 n = 1,f(n) = a
  • 虽然时间复杂度和蛮力法一致,但是体现的思想却不一样。

④ 减去常量因子

  • 每次算法迭代总是从实例的规模中减去一个相同的常数因子,在的多数应用中,这样的常数因子为 2。
  • 计算 an 的值是规模为 n 的实例,规模减半(常数因子等于 2)的实例计算就是 an/2 的值,它们之间有着明显的关系:an = (an/2)2
    • n 是正偶数,an = (an/2)2
    • n 是大于 1 的奇数,先提出一个 a 来再减半,an = (a(n-1)/2)2 * a;
    • n = 1,an = a。
  • 上式递归根据所做的乘法次数来度量效率,该算法属于 O(log n);因为每次迭代的时候,以不超过两次乘法为代价,问题的规模至少会减小一半。

⑤ 减可变规模

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

⑥ 算法思想的典型应用

  • 减去一个常量:
    • 插入排序
    • 深度优先查找
    • 广度优先查找
    • 拓扑排序(源删除法对无环有向图进行拓扑排序)
    • 生成排列
    • 生成子集
  • 减去常量因子:
    • 折半查找
    • 假币问题
    • 俄式乘法
    • 约瑟夫斯问题
  • 减可变规模:
    • 插值查找、二叉查找树
    • 欧几里得算法,随着不断求余,n 越来越小

三、变治

  • 基于变换的方法,首先把问题的实例变得容易求解,然后进行求解。
  • 根据对问题实例的变换方式,变治思想有 3 种主要类型:
    • 变换为同样问题的一个更简单或者更方便的实例:实例化简;
    • 变换为同样实例的不同表现:改变表现;
    • 变换为另一个问题的实例, 这种问题的算法是已知的:问题化简。
  • 基于这种思想的算法也有很多,如:预排序(把无序变为有序,然后处理)。

① 实例化简

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

② 改变表现

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

③ 问题化简

  • 背包问题(线性规划)

四、动态规划

  • 将原问题分解成若干个子问题,与分治法不同的是,其分解出的子问题往往不是相互独立的。这种情况下若用分治法会对一些子问题进行多次求解,这显然是不必要的。动态规划法在求解过程中把所有已解决的子问题的答案保存起来,从而避免对子问题重复求解。
  • 动态规划常用于解决最优化问题。对一个最优化问题可否应用动态规划法,取决于该问题是否具有如下两个性质:
    • 最优子结构性质:当问题的最优解包含其子问题的最优解时,称该问题具有最优子结构性质;要证明原问题具有最优子结构性质,通常采用反证法,假设由问题的最优解导出的子问题的解不是最优的,然后再设法说明在该假设下可构造出比原问题的最优解更好的解,从而导致矛盾。
    • 子问题重叠性质:子问题重叠性质是指由原问题分解出的子问题不是相互独立的,存在重叠现象。
  • 用动态规划法解题过程中,应当先找出最优解的结构特征,即原问题的最优解与其子问题的最优解的关联;然后有如下两种程序设计方法:
    • 自底向上递归法:利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。
    • 自顶向下递归法(即备忘录法):
      • 利用问题的最优子结构性质,用与直接递归法相同的控制结构自顶向下地进行递归求解。
      • 初始时在表格中为每个子问题存入一个标识解。
      • 在求解过程中,对每个待求子问题,首先查看表格中相应的记录项。
      • 若记录项为初始时的标识值,则表示该子问题是初次遇到,此时应利用问题的最优子结构性质进行递归求解,并将结果存入表格,以备以后查看。否则则说明该问题已被求解过,直接返回表格中相应的值即可,不必重新计算。
      • 当一个问题的所有子问题都要求解时,应当用自底向上递归法。当子问题空间中的部分子问题可不必求解时,自底向上递归法会进行多余的计算,此时应采用自顶向下递归法。

五、贪心

  • 当一个问题具有最优子结构性质时,可用动态规划法求解。但有时会有比动态规划更简单更直接效率更高的算法:贪心法。
  • 贪心法总是做出在当前看来最好的选择,也就是说贪心法并不从整体最优考虑,它所做出的选择只是在某种意义上的局部最优选择。虽然贪心法并不能对所有问题都得到整体最优解,但是对许多问题它能产生整体最优解。有些情况下,贪心法虽然不能得到整体最优解,但其最终结果却是最优解的很好的近似。
  • 贪心法常用于解决最优化问题。对一个最优化问题可否应用贪心法,取决于该问题是否具有如下两个性质:
    • 贪心选择性质:贪心选择性质是指原问题总有一个整体最优解可通过当前的局部最优选择,即贪心选择来达到。
      对于一个具体问题,要确定它是否具有贪心选择性质,通常可考察问题的一个整体最优解,并证明可修改这个最优解,使其以贪心选择开始。由此证明该问题总有一个最优解可通过贪心选择得到,即具有贪心选择性质。
  • 最优子结构性质:这一点与动态规划相同。做出贪心选择后,由于最优子结构性质,原问题简化为规模更小的类似子问题。如果将子问题的最优解和之前所做的贪心选择合并,则可得到原问题的一个最优解。
  • 贪心问题的整体最优解可通过一系列局部的最优选择,即贪心选择来达到。这也是贪心法与动态规划的主要区别。在动态规划中,每一步所做出的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能做出选择。而在贪心法中,仅做出当前状态下的最好选择,即局部最优选择。然后再去解做出这个选择之后产生的相应的子问题。贪心法所做出的贪心选择可以依赖于以往所做过的选择,但绝不依赖于将来所做的选择,也不依赖于子问题的解。正是由于这种差别,动态规划通常以自顶向上的方式解各子问题,而贪心法通常以自顶向下的方式进行,以迭代的方式做出相继的贪心选择,每做出一次贪心选择就将所求问题简化为规模更小的子问题。
  • 贪心算法的基本思路如下:
    • 建立数学模型来描述问题;
    • 把求解的问题分成若干个子问题;
    • 对每一子问题求解,得到子问题的局部最优解;
    • 把子问题的解局部最优解合成原来解问题的一个解。
  • 实现该算法的过程:
    • 从问题的某一初始解出发;
    • while 能朝给定总目标前进一步 do;
    • 求出可行解的一个解元素;
    • 由所有解元素组合成问题的一个可行解。

六、回溯

  • 回溯法是对问题的解空间树进行深度优先搜索,但是在对每个节点进行 DFS 之前,要先判断该节点是否有可能包含问题的解:
    • 如果肯定不包含,则跳过对以该节点为根的子树的搜索,逐层向其祖先节点回溯;
    • 如果有可能包含,则进入该子树,进行 DFS。
  • 回溯法通常的解题步骤如下:
    • 定义问题的解空间;
    • 将解空间组织成便于进行 DFS 的结构,通常采用树或图的形式;
    • 对解空间进行 DFS,并在搜索过程中用剪枝函数避免无效搜索。
  • 用回溯法解题时并不需要显式地存储整个解空间,而是在 DFS 过程中动态地产生问题的解空间。在任何时刻,算法只保存从根节点到当前节点的路径。如果解空间树的高度为 h,则回溯法的空间复杂度通常为 O(h)。
  • 用回溯法解题时,常会遇到以下两类典型的解空间树:
    • 当所给的问题是从 n 个元素的集合 S 中找出 S 满足某种性质的子集时,相应的解空间树称为子集树,例如 背包问题
  • 回溯法中的剪枝函数通常分为两类:
    • 用约束函数在指定节点处剪去不满足约束的子树,例如 背包问题

七、分支限界法

① 基本思想

  • 分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
  • 在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。
  • 此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。

② 常见的分支限界法

  • 队列式(FIFO)分支限界法:按照队列先进先出(FIFO)原则选取下一个节点为扩展节点。
  • 优先队列式分支限界法:按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。

③ 分支限界法与回溯法的不同

  • 求解目标:回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。
  • 搜索方式的不同:回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。
  • 回溯法是对解空间进行深度优先搜索,事实上任何搜索遍整个解空间的算法均可解决问题。所以采用通用图搜索(树可抽象为特殊的图)的任何实现作为搜索策略均可解决问题,只要做到穷举即可。除了深度优先搜索之外,我们还可采用广度优先搜索,而分支限界法则是对解空间进行优先级优先搜索。
  • 分支限界法的搜索策略是,在当前节点处,先生成其所有的子节点(分支),并为每个满足约束条件的子节点计算一个函数值(限界),再将满足约束条件的子节点全部加入解空间树的活结点优先队列。然后再从当前的活节点优先队列中选择优先级最大的节点(节点的优先级由其限界函数的值来确定) 作为新的当前节点。重复这一过程,直到到达一个叶节点为止。所到达的叶节点就是最优解。

以上是关于数据结构与算法之深入解析常用的七大算法设计策略的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法之深入解析“设计跳表”的求解思路与算法示例

数据结构与算法之深入解析“设计链表”的求解思路与算法示例

数据结构与算法之深入解析“铺瓷砖”的求解思路与算法示例

数据结构与算法之深入解析“TinyURL加密与解密”的求解思路与算法示例

数据结构与算法之深入解析“斐波那契数”的求解思路与算法示例

数据结构与算法之深入解析“最长连续序列”的求解思路与算法示例