Day31-贪心算法,leetcode56,738,968
- 合并区间
- 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
- 思路
- 先对数组进行排序,从小到大排序,定义res存放结果集
- 遍历,判断当前区间和上一个区间是否重叠,当前区间的左边界《上一个区间的右边界,是的话表示重叠,重叠的话将两个区间进行合并,左边界为上一个区间的左边界,右边界为当前区间和上一个区间的右边界的最大值,如果不重叠则直接将当前区间存入结果集
- 返回res
/**
1. 初始化结果集
const result = []; 用于存放合并后的区间。
2. 特殊情况处理
如果输入数组为空,直接返回空数组。
3. 排序
intervals.sort((a, b) => a[0] - b[0]);
按区间的左端点从小到大排序,方便后续合并。
4. 将第一个区间加入结果集
result.push(intervals[0]);
5. 遍历剩余区间
如果当前区间的左端点小于等于结果集中最后一个区间的右端点,说明有重叠,合并区间(更新右端点为较大值)。
否则,说明没有重叠,直接加入结果集。
6. 返回结果
返回合并后的区间数组。
排序保证了每次只需要和结果集最后一个区间比较即可。
合并时只需更新右端点,因为左端点已经是最小的。
先排序,再依次合并重叠区间,最后返回合并后的结果。
*/
var merge = function(intervals) {
const result = [];
if (intervals.length === 0) return result;
// 按左端点升序排序
intervals.sort((a, b) => a[0] - b[0]);
// 先把第一个区间放入结果集
result.push(intervals[0]);
for (let i = 1; i < intervals.length; i++) {
// 如果当前区间和结果集最后一个区间重叠,合并
if (result[result.length - 1][1] >= intervals[i][0]) {
result[result.length - 1][1] = Math.max(result[result.length - 1][1], intervals[i][1]);
} else {
// 不重叠,直接加入结果集
result.push(intervals[i]);
}
}
return result;
};
/**
1. 排序:先按左端点升序排序,方便后续合并。
2. 初始化:用 left 和 right 记录当前正在合并的区间的左右端点。
3. 遍历:从第二个区间开始遍历:
如果当前区间的左端点大于 right,说明和前面的区间不重叠,把 [left, right] 加入结果,并更新 left 和 right。
如果有重叠,只需更新 right 为较大值,继续合并。
4. 最后:循环结束后,把最后一个合并区间加入结果。
这种写法用两个变量 left 和 right 维护当前合并区间,而不是直接操作结果数组的最后一个元素。
只有在遇到不重叠区间时才把合并结果推入数组,最后别忘了推入最后一个区间。
用两个变量维护当前合并区间,遇到不重叠时推入结果,最后别忘了推入最后一个区间。这样可以高效地合并所有重叠区间。
*/
var merge = function(intervals) {
let n = intervals.length;
if (n < 2) return intervals; // 如果区间数小于2,直接返回
intervals.sort((a, b) => a[0] - b[0]); // 按左端点升序排序
let res = [];
let left = intervals[0][0], right = intervals[0][1]; // 初始化第一个区间的左右端点
for (let i = 1; i < n; i++) {
// 如果当前区间的左端点大于上一个区间的右端点,说明不重叠
if (intervals[i][0] > right) {
res.push([left, right]); // 把上一个合并区间加入结果
left = intervals[i][0]; // 更新left和right为当前区间
right = intervals[i][1];
} else {
// 有重叠,合并区间,更新右端点为较大值
right = Math.max(intervals[i][1], right);
}
}
// 最后一个区间别忘了加入结果
res.push([left, right]);
return res;
};
/**
1. 排序:先按左端点排序,保证遍历时只需和上一个区间比较。
2. prev:始终保存当前正在合并的区间。
3. 合并逻辑:
如果当前区间的左端点大于 prev 的右端点,说明不重叠,把 prev 加入结果,并更新 prev。
如果有重叠,只需更新 prev 的右端点为较大值,继续合并。
4. 最后:循环结束后,把最后一个 prev 加入结果。
用 prev 变量维护当前合并区间,遇到不重叠时推入结果,最后别忘了推入最后一个区间,实现高效合并所有重叠区间。
*/
var merge = function (intervals) {
intervals.sort((a, b) => a[0] - b[0]); // 1. 按左端点升序排序
let prev = intervals[0]; // 2. 用 prev 记录当前合并区间
let result = [];
for(let i = 0; i < intervals.length; i++){
let cur = intervals[i];
if(cur[0] > prev[1]){ // 3. 如果当前区间和 prev 不重叠
result.push(prev); // 把 prev 加入结果
prev = cur; // 更新 prev 为当前区间
}else{
prev[1] = Math.max(cur[1], prev[1]); // 4. 有重叠,合并区间(更新右端点)
}
}
result.push(prev); // 5. 最后一个区间别忘了加入结果
return result;
};
- 单调递增的数字
- 当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
- 给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
- 思路
/**
1. 转数组:方便逐位操作。
2. 从后往前遍历:一旦发现前一位比后一位大,前一位减1,当前位置及其后面都要变成9(保证最大)。
3. flag:记录第一个需要变9的位置,后面全部变9。
4. 最后转回数字返回。
举例
比如 n = 332:
第一次发现 3 > 2,把前一个3减1变2,flag=2,后面都变9,得到329。2又小于了第一位的3了,flag=1,真正的结果应该是299。
从后往前找到不是单调递增的位置,把前一位减1,后面全变9,保证结果最大且单调递增。
*/
var monotoneIncreasingDigits = function(n) {
n = n.toString(); // 转成字符串
n = n.split('').map(item => +item); // 转成数字数组
let flag = Infinity; // 记录需要变9的起始位置
// 从后往前遍历
for(let i = n.length - 1; i > 0; i--) {
if(n[i - 1] > n[i]) { // 如果前一位比后一位大,说明不是单调递增
flag = i; // 记录需要变9的位置
n[i - 1] = n[i - 1] - 1; // 前一位减1
n[i] = 9; // 当前位先变9(后面还会全部变9)
}
}
// flag及其后面的全部变成9
for(let i = flag; i < n.length; i++) {
n[i] = 9;
}
n = n.join('');
return +n;
};
- 监控二叉树
- 给定一个二叉树,我们在树的节点上安装摄像头。
- 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
- 计算监控树的所有节点所需的最小摄像头数量。
-
思路
-
摄像头可以覆盖上中下三层,尽量在叶子节点的父节点放摄像头,充分利用摄像头的覆盖面积,一层层往上推,每隔两个节点放摄像头,,直至到二叉树头结点。
-
每个节点可能的状态:
- 0 无覆盖
- 1 有摄像头
- 2 右覆盖
var minCameraCover = function(root) {
let result = 0;
// 定义遍历函数,返回当前节点的状态
// 后序遍历,根据左右节点的情况,来判断 自己的状态
// 0: 无覆盖,1: 有摄像头,2: 有覆盖
function traversal(node) {
// 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头
if (node === null) return 2; // 空节点视为有覆盖
let left = traversal(node.left);
let right = traversal(node.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;
}
// 情况4:如果根节点无覆盖,需要再加一个摄像头
if (traversal(root) === 0) {
result++;
}
return result;
};
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。

浙公网安备 33010602011771号