Day31-贪心算法,leetcode56,738,968

  1. 合并区间
  • 以数组 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;
};


  1. 单调递增的数字
  • 当且仅当每个相邻位数上的数字 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;
};


  1. 监控二叉树
  • 给定一个二叉树,我们在树的节点上安装摄像头。
  • 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
  • 计算监控树的所有节点所需的最小摄像头数量。

  • 思路

  • 摄像头可以覆盖上中下三层,尽量在叶子节点的父节点放摄像头,充分利用摄像头的覆盖面积,一层层往上推,每隔两个节点放摄像头,,直至到二叉树头结点。

  • 每个节点可能的状态:

    • 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;
};



参考&感谢各路大神

posted @ 2025-06-27 09:23  安静的嘶吼  阅读(6)  评论(0)    收藏  举报