1. 回顾
图论篇忽略了网络流、关键路径分析法和NP完全问题,在DSAA的论述里面,这些内容都是作为了解性质,以后如果需要深入再单独学习。整体上看,基本的数据结构已经全部学完了,从此篇开始将详细学习五大常用算法。
DSAA在该篇论述了简单调度问题、哈夫曼生成树及装箱问题,证明了贪婪算法在一些问题上可以找到最优解,但是大部分情况都只能接近最优解,甚至离最优解很远。笔者记录算法篇的目的是为了解决一些编程问题,而这类问题往往是求最优解,所以在做题场合贪婪的适用性非常小了。注:本篇引用来源DSAA和维基百科。
1. 定义
- Dijkstra’s, Prim’s, and Kruskal’s algorithms. Greedy algorithms work in phases. In each phase, a decision is made that appears to be good, without regard for future consequences.
- Generally, this means that some local optimum is chosen. When the algorithm terminates, we hope that the local optimum is equal to the global optimum. If this is the case, then the algorithm is correct; otherwise, the algorithm has produced a suboptimal solution.
贪婪算法的定义很简单:将一个过程分成多个阶段,每个阶段做出最优选择。这种选择是局部最优,如果算法结束时得到的结果与全局最优一致,则证明贪婪策略正确,否则贪婪就得到次最优解。
2. 特征
In general, greedy algorithms have five components:
- A candidate set, from which a solution is created
- A selection function, which chooses the best candidate to be added to the solution
- A feasibility function, that is used to determine if a candidate can be used to contribute to a solution
- An objective function, which assigns a value to a solution, or a partial solution, and
- A solution function, which will indicate when we have discovered a complete solution
以上五部分从抽象到函数的角度来考虑贪婪算法的实现,其实只要记住分阶段做出最优选择,最后得到整个问题的解向量。贪婪算法可以解决的问题应该满足下面几点:
- Greedy choice property
- We can make whatever choice seems best at the moment and then solve the subproblems that arise later. The choice made by a greedy algorithm may depend on choices made so far, but not on future choices or all the solutions to the subproblem. It iteratively makes one greedy choice after another, reducing each given problem into a smaller one. In other words, a greedy algorithm never reconsiders its choices.
- Optimal substructure
- “A problem exhibits optimal substructure if an optimal solution to the problem contains optimal solutions to the sub-problems.”
第一点中黑色粗体特别强调了马尔可夫性质(无后效性),笔者看到很多人困惑到底是未来不能影响过去,还是过去不能影响未来。其实两者等价,所以只要把握未来的状态只取决于现在的特性就可以了。在Leetcode题解的记录篇中分析到一个状态转移方程满足无后效性,但因为for
的错误取值也会导致其有后效性。第二点指出了最优子结构,贪婪算法和动态规划的交叉点很多,但是贪婪没有状态转移方程,其就不能保证每步的抉择达到全局最优,而选择方式往往按照既定的标准,在各个阶段选择。相反动态规划的状态转移方程保证了全局最优的结果,通过空间换时间,将每种可能的结果都记录下来(可能最优解不需要这种状态)。
下图为不能使用Greed算法的一个例子,每个阶段都选择最大的数,结果为次最优解。这个问题也不能用DP求解,因为其不满足最优子结构的性质,比较直接的解法就是回溯。
3. 应用
- For example, all known greedy coloring algorithms for the graph coloring problem and all other NP-complete problems do not consistently find optimum solutions. Nevertheless, they are useful because they are quick to think up and often give good approximations to the optimum.
- If a greedy algorithm can be proven to yield the global optimum for a given problem class, it typically becomes the method of choice because it is faster than other optimization methods like dynamic programming.
- Examples of such greedy algorithms are Kruskal’s algorithm and Prim’s algorithm for finding minimum spanning trees, and the algorithm for finding optimum Huffman trees.
4. 实战 Jump Game
我的解法和官网有点区别,因为使用的是DP,所以这里就简单陈述下。状态定义
F
[
i
]
F[i]
F[i]为从0位置出发到第i位置的状态,而状态转移方程为$ F[i]=F[j] if F[j] and nums[j] >i-j+1 0\leq j <i$。
然后思考贪婪算法,一种错误的思考如下:假设k步到达终点,然后第i步选择当前步长最大的方式,这样得到的结果不一定正确。以下是官网推荐的贪婪解法过程:
- Once we have our code in the bottom-up state, we can make one final, important observation. From a given position, when we try to see if we can jump to a GOOD position, we only ever use one - the first one (see the break statement). In other words, the left-most one. If we keep track of this left-most GOOD position as a separate variable, we can avoid searching for it in the array. Not only that, but we can stop using the array altogether.
- Iterating right-to-left, for each position we check if there is a potential jump that reaches a GOOD index (currPosition + nums[currPosition] >= leftmostGoodIndex). If we can reach a GOOD index, then our position is itself GOOD. Also, this new GOOD position will be the new leftmost GOOD index. Iteration continues until the beginning of the array. If first position is a GOOD index then we can reach the last index from the first position.
这种贪婪实际上是对bottom to up
动规的一种优化,修改笔者之前动规的状态定义为当前i位置是由前i个位置中某一个位置到达,此时for
从最右端开始,那么问题变成**从终点开始,然后向左判断当前位置是否可以到达当前的终点,如果可以就将终点更新为当前位置,再向左判断,直到0位置。**代码如下:
public class Solution {
public boolean canJump(int[] nums) {
int lastPos = nums.length - 1;
for (int i = nums.length - 1; i >= 0; i--) {
if (i + nums[i] >= lastPos) {
lastPos = i;
}
}
return lastPos == 0;
}
}
5. 总结
上面跳跃游戏的贪婪解法实际上就是一种优化后的动态规划。换种角度想:Kruskal、Prim算法或者huffman算法是非常明显的贪婪,因为每个阶段就选择最小或者最大的一个或者几个数,但是最终结果是全局最优。因为贪婪算法同样需要最优子结构和无后效性,所以上面贪婪的选择实际上构造了每个阶段状态的转移方程,从而达到全局最优。
当然这是笔者的理解,不权威,如果使用贪婪去解决最优问题,笔者个人觉得可行性不高,往往回溯和DP是更好的选择。**但是如果问题没有那么精准,求近似最优解,贪婪将会是非常好的选择。**当然一般编程题不太可能让求approximate solution。