用自底向上算法为一组整数构造一个大根堆。_动态规划算法Dynamic Programming

5c62900b03e67d06209f3af30222722a.png

动态规划与分治法相似,都是通过组合子问题的解来求解原问题。不同的是,分治法将问题划分为互不相交的子问题,递归的求解子问题,再将他们的解组合起来,求出原问题的解。与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。在这种情况下,分治法会做许多不必要的工作,他会反复求解那些公共子子问题。而动态规划只会对子子问题求解一次,将其保存在一个表格中,从而避免每次求解时都重新计算。现在我们来逐步的优化一个经典的问题——最长公共子序列,从中分析动态规划的思想方法。

0.问题描述:

最长公共子序列问题(longest - common - subsequence problem)给定两个序列X = 和Y = ,求X和Y的长度最长的公共子序列。

1.暴力搜索方法:

我们把问题分割成两个部分:遍历和判断。首先遍历X的所有的子序列,然后依次进行判断是否为Y的子序列,并比较得出其中最长的一个。

对于判断,可以从左向右扫描一遍即可实现,复杂度为O(n):

/**   * 判断sub是否为a的子序列   * @param a   * @param sub   * @return   */   private static boolean isSubsequence(char[] a, char[] sub) {       int i = 0, j = 0;       while (i 

对于遍历,一个具有m个字符的序列,一共有2^m个子序列。然后逐一进行判断比较,这个算法的时间复杂度为O(2^m)*O(n).

我们可以借助深度优先搜索的思想进行子序列的遍历:

/**   * 暴力穷举的方法取最长公共子序列   * 遍历一个字符串的所有的子序列,共有2^n种结果   */   private static void bruteforceForLCS(char[] chars1, char[] chars, String res, int k) {       if (k 

2.基于最优子结构性质的改进


基本上,任何问题都是可以使用暴力搜索的方式去解决的,但可怕的是,当问题的规模达到一定的程度以后,即使使用最快的计算机,它也需要若干年甚至是若干世纪以后才能给出我们答案。人生苦短,暴力不值得。

通过对公共子序列形成规律的深入分析,我们发现这样的性质:

X = ,Y = ,令Z = 位X和Y的任意最长公共子序列,则有:

如果Xm = Yn,那么Zk = Xm = Yn且Zk-1是Xm-1和Yn-1的一个最长公共子序列;如果Xm!= Yn,那么Zk!= Xm意味着Z是Xm-1和Y的一个最长公共子序列;如果Xm!= Yn,那么Zk!= Yn意味着Z是Xm和Yn-1的一个最长公共子序列;所以,我们用这个规律来建立一个描述最长公共子序列长度的递归公式:

有了这个递归公式,我们就可以写出一个指数时间的算法来进行递归计算了:

/**   * 直接根据递归公式进行求解最长公共子序列   * @return   */   private static int dpForLCS(char[] a, char[] b, int i, int j, Stack stack, boolean[] aflag) {       count++;       System.out.println("递归程序正在执行i=" + i + ",j=" + j);       if (i == -1 || j == -1) {           return 0;      } else {           if (a[i] == b[j]) {               if (!aflag[i]) {                   stack.push(a[i]);                   aflag[i] = true;              }               return dpForLCS(a, b, i - 1, j - 1, stack, aflag) + 1;          } else {               return Math.max(dpForLCS(a, b, i - 1, j, stack, aflag), dpForLCS(a, b, i, j - 1, stack, aflag));          }      }  }

3.基于缓存思想的动态规划算法


可以看出上面直接根据递归公式进行求解的方法会有很多的重复计算,比如为了求X和Y的一个LCS,我们可能需要求X和Yn-1的一个LCS以及Xm-1和Y的一个LCS,这几个问题都会包含Xm-1和Yn-1的LCS这个子问题,对于这种情况,我们将它描述为具有重叠子问题性质。解决的思路是借助缓存的思想,将已经计算出的子问题存储在一个表格里以避免重复进行计算。一般的,可以有如下的递归和迭代两种实现方式。

3.1自顶向下的递归法(top-down with memoization)

此方法依然按照自然的递归形式编写函数,但函数中会保存每个字问题的解(通常保存在一个数组或者散列表中)。当需要一个子问题的解时,函数会首先检查是否保存过此解。如果是,直接返回保存的值,从而节省了计算时间。

/**   * 自顶向下法(递归形式的动态规划)   */   private static int dpForLCSDown(char[] a, char[] b, int i, int j, int[][] length) {       if (i == 0 || j == 0) {           return 0;      } else {           if (a[i] == b[j]) {               length[i][j] = (length[i][j] == -1 ? (dpForLCSDown(a, b, i - 1, j - 1, length) + 1) : length[i][j]);               return length[i][j];          } else {               length[i - 1][j] = length[i - 1][j] == -1 ? dpForLCSDown(a, b, i - 1, j, length) : length[i - 1][j];               length[i][j - 1] = length[i][j - 1] == -1 ? dpForLCSDown(a, b, i, j - 1, length) : length[i][j - 1];               return Math.max(length[i - 1][j], length[i][j - 1]);          }      }  }

3.2自底向上的迭代法(bottom-up method)

把子问题按照规模进行排序,按照由小至大的顺序进行求解。当解决某个子问题时,它所依赖的更小的子问题都已经求解完毕,结果已经保存,每个子问题都只需求解一次。

/**   * 自底向上法(迭代形式的动态规划)   */   private static int dpForLCSUp(char[] a, char[] b) {       int[][] length = new int[a.length][b.length];       for (int i = 0; i 

上述两种方法具有相同的渐进运行时间,但是由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂度函数通常具有更小的系数。

4.总结

从上面的讨论过程中,我们可以看到适用动态规划算法求解的最优化问题应该具有两个要素:最优子结构性质和子问题重叠。首先,最优子结构性质可是使我们避免暴力遍历所有的子序列;其次,子问题重叠性质可以使我们借助缓存思想,避免对相同的子问题进行重复求解,从而达到比传统分治法更优的性能。最后,我们提炼出设计一个动态规划算法的步骤:

刻画一个最优解的结构特征;递归的定义最优解的值;计算最优解的值,通常采用自顶向上的方法;利用计算出的信息构造一个最优解;

参考文献: Thomas H.Cormen Charles E.Leiserson.Introduction To Algorithms (Third Edition) [M]. China Machine Press, 2013


文章首发于ruixuchen.github.io,请点击阅读原文

了解更多,欢迎关注公众号:经管微报

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
├──Package │ ├── Sort 排序篇 │ │ ├── BubbleSort.php 冒泡排序 │ │ ├── HeapSort.php 堆排序 大根堆 │ │ ├── MBaseSort.php 基数排序 MSD │ │ ├── LBaseSort.php 基数排序 LSD │ │ ├── QuickSort.php 快速排序 │ │ ├── ShuttleSort.php 飞梭排序 │ │ ├── ShellSort.php 希尔排序 │ │ ├── MergeSort.php 归并排序 │ │ ├── InsertSort.php 插入排序 │ │ └── SelectSort.php 选择排序 │ │ │ ├── Query 查找篇 │ │ ├── BinaryQuery.php 二分查找 │ │ ├── InseertQuery.php 插入查找 │ │ ├── FibonacciQuery.php 斐波那契查找 │ │ ├── BFSQuery.php 广度优先查找 │ ├── Kmp.php 算法导论-KMP算法 │ ├── DijkstraQuery.php 迪克斯特拉算法 │ │ └── QulickQuery.php 快速查找 │ │ │ ├── Structure 数据结构 │ │ ├── StackExample.php 堆栈 先进后出 LIFO (Last In First Out) │ │ ├── LinearChain.php 线性表 单链存储 │ │ └── LinearOrder.php 线性表 顺序存储 │ │ └── BinarySearchTree.php 二叉搜索树 │ │ │ ├── Tools 小工具集 │ │ └── SystemSwitch.php 堆栈实现进制转换 │ │ │ └── Other 其他 │ ├── MonkeyKing.php 约瑟夫环 │ ├── DynamicProgramming.php 动态规划 │ ├── Fibonacci.php 斐波那契数列 │ ├── StealingApples.php 偷苹果求余 │ ├── HanoiGames.php 汉诺塔游戏 │ ├── BidirectionalQueue.php 双向队列 │ ├── ColorBricks.php 彩色砖块 │ ├── GetCattle.php 牛年求牛 │ ├── OnlyNumbers.php 求唯一数 │ ├── PokerGames.php 洗扑克牌 │ ├── Interval.php 抽奖区间算法 │ ├── Maze.php 迷宫寻址算法 │ ├── AntsClimb.php 蚂蚁爬杆算法 │ ├── Encryption.php 对称加密算法 │ ├── ElevatorDispatch.php 编程之美-电梯调度算法 │ ├── PointInTriangle.php 向量叉集计算点是否在三角形中 │ ├── TraversalOfBinary.php 二叉树非递归遍历算法实现 │ ├── Knapsack.php 贪心算法之背包问题实现 │ └── BigSmallReplace.php Hello World 输出 Olleh Dlrow │ └── Solution.php Facebook面试题之岛屿周长算法 │ └── RotationSort.php Facebook面试题之顺时针回旋算法 │ └── Square.php Facebook面试题之判断四个点能否组成正方形算法 │ └── Prim.php Prim算法(最小生成树算法) │ └── CartesianProduct.php 笛卡尔积算法 │ └── Square.php 面试题之平面任意四点能否组成一个矩形 │ └── Judge.php 面试题之扑克牌中任选五张判断是不是顺子 │ └── Factorial.php 面试题之N的阶乘末尾有多少个0

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值