10.6 贪心算法详解及LeetCode题目

可参考几篇博客

详解贪心算法(Python实现贪心算法典型例题)

五大常用算法之一:贪心算法

 

算法概述

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,所做出的是在某种意义上的局部最优解。

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

最重要的一点,动态规划问题我们强调算法框架,然鹅贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择,具体问题具体分析,我们通过几个问题理解算法设计思想。

 

算法特点

贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。贪心选择是采用从顶向下、以迭代的方法做出相继选择。

如何确定可用贪心算法?其实这并不是一个容易确定的问题。

需要证明每一步所作的贪心选择最终能得到问题的最优解。通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解。这是常用的数学归纳法。

第二个办法是证明在接近问题目标的过程中贪心算法每一步的选择至少不比任何其他算法差。第三个办法是基于算法的输出来证明。这里不做解释了,可参考《算法设计与分析基础》。

 

算法思路

  • 建立数学模型来描述问题
  • 把求解的问题分成若干个子问题
  • 对每个子问题求解,得到子问题的局部最优解
  • 把子问题的解局部最优解合成原来问题的一个解

贪婪算法建议通过一系列步骤来构造问题的解,每一步对目前构造的部分解做一个扩展,直到获得问题的完整解为止。

这个技术的核心是,所做的每一步选择都必须满足以下条件:

  • 可行性(feasible)即必须满足问题的约束
  • 局部最优(locally optimal)即它是当前步骤中所有可行的选择中最优的
  • 不可取消(irrevocable)即一旦做出选择在算法后面步骤中就无法改变了

以上引自《算法设计与分析基础》,感觉用语十分准确。

我们再谈最优子结构问题,动态规划和贪心算法都是最优子结构。我们需要辨析二者的区别。

贪心算法对每个子问题的解决方案都做出最佳选择并产生整个问题的最优解,f(i)及其以前的选择都不再改变,不能回退;动态规划则会根据以前的选择结果对当前进行选择,f(i+1)的选择还可以根据f(1),f(2)...f(i)的结果,毕竟所有结果都可以记忆起来,这也是动态规划算法的核心,因此DP有回退功能。

动态规划主要运用于二维或三维问题,而贪心一般是一维问题 。

 

算法应用

这里,我们简要介绍贪心思想的几个经典算法。

Prim算法和Kruskal算法:这是图算法中最小生成树(minimun spanning tree)的两种解决方法,Prim算法在构造最小生成树时,每一轮,把不在树中的顶点,贪婪的包含进来,只选择一个最优的(最近的)顶点,如此迭代n-1 轮(n为顶点数),最终培养起一棵最小生成树。算法的关键是贪婪策略的选择,即每一次迭代时选择的顶点的策略。Kruskal算法,也是解决这一问题的,它的贪婪策略的设计完全不同,首先按照权重对边进行非递减排序,然后每一轮迭代,试图将下一条边加进来,可以不连通但是不可有环。算法细节不再介绍,理解对同一问题的不同贪婪策略的设计思想。

Dijkstra算法,用于解决加权图的单起点最短路径问题,其贪心策略的设计与Prim相似,不同的是它比较的是路径的长度而不是边的长度。

哈夫曼编码,也是贪心思想的重要应用,它是一种最优的自由前缀变长编码方案,基于字符在给定文本中出现频率,把位串赋给字符,通过贪婪思想构造一颗二叉树(哈夫曼树),形成一组编码方案。

 

典型题目

下面我们介绍几道典型题目。刷了几道题后,发现大多数贪心思想并不明显,可以理解为就是一些解题思路和诡计。=。=

455. Assign Cookies

Assume you are an awesome parent and want to give your children some cookies. But, you should give each child at most one cookie. Each child i has a greed factor gi, which is the minimum size of a cookie that the child will be content with; and each cookie j has a size sj. If sj >= gi, we can assign the cookie j to the child i, and the child i will be content. Your goal is to maximize the number of your content children and output the maximum number.

题目解析:

这是最典型的一道贪心的题目了。一群孩子和一堆饼干,我们要满足最多的孩子。解题思路就是,对于每一个孩子来说,我们找一个能满足他且最小的饼干即可。

在代码中看不到算法框架,只是一种思想。我们将孩子和饼干排序,然后依次满足孩子,从小饼干选起,直到饼干没有合适的或者孩子都满足为止。

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        if not g or not s:
            return 0
        cookies = sorted(s)
        chs = sorted(g)
        if chs[-1] < cookies[0]:
            return len(g)
        if chs[0] > cookies[-1]:
            return 0
        j = 0
        for i, x in enumerate(chs):
            while j < len(s) and x > cookies[j]:
                j += 1
            if j > len(s)-1:
                return i
            j += 1
        return i+1

402. Remove K Digits

Given a non-negative integer num represented as a string, remove k digits from the number so that the new number is the smallest possible.

Note:

  • The length of num is less than 10002 and will be ≥ k.
  • The given num does not contain any leading zero.

Example 1:

Input: num = "1432219", k = 3
Output: "1219"
Explanation: Remove the three digits 4, 3, and 2 to form the new number 1219 which is the smallest.

题目解析:

移出几个数字使新的数字最小,那么我们肯定是从高位开始,尽量去掉大的数字留下小的数字,这就是其中贪心的思想。

在具体的代码中我们是这么做的,从前往后对于每一个数字,我们和stack中栈顶数字比较,大的话就去掉,直到k个。贪心就体现在移除数字时,我们从高位开始并且选择当前最大的。

另外,本题还要注意一些后续处理,比如最后k>0的情况(比如数字本身就是递增的),此时将末尾的数字依次去掉。

class Solution:
    def removeKdigits(self, num: str, k: int) -> str:
        if len(num) == k:
            return "0"
        stack = []
        for s in num:
            if k == 0:
                stack.append(s)
            else:
                n = int(s)
                while stack and int(stack[-1]) > n and k > 0:
                    stack.pop()
                    k -= 1
                stack.append(s)  
        while k > 0:
            stack.pop()
            k -= 1
        if not stack:
            stack.append("0")      
        res = "".join(stack)
        return str(int(res))

45. Jump Game II

Given an array of non-negative integers, you are initially positioned at the first index of the array.

Each element in the array represents your maximum jump length at that position.

Your goal is to reach the last index in the minimum number of jumps.

Example:

Input: [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2.
    Jump 1 step from index 0 to 1, then 3 steps to the last index.

Note:

You can assume that you can always reach the last index.

题目解析:

最后看一道hard 的题目,如何用最少的步骤到达最后。

这道题的难点就是贪心思想的设计,可以想到,每次都走最大的步伐,不一定是最优的。还要考虑到中间位置的最大步伐。

因此本题中贪心的思路是,在i位置,我们能到达的位置是i+nums[i],但是这不是最优的,我们要选择一个step使得从i+step位置可以走的最远即i+step+nums[i+step]最大,我们选择最优的这个step_作为当前步伐。

class Solution:
    def jump(self, nums: List[int]) -> int:
        if len(nums) <= 1:
            return 0
        i = 0
        res = 0
        while i <= len(nums)-1:
            step = nums[i]
            goal = i+step
            step_ = step
            if goal >= len(nums)-1:
                return res + 1
            
            for j in range(1, step+1):
                try_ = i + j + nums[i+j]
                if try_ > goal:
                    goal = try_
                    step_ = j
            res += 1
            i += step_
            

 

你可能感兴趣的:(Python数据结构与算法,LeetCode)