目录
一、做题心得
今天是代码随想录打卡的第31天,迎来了贪心章节的最后部分。个人感觉今天的最后一题监控二叉树还是挺新颖的,巧妙地运用到了贪心思想。
话不多说,直接开始今天的内容。
二、题目与题解
题目一:56. 合并区间
题目链接
以数组
intervals
表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].示例 2:
输入:intervals = [[1,4],[4,5]] 输出:[[1,5]] 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。提示:
1 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 104
题解:排序+贪心
这个题也是很经典的重叠区间问题,和昨天的几道打卡题做法差不多,就不详细述说了。
这里需要注意的是:我们存放的结果区间是由不重叠区间以及合并之后的重叠区间构成。
如何合并两重叠区间呢?我们先存放前者区间(注意是有排序之后的)到结果数组里,再对刚存放的前者区间扩充到两区间(前者和后者区间)的范围--只需要右边界取两区间最后边(较大的终止位置)即可。
代码如下:
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> ans;
vector<int> vec;
int n = intervals.size();
sort(intervals.begin(), intervals.end(), cmp);
ans.push_back(intervals[0]);
for (int i = 1; i < n; i++) {
if (intervals[i][0] <= intervals[i - 1][1]) { //关键(合并重叠区间):当前区间与上一个区间重叠时,只需要在上一个区间的基础上选择两者间较大的end作为结束位置
ans.back()[1] = max(intervals[i - 1][1], intervals[i][1]);
intervals[i][1] = max(intervals[i - 1][1], intervals[i][1]); //更新合并区间后的右边界
}
else ans.push_back(intervals[i]);
}
return ans;
}
};
题目二:738.单调递增的数字
题目链接
当且仅当每个相邻位数上的数字
x
和y
满足x <= y
时,我们称这个整数是单调递增的。给定一个整数
n
,返回 小于或等于n
的最大数字,且数字呈 单调递增 。示例 1:
输入: n = 10 输出: 9示例 2:
输入: n = 1234 输出: 1234示例 3:
输入: n = 332 输出: 299提示:
0 <= n <= 109
题解:贪心
这道题不要想着在小于n的数中挨个遍历找到最大的满足单调递增的数字--这肯定还要自定义个函数判断(每个数都进去判断一次肯定会超时的)。
这道题采用贪心的思路。
思路:当发现当前数字 > 下一个数字时(不满足单调递增),就把当前数字 - 1,后面的所有数字都设置成 9。
这个思路是不是很巧妙?你仔细理一下就会发现的确如此。不过我们需要对每一个不满足递增的情况都进行同样的操作,这里我们采用从后往前遍历来实现(从前往后也可以,个人感觉会麻烦一些)。
注意:我们可以设置一个 flag 作为不满足单调递增位置的指标,随着从后往前遍历,不断更新flag,最后统一将flag及其之后的数字变为9。(这样就只需要最后的一次循环即可)
代码如下:
class Solution {
public:
int monotoneIncreasingDigits(int n) {
if (n < 10) { //特殊情况简单考虑
return n;
}
string s = to_string(n); //数字转化为字符串
int flag = s.size(); //初始化flag
for (int i = s.size() - 1; i > 0; i--) { //从右向左遍历字符串,找到不满足单调递增的位置
if (s[i - 1] > s[i]) { //左边数字大于右边数字--当前位置递减
flag = i; //更新flag位置
s[i - 1]--; //左边数字-1
}
}
for (int i = flag; i < s.size(); i++) { //从flag位置开始,到字符串的末尾,将所有的数字都设置为'9'
s[i] = '9';
}
return stoi(s); //重新转化为数字
}
};
题目三:968.监控二叉树
题目链接
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:
输入:[0,0,null,0,0] 输出:1 解释:如图所示,一台摄像头足以监控所有节点。示例 2:
输入:[0,0,null,0,null,0,null,null,0] 输出:2 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
提示:
- 给定树的节点数的范围是
[1, 1000]
。- 每个节点的值都是 0。
题解:贪心+二叉树后序遍历
这道题难度较大,主要是考虑的情况很多容易乱,我选择直接看题解,它用数字存储状态的方法很巧妙。
贪心策略:从下往上看,实现局部最优--让叶子节点的父节点安摄像头,所用摄像头最少 -- 即后序遍历实现 -- 左右根,实现从下往上遍历。
每个节点可能存在三种状态:(分别用0,1,2表示)
一.该节点无覆盖:0 (覆盖表示在摄像头范围内--包含父节点,子节点)
二.该节点有摄像头:1
三.该节点有覆盖:2 -- 这里不包含该节点有摄像头的情况
知道了以上之后,直接看代码,结合代码里的注释,会比较好理解:
class Solution {
public:
int ans;
int traversal(TreeNode* cur) { // 后序遍历:从下往上看,实现局部最优--让叶子节点的父节点安摄像头,所用摄像头最少
// 空节点等价于该节点有覆盖
if (cur == nullptr) return 2;
//后序遍历:左右根
int left = traversal(cur->left); // 左:递归遍历左子树
int right = traversal(cur->right); // 右:递归遍历右子树
// 情况1:左右子节点都有覆盖
if (left == 2 && right == 2) return 0; //当前节点不需要额外的摄像头来覆盖
// 情况2:左右子节点中至少有一个无覆盖
if (left == 0 || right == 0) {
ans++; // 需要在该节点装摄像头
return 1; // 当前节点已有摄像头(状态)
}
// 情况3:左右子节点中至少有一个有摄像头
if (left == 1 || right == 1) return 2; // 当前节点有覆盖(状态)-- 由子节点的摄像头覆盖
return -1; // 理论上不会执行到这里,但为了代码的完整性(只有if,没有else),可以返回一个错误码-1
}
int minCameraCover(TreeNode* root) {
ans = 0;
if (traversal(root) == 0) { // 遍历完成后,如果根节点无覆盖,就得在根节点处安装一个摄像头
ans++;
}
return ans;
}
};
三、小结
贪心章节的打卡到此也就结束了,不过贪心这一块的内容以后也会多加练习。最后,我是算法小白,但也希望终有所获。