这周教学的主题是贪心算法。贪心算法采取步步紧逼的方式构造问题的解,其下一步的选择总是在当前看来收效最快和效果最明显的那一个。贪心算法的难点在于“构造贪心的条件”以及验证其正确性。请看以下两道题。
原题:
Jump Game
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.
Determine if you are able to reach the last index.
For example:
A = [2,3,1,1,4], return true.
A = [3,2,1,0,4], return false.
思路:给定一个数组,每次能跳跃的最大距离为数组该位置的值。问,初始位置在数组的第一个位置,能否按照规定的跳跃规则到达数组的最后一个位置。
那么问题来了,什么情况下能够跳到最后一格?怎么样的跳跃是最好的跳跃(暂且把最好的跳跃定义为能够到达最后一格的跳跃组合)?是不是每次跳得越远,结果就越好呢?
请看下面的例子:假设给定的数组是a[5]=[3,2,2,0,4],如果我们假设每一步跳跃得最远是最好的选择。那么在第一个位置,a[0]=3,这时候可以跳3格,跳到a[3]的位置,但a[3]=0,跳跃就不能继续进行下去了。显然上述假设是不成立的。然后我们我发现,a[3]处不能继续跳跃下去是因为该处数组的值为0。因为原题已经假定数组的值为非负数,所以只有等于0的时候才有可能使跳跃不能进行下去。那么是不是一旦出现了0就一定不能继续跳下去呢?答案显然也是否定的。那在出现0的情况下,怎么判定能否继续跳下去就成了本题的关键。我们假定从第一格开始遍历数组,并维护一个max_jump_position的值,max_jump_position记录了在到达当前位置的情况下,下一步能够达到的最远距离(其他情况都包含在最远距离里面,所以不需要维护其他)。显然,如果下一步能够达到的最远距离大于数组的下标,那么就返回true;如果遍历到某一格,该位置的值为0,而且上一格记录的max_jump_position就在此处,那么这时跳跃就进行不下去了,就返回false。所以只要我们一格一格走下去,并且每走一格都更新max_jump_position,就能找到我们所要求的答案。
代码:
class Solution {
public:
bool canJump(vector<int>& nums) {
int length=nums.size();
int position=0;
int max_jump_position=0;
for(int i=0;i<length;i++){
if(max_jump_position>=(length-1))
return true;
if((nums[i]+i)>max_jump_position)
max_jump_position=nums[i]+i;
else if(((nums[i]+i)==max_jump_position)&&(nums[i]==0))
return false;
else
continue;
}
}
};
原题:
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.
For example:
Given array A = [2,3,1,1,4]
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.
思路:这题leetcode给的难度是hard。但在做过第一题的情况下继续思考,其实并不难。这题的跳跃规则和第一题一样,并且题目已知给定的所有数组都能从第一格跳跃到最后一格,求最少的跳跃次数。比如,题目中给定的例子: A = [2,3,1,1,4],按照跳跃规则,你从第一格到最后一格,当然你可以每一格跳一步,这样你要跳4次才到最后一格,当然这明显并不是跳跃次数最少的。你可以第一次跳跃从A[0]跳到A[1],第二次跳跃从A[1]跳到A[4],这样仅需两次就完成任务了。
那么,问题来了,什么情况下跳跃次数最少?已知起点在第一格,那么我们很自然会想,到底我们下一步要跳到哪里去最合适?已知在A[0]处,A[0]=2,那么在当前情况下,我们第一次跳跃只有两种选择,即跳到A[1]或者A[2]。如果第一次跳跃从A[0]跳到A[1],A[1]=3,那么我们下一步(第二次)跳跃的选择就是从A[1]到A[2],A[3],A[4];如果第一次跳跃从A[0]跳到A[2],A[2]=1,那么我们下一步(第二次)跳跃的选择就只能是从A[2]到A[3]。这样对比起来,很明显第一次跳跃跳到A[0]要好于A[1],因为第一次跳到A[0]处之后第二次选择更多,能够到达的距离更远,A[1]的所有情况都被包含在里面。而第一次跳跃本身只有两种选择,即从A[0]到A[1],或者A[0]到A[2]。经过上面的分析,我们当然选择A[0]到A[1]。那么这时我们就达到了A[1],那第二次跳跃要怎么继续跳下去,我们完全可以按照刚才分析第一次跳跃选择的思路分析第二次。依次类推,直到得出结果。
在代码中,假设我们第n次跳跃到达A[n]处(这是一个固定位置),那么第n+1次跳跃可选择的落点为next_begin(A[n+1])到next_end(A[n+A[n]]),在这个范围内,我们需要找到能够使第n+2次跳跃能够达到最远位置的第n+1次跳跃的落点(同时也是第n+2次跳跃的起点),并把这个落点更新为next_step。
可以说,这道题把贪心的思想运用得淋漓尽致。
代码:
class Solution {
public:
int jump(vector<int>& nums) {
int length=nums.size();
int min_step=0;
int next_begin=0,next_end=0;
int next_step;
int tmp_max_end;
for(int i=0;i<length;i++){
if(next_end>=(length-1))
return min_step;
next_step=next_begin;
tmp_max_end=next_step;
min_step++;
for(int j=next_begin;j<=next_end;j++){
if(nums[j]+j>tmp_max_end){
tmp_max_end=nums[j]+j;
next_step=j;
}
}
next_begin=next_step+1;
next_end=next_step+nums[next_step];
}
}
};
贪心的题目的关键在于找到正确的贪心条件。在做此类题目的时候,一定要看清楚题意,找到准确的贪心条件,多考虑几种特殊的情况,多测试几组样例再交,不要那么着急。虽然这两题做得比较顺利,但主要是由于leetcode提交代码之后反馈得比较到位,如果是sicily,可能要卡很久。