今天是贪心算法学习的第三天,感觉有摸到一点门道。贪心算法感觉还是离不开对题目的模拟。
134. 加油站
该题目给出了两个数组,一个是每个加油站能加的油量,一个是到达该加油站所消耗的油量。需要从数组中挑选一个起点,从该起点出发可以遍历完这个环形路段。随想录提供了两个题解,我觉得第二种思路是更好理解的,两种解题思路都写一下。
一种思路是首先求解一个数组,对于每一个加油站求解一个净加油量,就是用加油量减去路上损耗的油量,并对净加油量求和,如果求和为负说明无法实现走完整个数组。如果求和量为正,就从0开始遍历净加油量数组并求和,一旦求和为负,就将求和量置为0,并从下一个点为新的起点。具体代码实现如下:
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
vector<int> nums(gas.size(),0);
for(int i=0;i<nums.size();i++) nums[i]=gas[i]-cost[i];
int sum=0;
for(int i=0;i<nums.size();i++) sum+=nums[i];
int start=0;
if(sum<0) return -1;
else
{
int rest_sum=0;
for(int i=start;i<nums.size();i++)
{
rest_sum+=nums[i];
if(rest_sum<0)
{
start=i+1;
rest_sum=0;
continue;
}
}
}
return start;
}
};
第一种思想我个人是不太好理解的,我觉得第二种思路更加好理解。同样是对净加油量数组,从头开始求和,求解一个求和的最小值,假如一个数组的和是大于0的,要不两端子数组都大于0,要不一段大于0一段小于0;如果所有子数组都是大于0,那么求和的最小值就是大于0的,此时直接选择0作为起点即可;如果是一段大于0一段小于0,我们最小的求和子数组,从他后面开始求和一定是大于0的。具体代码实现如下:
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
vector<int> nums(gas.size(),0);
for(int i=0;i<nums.size();i++) nums[i]=gas[i]-cost[i];
int sum=0;
for(int i=0;i<nums.size();i++) sum+=nums[i];
int min_sum=10010;
int index=0;
int msum=0;
if(sum<0) return -1;
else
{
for(int i=0;i<nums.size();i++)
{
msum+=nums[i];
if(min_sum>msum)
{
index=i;
min_sum=msum;
}
}
}
if(min_sum>0) return 0;
if(index==nums.size()-1) return 0;
else return index+1;
}
};
135. 分发糖果
这个题目是给出一个评分数组,要求相邻孩子中较高评分的孩子有更多的糖果数,求满足这一条件的最小糖果总和。
思路是这样的,首先从左往右遍历评分数组,如果右孩子大于左孩子,并且右孩子的糖果小于等于左孩子的糖果,就把右孩子的糖果在左孩子糖果的基础上加1;再从右往左遍历评分数组,如果左孩子大于右孩子,并且左孩子的糖果小于等于右孩子的糖果,就把左孩子的糖果在右孩子糖果的基础上加1。这样两次贪心的过程就可以得到全局最优解。具体代码实现如下:
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candy(ratings.size(),1);
for(int i=0;i<candy.size()-1;i++)
{
if(ratings[i+1]>ratings[i]&&candy[i+1]<=candy[i]) candy[i+1]=candy[i]+1;
}
for(int i=candy.size()-1;i>0;i--)
{
if(ratings[i-1]>ratings[i]&&candy[i-1]<=candy[i]) candy[i-1]=candy[i]+1;
}
int sum=0;
for(int i=0;i<candy.size();i++) sum+=candy[i];
return sum;
}
};
860.柠檬水找零
题目链接:860. 柠檬水找零 - 力扣(LeetCode)
这个题目挺简单的,就是一个模拟题。用一个数组统计5元,10元,20元纸币的数量,收到一张十元就退一张五元,收到一张二十可以退一五一十或三张五元,优先选择一五一十的退法,因为五月是更有用的。最后判断是否有某个纸币为负值,就可以判断是否可行。具体代码实现如下:
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int nums[3]={0};
for(int i=0;i<bills.size();i++)
{
if(bills[i]==5) nums[0]++;
else if(bills[i]==10)
{
nums[0]--;
nums[1]++;
}
else if(bills[i]==20)
{
if(nums[1]>0)
{
nums[0]--;
nums[1]--;
nums[2]++;
}
else{
nums[0]-=3;
}
}
for(int i=0;i<3;i++) if(nums[i]<0) return false;
}
return true;
}
};
406.根据身高重建队列
题目链接:406. 根据身高重建队列 - 力扣(LeetCode)
这个题目有点难度。题目的要求是给出一个数组,里面每个元素包含两个属性h和k,h是身高,k是前面有k个人的身高大于等于h。要求将数组正确的排序。
思路是这样的。首先将数组由身高从大到小排序,身高一样的按k从小到大排序。然后遍历整个数组,将元素插入到新数组的k的位置,由于是身高从大到小插入,即使有新的元素插入到前面也不影响后面的元素的k的位置,因为新的元素的身高一定小于等于已经在数组中的元素。
还有一个值得注意的点,这个题使用list容器比使用vector容器时间复杂度会优很多。这是由于list的底层实现是链表,插入元素是o(1)的时间复杂度,vector的底层实现是数组,插入元素要将后面的所有元素右移,扩容时是直接复制一个同等大小的空数组并进行拼接。所以插入操作的效率会比list低很多。该题目的具体代码实现如下所示:
class Solution {
public:
static bool cmp(vector<int> &a,vector<int> &b)
{
if(a[0]!=b[0]) return a[0]>b[0];
else return a[1]<b[1];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
list<vector<int>> que;
sort(people.begin(),people.end(),cmp);
for(int i=0;i<people.size();i++)
{
int position=people[i][1];
std::list<vector<int>>::iterator it=que.begin();
while(position--) it++;
que.insert(it,people[i]);
}
return vector<vector<int>>(que.begin(),que.end());
}
};