文章目录
贪心算法
非常感谢程序员carl的讲解,下面的内容主要来自自己的理解以及参考网站 代码随想录
买卖股票的最佳时间2
之前使用动态规划解决了这个问题
d p [ i ] [ 0 ] dp[i][0] dp[i][0]表示第i天手中持有股票的状态下有多少钱
d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示第i天手中不持有股票的状态下有多少钱
现在我们使用贪心算法来解决这个问题
这里有一个非常巧妙的点
就是可以把利润分成每一天的维度
55.跳跃游戏
我自己的解法把cover当做了能覆盖到的下标的位置,使用这种方法我调试了很久的代码
bool canJump(vector<int>& nums) {
int cover = nums[0];
if(cover >= nums.size() - 1) return true;
for(int i = 1; i < nums.size(); ++i){
int tempcover = 0;
int tempi = i;
for(int j = i; j <= cover; ++j){
if((nums[j] + i) > tempcover){
tempcover = nums[j] + i;
tempi = j;
cout << tempcover<< " "<< tempi<< endl;
}
if(tempcover >= nums.size() - 1)
return true;
}
if(tempi != i){
i = tempi - 1;
}
if(tempcover > cover)
cover = tempcover;
}
if(cover >= nums.size() - 1) return true;
else return false;
}
这种解决方法明显把这道题想复杂了,下面看别人的方法,中间并不对i进行改变
遍历的时候只遍历cover内部
整个过程只维护了一个cover
class Solution {
public:
bool canJump(vector<int>& nums) {
int cover = 0;
if (nums.size() == 1) return true; // 只有一个元素,就是能达到
for (int i = 0; i <= cover; i++) { // 注意这里是小于等于cover
cover = max(i + nums[i], cover);
if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了
}
return false;
}
};
45.跳跃游戏2
上面一道题目要求判断能否到达结尾,这道题目要求以最少跳跃次数到达结尾
这道题目比较巧妙,我之前使用的是两个循环,外部循环遍历数组,内部循环遍历nums[i]范围内部的找到最大的cover,这样是两个循环,要维护的变量也很多
下面的解法更加的简洁,只需要维护两个距离,需要注意的是i遍历到curDistance的时候,最大的nextDistance也找到了,这时候执行跳跃一次,并且更新curDistance
int jump(vector<int>& nums) {
if(nums.size() == 1) return 0;
int curDistance = 0;
int nextDistance = 0;
int ans = 0;
for(int i = 0; i < nums.size(); ++i){
nextDistance = max(nextDistance, nums[i] + i);
if(i == curDistance){
ans++;
curDistance = nextDistance;
if(nextDistance >= nums.size() - 1)
break;
}
}
return ans;
}
134.加油站
这道题使用两个for循环的暴力解法,处理边界情况使用我将近一个小时的时间,题目也有误导的成分在里面
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
if(cost.size() == 1 && gas[0] >= cost[0]) return 0;//只有一个加油站我真服了,这种环形还要搞
for(int i = 0; i < cost.size(); ++i){
int tank = gas[i] - cost[i];
if(tank <= 0)//要去下一个地方,必须当前邮箱里面的油比当前的花费多
continue;
int index = 0;
for(int j = i + 1; j < i + cost.size(); ++j){
index++;
tank += gas[j % cost.size()] - cost[j % cost.size()];
if(tank <= 0)
break;
}
if(tank >= 0 && index == cost.size() - 1)
return i;
}
return -1;
}
这时候程序员carl告诉我们,for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
for (int i = 0; i < cost.size(); i++) {
int rest = gas[i] - cost[i]; // 记录剩余油量
int index = (i + 1) % cost.size();
while (rest > 0 && index != i) { // 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了)
rest += gas[index] - cost[index];
index = (index + 1) % cost.size();
}
// 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
if (rest >= 0 && index == i) return i;
}
return -1;
}
23_12_14,回到我们最爱的加油站问题
很明显不管怎么样的暴力算法,最终都会超时
这时候,我们掏出我们的贪心算法!
这里需要注意的是
从任何地方出发都不能跑完一圈的只有一种情况,就是totaltank < 0
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curtank = 0;//当前油箱里面的油量
int totaltank = 0;//判断总的加油数是不是小于总花费,这种情况不可能跑完
int startstation = 0;
for(int i = 0; i < cost.size(); ++i){
curtank += gas[i] - cost[i];
totaltank += gas[i] - cost[i];
if(curtank < 0){
startstation = i + 1;
curtank = 0;
}
}
if(totaltank < 0) return -1;
return startstation;
}
这里贴上代码,判断非常巧妙,如果i位置的curtank < 0的话说明前面所有位置出发到达i位置都没法走,那么可能的出发位置只能在i+1及以后
135.分发糖果
每个孩子至少发一个糖果,难么先全部初始化发一个;
这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。
先确定右边评分大于左边的情况(也就是从前向后遍历)
此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
局部最优可以推出全局最优。
如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1
// 如果只比左边的值大,那么当前孩子的糖果数量要比左边孩子的糖果多1。
// 如果只比右边的值大,那么当前孩子的糖果数量要比右边孩子的糖果多1。
// 那么i位置最终的糖果数量就是选择比左边孩子多1和比右边孩子多1中最大的那个。
// 要满足上面的第二步,可以使用两次遍历,第一次从左往右,如果右边的孩子比左边得分高,那么右边的孩子糖果数量就要比左边的多1。这样每个孩子都能满足右边的条件,就是右边比他得分高的都会比他多一个。满足了右边的条件之后我们还要满足左边的条件.
//左边遍历一次,右边遍历一次,然后在每个点取得最大值
int candy(vector<int>& ratings) {
vector<int> leftCandy(ratings.size(), 1);
vector<int> rightCandy(ratings.size(), 1);
int result = 0;
//当前位置和左边比较
for(int i = 1; i < ratings.size(); ++i){
if(ratings[i] > ratings[i-1])
leftCandy[i] += 1;
}
//当前位置和右边比较
for(int i = ratings.size() - 2; i >= 0; --i){
if(ratings[i] > ratings[i + 1])
rightCandy[i] += 1;
}
for(int i = 0; i < ratings.size(); ++i){
result += max(leftCandy[i], rightCandy[i]);
}
return result;
}
860柠檬水找零
406.根据身高重建队列
这道题目的思路比较巧妙
首先按照身高从大到小排序,然后后面从头开始,根据k来确定在结果中放置的位置
这样做的原理就是,比如插入【6,1】不会在有比6大的数插入到他前面所以会一直满足要求
很巧妙的处理方法
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort (people.begin(), people.end(), cmp);
list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
for (int i = 0; i < people.size(); i++) {
int position = people[i][1]; // 插入到下标为position的位置
std::list<vector<int>>::iterator it = que.begin();
while (position--) { // 寻找在插入位置
it++;
}
que.insert(it, people[i]);
}
return vector<vector<int>>(que.begin(), que.end());
}
注意上面代码使用是list<vector> 而不是 vector<vector>
因为list底层是链表实现的,做插入运算的速度远远比vector快
并且还要注意的是list 的迭代器不支持随机访问!!!
由于链表的结构,std::list
的迭代器只支持前进和后退操作,不能像随机访问迭代器那样使用[]
运算符来直接访问列表中的任意元素。这意味着不能使用迭代器的算术运算符(如+
和-
)进行跳跃式的访问。
所以在寻找插入位置的时候vector可以使用 q u e . b e g i n ( ) + p o s i t i o n que.begin() + position que.begin()+position,而list必须使用while对迭代器进行累加来确切插入的位置。
452用最少数量的箭引爆气球
这道题目处理的也非常巧妙
如何判断当前的气球无法和前面的不使用一支箭呢?
我们维护一个同一只箭矢的最小右边界 (这也是箭矢应该射入的地方)
class Solution {
static bool cmp(vector<int> &a, vector<int> &b){
return a[0] < b[0];
}
public:
int findMinArrowShots(vector<vector<int>>& points) {
//按照气球左边边界进行排序
sort(points.begin(), points.end(), cmp);
int arrayIndex = points[0][1];
int arrayCount = 1;
if(points.size() == 1) return 1;
for(int i = 1; i < points.size(); ++i){
if(points[i][0] > arrayIndex){
arrayCount++;
arrayIndex = points[i][1];
}
else{
arrayIndex = min(arrayIndex, points[i][1]);
}
}
return arrayCount;
}
};
cmp函数传参不传引用的话会超时
如果不使用引用,而是将参数类型定义为vector<int>
,则在函数调用时会发生向量的拷贝,将整个向量的内容复制到函数的形参中。对于大型向量来说,这样的拷贝操作可能会导致性能下降和额外的内存开销。
为了保证引用的数据的安全,最好加上const关键字进行修饰
435.无重叠区间
这道题目其实就是上面的题目的变体
上一道题目求的是有重叠起来的区间算一个
这道题目就是求重叠区间的数量,刚好累加加结果的位置不一样罢了,一个在if里面,一个在else里面
还要注意的是上一道题目如果重叠的位置是一个点,也是可以的
但是这道题目不可以
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
//计算需要删除的区间数量
//用一支箭穿过重叠的区间
//维护一个重叠区间的最小右边界rightIndex
sort(intervals.begin(), intervals.end(), cmp);
int rightIndex = intervals[0][1];
int arrowCount = 0;
for(int i = 1; i < intervals.size(); ++i){
if(intervals[i][0] >= rightIndex){//不重叠
rightIndex = intervals[i][1];
}
else{//重叠
rightIndex = min(rightIndex, intervals[i][1]);
arrowCount++;
}
}
return arrowCount;
}
763.划分字母区间
非常有趣的一道题目
要注意的点全部放在了代码注释中
vector<int> partitionLabels(string s) {
vector<int> rightestIndex(26, 0);
for(int i = 0; i < s.size(); ++i){ //循环过后对每个字母的最大值的位置就找到了
rightestIndex[s[i] - 'a'] = i;
}
int cutIndex = 0; //记录裁切的位置 这个裁切位置其实也是在找一段中的字母中最大的右边的位置的Index
int lastcutIndex = 0; //记录上次裁切的位置
vector<int> result;
for(int i = 0; i < s.size(); ++i){
cutIndex = max(cutIndex, rightestIndex[s[i] - 'a']); //记录当前段中的字母携带的最大的的裁切位置
if(i == cutIndex){//如果当前位置恰好是cutIndex记录的最大的裁切位置
result.push_back(i - lastcutIndex + 1);
lastcutIndex = cutIndex + 1;
}
}
return result;
}
56.合并区间
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(),
[](const vector<int> &a, const vector<int> &b){ return a[0] < b[0];});
vector<vector<int>> result;
result.emplace_back(intervals[0]);
for(int i = 1; i < intervals.size(); ++i){
if(intervals[i][0] <= result.back()[1]){
result.back()[1] = max(result.back()[1], intervals[i][1]);
}
else
result.emplace_back(intervals[i]);
}
return result;
}
使用了.back()函数来获取vector中的最后一个值,用来拓展重叠区间的右边,比我自己想的方法,需要记录两边的位置要方便多了。
738.单调递增的数组
说实话这道题比较抽象,要找到比当前数字小的最大的单调递增的数字
332->299
注意的点包括从后向前遍历
反正大概率如果是个大数的话,大概率后面有很多的9
int monotoneIncreasingDigits(int n) {
string str = to_string(n);
int flag = -1;
for(int i = str.size() - 1; i > 0; --i){
if(str[i-1] > str[i]){
flag = i;
str[i-1]--;
}
}
for(int i = flag; i < str.size(); ++i){
str[i] = '9';
}
return stoi(str);
}
注意to_string 和 stoi 的用法
968监控二叉树
从题目中示例,其实可以得到启发,我们发现题目示例中的摄像头都没有放在叶子节点上!
这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。
所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。
那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢?
因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。
所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!
局部最优推出全局最优,找不出反例,那么就按照贪心来!
此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。
来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:
有如下三种:
- 该节点无覆盖
- 本节点有摄像头
- 本节点有覆盖
我们分别有三个数字来表示:
- 0:该节点无覆盖
- 1:本节点有摄像头
- 2:本节点有覆盖
class Solution {
private:
int result;
int traversal(TreeNode* cur) {
// 空节点,该节点有覆盖
if (cur == NULL) return 2;
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
// 情况1
// 左右节点都有覆盖
if (left == 2 && right == 2) return 0;
// 情况2
// left == 0 && right == 0 左右节点无覆盖
// left == 1 && right == 0 左节点有摄像头,右节点无覆盖
// left == 0 && right == 1 左节点有无覆盖,右节点摄像头
// left == 0 && right == 2 左节点无覆盖,右节点覆盖
// left == 2 && right == 0 左节点覆盖,右节点无覆盖
if (left == 0 || right == 0) {
result++;
return 1;
}
// 情况3
// left == 1 && right == 2 左节点有摄像头,右节点有覆盖
// left == 2 && right == 1 左节点有覆盖,右节点有摄像头
// left == 1 && right == 1 左右节点都有摄像头
// 其他情况前段代码均已覆盖
if (left == 1 || right == 1) return 2;
// 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解
// 这个 return -1 逻辑不会走到这里。
return -1;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
// 情况4
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
};
// left == 2 && right == 1 左节点有覆盖,右节点有摄像头
// left == 1 && right == 1 左右节点都有摄像头
// 其他情况前段代码均已覆盖
if (left == 1 || right == 1) return 2;
// 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解
// 这个 return -1 逻辑不会走到这里。
return -1;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
// 情况4
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
};