Skip to content

Latest commit

 

History

History
274 lines (202 loc) · 9.83 KB

技巧-贪心.md

File metadata and controls

274 lines (202 loc) · 9.83 KB

贪心

Problems


LeetCode 0300 最长递增子序列 (中等, 2022-01)

动态规划 贪心 经典 LeetCode

问题简述
给定整数数组 nums,返回最长严格递增子序列的长度;
进阶:
    你可以设计时间复杂度为 O(N^2) 的解决方案吗?
    你能把时间复杂度降到 O(NlogN) 吗?
详细描述
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:
    输入:nums = [10,9,2,5,3,7,101,18]
    输出:4
    解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
    输入:nums = [0,1,0,3,2,3]
    输出:4
示例 3:
    输入:nums = [7,7,7,7,7,7,7]
    输出:1

提示:
    1 <= nums.length <= 2500
    -104 <= nums[i] <= 104

进阶:
    你可以设计时间复杂度为 O(n2) 的解决方案吗?
    你能将算法的时间复杂度降低到 O(n log(n)) 吗?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:动态规划

状态定义dp[i] 表示以 nums[i] 结尾的最长递增子序列长度;

不能将 dp[i] 定义 nums[:i] 子数组中的最长递增子序列长度,虽然这样定义很直观,但它不满足最优子结构的条件,简单来说,就是你无法通过 dp[i-1] 得到 dp[i]

Python
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        
        ret = 1
        dp = [1] * len(nums)
        for i in range(1, len(nums)):
            for j in range(i):
                if nums[i] > nums[j]:  # 如果要求非严格递增,将 '>' 改为 '>=' 即可
                    dp[i] = max(dp[i], dp[j] + 1)
            
            ret = max(ret, dp[i])
        
        return ret
思路2:优化 DP 的状态定义
  • 考虑新的状态定义dp[i] 表示长度为 i + 1 的最长递增子序列末尾的最小值;

    dp 序列一定时单调递增的,可用反证法证明,详见:最长递增子序列(动态规划 + 二分查找,清晰图解) - Krahets

    该怎么想出这个定义?——多看多做

  • 是否满足最优子结构
    • 即已知 dp[i - 1] 能否递推得到 dp[i] ;显然是可以的,当nums[i] > dp[i - 1]时,长度 +1,否则,长度不变;
  • 如何更新dp
    • nums[i] > dp[i - 1] 时,直接添加到末尾;
    • 否则,要看是否需要更新 dp。根据 dp 递增的性质,找到 nums[i]dp 中应该插入的位置,记 idx;比较 dp[idx]nums[i] 的大小,如果 dp[idx] > nums[i] 根据定义,更新 dp[idx] = nums[i]

从“贪心”角度来解释以上过程:如果我们要使上升子序列尽可能的长,则应该让序列上升得尽可能慢,即每次在上升子序列最后加上的那个数尽可能的小。

最长上升子序列 - 力扣官方题解

Python
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums: return 0

        # from bisect import bisect_left

        # 手写二分查找
        def bisect_left(ls, x):
            l, r = 0, len(ls)
            while l < r:
                m = (l + r) // 2
                if ls[m] < x:  # 注意这里要 <,如果是 <= 就是 bisect_right 了,不满足题意
                    l = m + 1
                else:
                    r = m
            return l

        dp = [nums[0]]  # dp[i] 表示长度为 (i+1) 的 LIS 的最后一个元素的最小值
        for x in nums[1:]:
            if x > dp[-1]:
                dp.append(x)
            else:
                idx = bisect_left(dp, x)  # 不能使用 bisect/bisect_right
                # if dp[idx] > x:
                #     dp[idx] = x
                dp[idx] = x  # 因为 bisect_left 返回的定义就是 dp[idx] <= x,所以可以直接赋值

        return len(dp)

剑指Offer 1401 剪绳子(整数拆分) (中等, 2021-11)

动态规划 贪心 数学 剑指Offer

问题简述
给定一个正整数 n,将其拆分为至少两个正整数的和,使这些整数的乘积最大化。返回最大乘积。
详细描述
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

示例 1:
    输入: 2
    输出: 1
    解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
    输入: 10
    输出: 36
    解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
提示:
    2 <= n <= 58

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/jian-sheng-zi-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:动态规划
  • 在不使用任何数学结论的前提下,可以把本题当做纯 DP 来做:
Python(写法1)

LeetCode 官方题解中的写法:整数拆分

class Solution:
    def integerBreak(self, n: int) -> int:
        dp = [1] * (n + 1)

        for i in range(2, n + 1):
            for j in range(1, i):
                # 状态定义:dp[i] 表示长度为 i 并拆分成至少两个正整数后的最大乘积(i>=1)
                #   j * (i - j)   表示将 i 拆分成 j 和 i-j,且 i-j 不再拆分
                #   j * dp[i - j] 表示将 i 拆分成 j 和 i-j,且 i-j 会继续拆分,dp[i-j] 即为继续拆分的最优结果(最优子结构)
                dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]))

        return dp[n]
Python(写法2,推荐)

《剑指Offer》中的写法

class Solution:
    def cuttingRope(self, n: int) -> int:
        # 对于 n = 2、3 的情况,直接硬编码
        if n == 2:
            return 1
        if n == 3:
            return 2

        # 状态定义:dp[i] 表示长度为 i 并拆分成至少两个正整数后的最大乘积(i>3)
        #   当 i <= 3 时,不满足该定义,此时不拆效率最高
        #   初始状态(dp[0] 仅用于占位)
        dp = [0,1,2,3] + [0] * (n - 3) 

        for i in range(4, n + 1):
            for j in range(2, i):
                dp[i] = max(dp[i], dp[i-j] * dp[j])

        return dp[n]
思路2:数学/贪心
  • 数学上可证:尽可能按长度为 3 切,如果剩余 4,则按 2、2 切;

    证明见:剪绳子1(数学推导 / 贪心思想,清晰图解)

  • 简述:当 x >= 4 时,有 2(x-2) = 2x - 4 >= x;简言之,对任意大于等于 4 的因子,都可以拆成 2 和 x-2 而不损失性能;因此只需考虑拆成 2 或 3 两种情况(1除外);而由于 2*2 > 3*13*3 > 2*2*2,可知最多使用两个 2;

Python
class Solution:
    def cuttingRope(self, n: int) -> int:
        import math
        if n <= 3:
            return n - 1
        
        a, b = n // 3, n % 3
        if b == 1:
            return int(math.pow(3, a - 1) * 4)
        elif b == 2:
            return int(math.pow(3, a) * 2)
        else:
            return int(math.pow(3, a))