动态规划和贪心算法的知识点
动态规划的核心思想是把原问题分解成子问题进行求解,也就是分治的思想。
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
题目
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n^2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Go版动态规划
首先这道题其实是有两问,一个是算法时间复杂度为现O(n^2),二是时间复杂度为O(n log n)。我们先来看看怎么实现O(n^2)。
算法:动态规划
状态定义:
1. dp[i] 的值代表 nums 前 i 个数字的最长子序列长度。
2. 注意 nums[i] 必须被选取。
转移方程:
1. dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)
2. 注意只有nums[i]>nums[j]的时候,nums[i]可以接在nums[j]之后
初始状态
1. dp[i]所有元素设置为1,表明每个元素都可以单独成为子序列
返回值
1. 返回dp数组中的最大值
算法复杂度分析
1. 时间复杂度 O(n^2)
2. 空间复杂度 O(n)
执行用时:12 ms
内存消耗:2.4 MB
func lengthOfLIS(nums []int) int {
l := len(nums)
if l==0 {
return 0
}
dp,max := make([]int,l),1
dp[0]=1
for i:=1;i<len(nums);i++ {
maxitem := 0
for j:=0;j<i;j++ {
if nums[i]>nums[j]&&dp[j]>maxitem {
maxitem = dp[j]
}
}
dp[i] = maxitem+1
if dp[i]>max {
max = dp[i]
}
}
return max
}
PHP版贪心算法
接下来,我们思考一下如何将时间复杂度从O(n^2)提升到O(n log n)呢?其实,一般见到logn,会下意识的想到二分法。那么我们的二分法怎么使用?这就到了贪心算法+二分法的环节了。
贪心算法+二分法
解析
- 考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
状态设计
- 基于上面的贪心思路,我们维护一个数组tails,其中每个元素 tails[k]的值代表 长度为k+1的子序列尾部元素的值
状态转移设计
- 遍历数组nums,同时二分法搜索tails
- 如果 tails 中元素都比它小,将它插到最后
- 否则,用它覆盖掉比它大的元素中最小的那个
返回值
- tails数组的长度
执行用时:8 ms
内存消耗:15.2 MB
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function lengthOfLIS($nums) {
$size = count($nums);
if ($size<2) {
return $size;
}
$tails[0] = $nums[0];
$i = 1;
while ($i<$size) {
if ($nums[$i]>$tails[count($tails)-1]) {
$tails[]=$nums[$i];
}
$l=0;
$r=count($tails)-1;
while($l<$r) {
$mid = Floor(($l+$r)/2);
if ($tails[$mid]<$nums[$i]) {
$l = $mid+1;
} else {
$r = $mid;
}
}
$tails[$l] = $nums[$i];
$i++;
}
return count($tails);
}
}
总结
- tails未必是真实的最长上升子序列,但长度是对的。
- 动态规划的核心在于分治
- 贪心算法的核心是局部最优解
- practice makes perfect