动态规划设计方法详解最长递增子序列
很多读者反应,就算看了前文动态规划详解,了解了动态规划的套路,也不会写状态转移方程,没有思路,怎么办?本文就借助「最长递增子序列」来讲一种设计动态规划的通用技巧:数学归纳思想。
最长递增子序列(Longest Increasing Subsequence,简写 LIS)是比较经典的一个问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2),我们借这个问题来由浅入深讲解如何写动态规划。比较难想到的是利用二分查找,时间复杂度是 O(NlogN),我们通过一种简单的纸牌游戏来辅助理解这种巧妙的解法。
先看一下题目,很容易理解:
注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。下面先来一步一步设计动态规划算法解决这个问题。
一、动态规划解法
动态规划的核心设计思想是数学归纳法。
相信大家对数学归纳法都不陌生,高中就学过,而且思路很简单。比如我们想证明一个数学结论,那么我们先假设这个结论在 k < n k<n k<n 时成立,然后想办法证明 k = n k=n k=n 的时候此结论也成立。如果能够证明出来,那么就说明这个结论对于 k 等于任何数都成立。
类似的,我们设计动态规划算法,不是需要一个 dp 数组吗?我们可以假设 d p [ 0... i − 1 ] dp[0...i-1] dp[0...i−1] 都已经被算出来了,然后问自己:怎么通过这些结果算出 dp[i]?
直接拿最长递增子序列这个问题举例你就明白了。不过,首先要定义清楚 dp 数组的含义,即 dp[i] 的值到底代表着什么?
我们的定义是这样的:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
举两个例子:
算法演进的过程是这样的,:
根据这个定义,我们的最终结果(子序列的最大长度)应该是 dp 数组中的最大值。
int res = 0;
for (int i = 0; i < dp.size(); i++) {res = Math.max(res, dp[i]);
}
return res;
读者也许会问,刚才这个过程中每个 dp[i] 的结果是我们肉眼看出来的,我们应该怎么设计算法逻辑来正确计算每个 dp[i] 呢?
这就是动态规划的重头戏了,要思考如何进行状态转移,这里就可以使用数学归纳的思想:
我们已经知道了 d p [ 0...4 ] dp[0...4] dp[0...4] 的所有结果,我们如何通过这些已知结果推出 d p [ 5 ] dp[5] dp[5] 呢?
根据刚才我们对 dp 数组的定义,现在想求 dp[5] 的值,也就是想求以 nums[5] 为结尾的最长递增子序列。
nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
当然,可能形成很多种新的子序列,但是我们只要最长的,把最长子序列的长度作为 dp[5] 的值即可。
for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
这段代码的逻辑就可以算出 dp[5]。到这里,这道算法题我们就基本做完了。读者也许会问,我们刚才只是算了 dp[5] 呀,dp[4], dp[3] 这些怎么算呢?
类似数学归纳法,你已经可以算出 dp[5] 了,其他的就都可以算出来:
for (int i = 0; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);}
}
还有一个细节问题,dp 数组应该全部初始化为 1,因为子序列最少也要包含自己,所以长度最小为 1。下面我们看一下完整代码:
public int lengthOfLIS(int[] nums) {int[] dp = new int[nums.length];// dp 数组全都初始化为 1Arrays.fill(dp, 1);for (int i = 0; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);}}int res = 0;for (int i = 0; i < dp.length; i++) {res = Math.max(res, dp[i]);}return res;
}
至此,这道题就解决了,时间复杂度 O(N^2)。总结一下动态规划的设计流程:
首先明确 dp 数组所存数据的含义。这步很重要,如果不得当或者不够清晰,会阻碍之后的步骤。
然后根据 dp 数组的定义,运用数学归纳法的思想,假设 d p [ 0... i − 1 ] dp[0...i-1] dp[0...i−1] 都已知,想办法求出 d p [ i ] dp[i] dp[i],一旦这一步完成,整个题目基本就解决了。
但如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。
最后想一想问题的 base case 是什么,以此来初始化 dp 数组,以保证算法正确运行。
二、二分查找解法
这个解法的时间复杂度会将为 O(NlogN),但是说实话,正常人基本想不到这种解法(也许玩过某些纸牌游戏的人可以想出来)。所以如果大家了解一下就好,正常情况下能够给出动态规划解法就已经很不错了。
根据题目的意思,我都很难想象这个问题竟然能和二分查找扯上关系。其实最长递增子序列和一种叫做 patience game 的纸牌游戏有关,甚至有一种排序方法就叫做 patience sorting(耐心排序)。
为了简单期间,后文跳过所有数学证明,通过一个简化的例子来理解一下思路。
首先,给你一排扑克牌,我们像遍历数组那样从左到右一张一张处理这些扑克牌,最终要把这些牌分成若干堆。
处理这些扑克牌要遵循以下规则:
只能把点数小的牌压到点数比它大的牌上。如果当前牌点数较大没有可以放置的堆,则新建一个堆,把这张牌放进去。如果当前牌有多个堆可供选择,则选择最左边的堆放置。
比如说上述的扑克牌最终会被分成这样 5 堆(我们认为 A 的值是最大的,而不是 1)。
为什么遇到多个可选择堆的时候要放到最左边的堆上呢?因为这样可以保证牌堆顶的牌有序(2, 4, 7, 8, Q),证明略。
按照上述规则执行,可以算出最长递增子序列,牌的堆数就是最长递增子序列的长度,证明略。
我们只要把处理扑克牌的过程编程写出来即可。每次处理一张扑克牌不是要找一个合适的牌堆顶来放吗,牌堆顶的牌不是有序吗,这就能用到二分查找了:用二分查找来搜索当前牌应放置的位置。
PS:旧文二分查找算法详解详细介绍了二分查找的细节及变体,这里就完美应用上了。如果没读过强烈建议阅读。
public int lengthOfLIS(int[] nums) {int[] top = new int[nums.length];// 牌堆数初始化为 0int piles = 0;for (int i = 0; i < nums.length; i++) {// 要处理的扑克牌int poker = nums[i];/***** 搜索左侧边界的二分查找 *****/int left = 0, right = piles;while (left < right) {int mid = (left + right) / 2;if (top[mid] > poker) {right = mid;} else if (top[mid] < poker) {left = mid + 1;} else {right = mid;}}/*********************************/// 没找到合适的牌堆,新建一堆if (left == piles) piles++;// 把这张牌放到牌堆顶top[left] = poker;}// 牌堆数就是 LIS 长度return piles;
}
至此,二分查找的解法也讲解完毕。
这个解法确实很难想到。首先涉及数学证明,谁能想到按照这些规则执行,就能得到最长递增子序列呢?其次还有二分查找的运用,要是对二分查找的细节不清楚,给了思路也很难写对。
所以,这个方法作为思维拓展好了。但动态规划的设计方法应该完全理解:假设之前的答案已知,利用数学归纳的思想正确进行状态的推演转移,最终得到答案。
如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~
动态规划设计方法详解最长递增子序列相关推荐
- 计算机算法设计与分析 动态规划 实验报告,动态规划法解最长公共子序列(计算机算法设计与分析实验报告).doc...
动态规划法解最长公共子序列(计算机算法设计与分析实验报告) 实报 告 实验名称:任课教师::姓 名:完成日期:二.主要实验内容及要求: 要求按动态规划法原理求解问题: 要求交互输入两个序列数据: 要求 ...
- Leetcode动态规划:300.longest-increasing-subsequence(最长递增子序列)
300. 最长递增子序列 最近一直在攻克动态规划的题,Leetcode的简单题已经刷完,现在冲中等题,这道题算是一个比较经典的题吧,独立完成,虽然花了两个多小时,但收获很多: 思路:动态规划首先要找到 ...
- 算法设计-递归法解最长公共子序列问题 C代码
给大家推荐一个公众号:诗葵1931 里面的诗歌很美 主要功能:递归法解最长公共子序列问题 #include<stdio.h> #include<string.h> /* 递归思 ...
- PCB布线、焊盘及敷铜的设计方法详解
随着电子技术的进步, PCB (印制电路板)的复杂程度.适用范围有了飞速的发展.从事高频PCB的设计者必须具有相应的基础理论知识,同时还应具有丰富的高频PCB的制作经验.也就是说,无论是原理图的绘制, ...
- vb treeview 展开子节点_详解最长公共子序列问题,秒杀三道动态规划题目
学算法认准 labuladong 后台回复进群一起力扣? 读完本文,可以去力扣解决如下题目: 1143.最长公共子序列(Medium) 583. 两个字符串的删除操作(Medium) 712.两个字符 ...
- 黑盒测试用例设计方法详解
黑盒测试用例设计方法包括等价类划分法.边界值分析法.错误推测法.因果图法.判定表驱动法.正交试验设计法.功能图法.场景图法等. (一)等价类划分法 定义:等价类划分法是把所有可能输入的数据,即程序的输 ...
- 51Nod:1134 最长递增子序列
动态规划 修改 隐藏话题 1134 最长递增子序列 基准时间限制:1 秒 空间限制:131072 KB 分值: 0 难度:基础题 收藏 关注 给出长度为N的数组,找出这个数组的最长递增子序列.( ...
- 【Leetcode】最长递增子序列问题及应用
文章目录 最长递增子序列问题及应用 300. 最长递增子序列 面试题 17.08. 马戏团人塔 354. 俄罗斯套娃信封问题 面试题 08.13. 堆箱子 1691. 堆叠长方体的最大高度 406. ...
- Leetcode——最长递增子序列(leetcode 300)
题目选择Leetcode 300 最长递增子序列 动态规划的典型例题,最长递增子序列 解题代码:C++ class Solution { public:int lengthOfLIS(vector&l ...
最新文章
- java和python哪个好学-Python和Java,哪个容易学呢?
- springboot 的两种配置文件语法||配置文件占位符||@Value 读取配置文件及验证处理
- 为什么正则化可以起到对模型容量进行控制_论文解读 | 基于正则化图神经网络的脑电情绪识别...
- leetcode111 爬楼梯 python实现
- OpenCASCADE:使用扩展数据交换 XDE之颜色和图层
- Linux查看设置系统时区
- 对996最客观的描述,一叶知秋
- 漫画 | 为什么 MySQL 数据库要用 B+ 树存储索引?
- Oracle之pl/sql编程(一)函数,过程,包
- 从零基础入门Tensorflow2.0 ----五、22TF1.0计算图构建
- NRPE: Unable to read output 问题处理总结
- 解密深圳IT人士的当前薪情【转自:中国it实验室】
- wowza 技术交流群/ wowza 流媒体软件交流群
- PS利用蒙版把图片调暗
- Matlab读取shape文件并统计均值
- GTD时间管理-节假日时间安排 | 每天成就更大成功
- 我用什么工具写公众号
- php过滤微信表情符号的正则表达式方法
- 移位寄存器SHIFT RAM IP之模拟图像卷积
- win10自启动方法
热门文章
- html如何设置下拉列表
- 看门狗watchdog的理解
- 关于thymeleaf的报错:Caused by: org.attoparser.ParseException: Could not parse as expression: ......
- ic618画版图2.0
- 什么是 NullPointerException?
- Shiro logout302重定向问题
- python爬取凤凰新闻网_python3.6爬取凤凰网新闻-爬虫框架式思维
- JAVA 多用户商城系统b2b2c-Spring Cloud Stream 介绍
- 什么是内部类?成员内部类、静态内部类、局部内部类和匿名内部类的区别及作用?
- 软件著作权的好处有哪些?软著含金量高吗?