738.单调递增的数字
力扣题目链接
题目描述:
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
示例 1:
输入: n = 10
输出: 9
示例 2:
输入: n = 1234
输出: 1234
示例 3:
输入: n = 332
输出: 299
思路:
- 暴力方法很好想,就是倒序遍历,然后判断每个数字是否符合条件就行了,但是提交后会超时😄。
- 细想一下,如果每次只是–1,然后再去判断的话,那么在上一个数字不符合条件时,那个位置之前做的所有判断就白做了。
- 那么能不能把之前的利用起来呢? 当然可以!我们把数字用库函数转换为字符串,然后去逐字符倒序比较(注意是倒序,不能是正序),遇到
s[i - 1] > s[i]
的情况就让s[i - 1] - 1
,然后让s[i]
之后色所有数字都变为9,只有这样才能保证一直符合条件也能保证是最大值(而不是把s[i - 1]
减到s[i] - 1
,然后s[i]
后面都不动,很显然只让s[i - 1] - 1
然后后面所有值变为9更大一些,本题的需求是求最大值!) - 为什么不是正序遍历?
- 因为正序遍历后,假如此时
s[i - 1]
和s[i]
是符合递增条件的,那后面万一s[i + 1] < s[i]
的话,必须让s[i] - 1
,那么此时可能s[i - 1] > s[i]
了,全面的又不符合条件了,唯有倒序遍历,后面的值一直比前面值大。
- 因为正序遍历后,假如此时
代码实现:
int monotoneIncreasingDigits(int n) {
if (n < 10) return n;
string s = to_string(n);
int flag = s.size();
for (int i = s.size() - 1; i > 0; --i) {
if (s[i] < s[i - 1]) {
s[i - 1]--;
flag = i;
}
}
//为了减少循环里多做的重复修改动作,直接设置一个flag位,在退出后,统一修改
for (int j = flag; j < s.size(); ++j) {
s[j] = '9';
}
return stoi(s);
}
968.监控二叉树
力扣题目链接
题目描述:
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:
输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。
思路:
- 这道题是贪心 + 二叉树的结合,在哪个节点放摄像机才能让摄像机数量整体保证最小?那根据贪心的思想肯定是放中间节点最好,因为其可以辐射两个子节点和一个父节点,叶子节点肯定不能放,因为它只能辐射自己的父节点,不可能让我们的总量最小。
- 那根据二叉树的做题思路,先判断用哪种遍历方式,因为我们不在叶子节点放摄像头,那么肯定需要判断左右子树的结果,是空还是叶子节点还是非叶子节点,肯定是
左 > 右 > 中
了,后序遍历走起!。 - 决定了遍历方式后,就要决定返回值,参数、函数退出逻辑以及单层递归逻辑,递归三部曲:
- 函数参数:参数肯定是树节点root,和传出参数count表示摄像机个数。
- 返回值:由于我们需要根据左右子树的函数返回值来判断当前节点是否需要安装摄像头,那么返回值肯定是左右子节点的状态了,状态无非是:安装或者未安装,但是未安装其实是有两种状态的,如果子节点安装了,那父节点就不必要安装了,此时是被覆盖而不用安装,还有一种就是没有被覆盖也没有安装的(比如叶子节点),所以,其实一共三种状态:未覆盖、安装、被覆盖,我们分别用0,1,2来表示,分别需要有不同的应对处理方式。
- 函数退出条件:
- 当遇到空节点时,返回2,为什么?因为空节点是不用管他的,就当它被覆盖了,但是肯定不能是0和1吧?
- 当遇到叶子节时,返回0,这个很好理解,叶子节点就要被它的父节点去覆盖。
- 当遇到非叶子节点,直接交给递归。(这一步就属于单步处理逻辑了,因为不用退出我们需要处理它的返回值)
- 单步处理逻辑:分别递归左右子节点,然后取其返回值进行比较
- 当
left == 0 || right == 0)
时,我们需要安装摄像头,因为子节点未覆盖,直接count++,然后返回 1。 - 当
left == 1 || right == 1
时,我们不需要安装摄像头了,因为子节点安装了,但是当前节点被覆盖了,所以返回 2。 - 剩下就是等于 2 的情况了,不用单独判断,直接返回 0。为啥? 因为子节点都是被覆盖,而当前节点最好交给父节点去判断,如果当前节点安装了摄像头就和叶子节点一个逻辑了,相当于只辐射了父节点,两个子节点人家已经被别的节点覆盖了!
- 当
- 另外注意根节点的判断,因为没人能处理root的返回值,只能交给调用递归函数的主函数了!
- 自己第一次的做题思路:想到了后序遍历,也想到了叶子节点不能放摄像头,就是没想到返回值有三种状态,只是返回了true或者false,安装了就返回true,未安装就返回false,然后处理返回值的时候,只要左右返回值有true就当前节点就返回false,因为是子节点安装了嘛,我肯定被辐射到了!左右节点返回值都是false,我自己就安装一个,其实这也是错误的地方,因为无法判断两个子节点是因为被覆盖而返回false,还是因为未被覆盖而返回的false
代码实现:
int traverse(TreeNode* root, int& count) {
if (root == nullptr) return 2;
if (!root->left && !root->right) return 0;
int left = traverse(root->left, count);
int right = traverse(root->right, count);
if (left == 0 || right == 0) {
count++;
return 1;
}
if (left == 1 || right == 1) return 2;
return 0;
}
int minCameraCover(TreeNode* root) {
int count = 0;
if (traverse(root, count) == 0) {
count++;
}
return count;
}
贪心算法阶段学习总结
- 学了一周贪心,除了覆盖问题有点套路外,其实题还是懵懵懂懂的,感觉做出来的题和贪心没啥大关系,哎,多练吧😑
- 下面做个总结:
- 什么是贪心?
- 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
- 大概分为四个步骤分析问题:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
- 这个很虚无缥缈,一般情况怎么拆解子问题都不好想,凭感觉如果觉得可以通过局部最优实现全局最优,且列举不出来反例,就可以试试贪心算法,可能做的题不多,体会不深很深😄。
- 简单题:
- 分发饼干:这道题就是对胃口数组和饼干数组排序,然后尽量把小饼干分给小胃口,大饼干分给大胃口,注意只用一个循环的写法,怎么写?
- K次取反后最大化的数组和:这道题要对数组根据绝对值排序,然后每次先反转负数,等都反转为正数后,取最小值重复反转就可以保证整体值最大,贪起来。
- 贪心算法:柠檬水找零, 这个题也很简单,就是把三种金额的币分别存起来,遇到20优先找10和5,没有10再找三个5,其他没有什么难度。
- 中等题:
- 单调递增的数字:就是上面做的第一道题,数字转换为字符串的处理思路,可以省去取余和除法操作!然后遇到不符合情况-1而不是减到比后一位少1
- 股票题:股票题最好用动态规划做,就只有一个题用贪心,性能会好一点,直接拿到相邻两天的股票价格差,取正数相加即可,可以在遍历的途中就取最值,得结果了,也可以滑动窗口。
- 区间覆盖问题:
- 一共是六道题,重叠区间四道题,有:435. 无重叠区间、763.划分字母区间、56. 合并区间、452. 用最少数量的箭引爆气球,除了划分字符串,其他三道题都是套模版,字母区间这道题就是记录每个字母的右边界,然后循环不断更新right,直到i走到right就是一个子序列
- 还有两道跳跃游戏题 也属于区间问题,跳跃游戏是不断更新自己能走到的最远处,只要最远处包含了数组最后一个元素就返回true,而跳跃游戏2是不仅要道最远处还要使得步数最小,那么就每次先走到第一步能走到的最远处,然后走的途中记录第二步能到的最远处,以便更新,第二部的右界,每次走到了右界,就判断下下一步能不能走到数组结尾,能得话步数+1退出 ,其实感觉和划分字母子区间的思路类似,都是不断更改上界,直到上界遇到某个临界值或者走到上界了要怎么处理。
- 两个维度权衡问题
- 分发糖果:先满足左或右边界,再满足另一边
- 根据身高重建队列:这道题必须先满足身高,然后再根据第二个元素去插入即可,注意这道题的链表实现,涉及到vector扩容问题,还有注意身高相同时要,怎么排序
- hard题目
- 加油站:这道题思路很难想,但是实现很简单,就是一个循环,然后不断求出当前剩余油是否是正数,如果是负数,那么起点一定在当前加油站之后。
- 最大连续子序和:和加油站一个思路,只要出现负数就不要了,从负数下一个点开始算,因为出现负数就会令整体和变小。
- 摆动序列:这道题就有点难度了,贪心思想贪的点在于找到一段序列让每个坡都只有开头和结束两个点,因为要比较坡度是否一样,就要和前一组之差比较,只要符号不同就算符合条件,
pre_diff > 0 && cur_diff < 0 || pre_diff < 0 && cur_diff > 0
,但要注意平坡的出现,以及什么时候更新pre_diff - 监控二叉树:后序+贪心,注意递归函数返回值的三种状态
- 什么是贪心?