算法日记day 32(贪心之划分字母区间|合并区间|单调递增的数字|监控二叉树)

一、划分字母区间

题目:

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

示例 1:

输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。 

示例 2:

输入:s = "eccbbbbdec"
输出:[10]

思路:

如何确立划分的一个片段有多少是关键,我们需要记录同一个字母的下标,在循环遍历字符串的过程中,每当相同的字母出现一次就更新成最新的位置,在遍历时如果找到最远出现下标的位置,说明找到了分割点,此时就属于一个片段,记录其长度至结果数组中,重复此操作,直到找到遍历完全部字符串

代码:

public List<Integer> partitionLabels(String s) {
    List<Integer> list = new LinkedList<>(); // 创建一个存储结果的链表
    int[] edge = new int[26]; // 创建一个长度为26的数组,用于记录每个字母在字符串中最后出现的位置
    char[] chars = s.toCharArray(); // 将字符串转换为字符数组,方便遍历和操作
    for (int i = 0; i < chars.length; i++) {
        edge[chars[i] - 'a'] = i; // 记录每个字母在字符串中最后出现的位置
        //char[i] - 'a'为对应字母的ascii码形式
    }
    int left = 0; // 初始化左边界
    int right = 0; // 初始化右边界
    for (int i = 0; i < chars.length; i++) {
        right = Math.max(right, edge[chars[i] - 'a']); // 更新当前片段的右边界
        if (i == right) { // 如果当前位置i达到了当前片段的右边界
            list.add(right - left + 1); // 将当前片段的长度添加到结果列表中
            left = right + 1; // 更新下一个片段的左边界
        }
    }
    return list; // 返回结果列表
}

二、合并区间

题目:

以数组 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] 可被视为重叠区间。

思路:

首先对全部区间的左边界从小到大排序,遍历的过程中比较该区间的左边界是否小于上一个区间的右边界 ,如果是,说明这两个区间有重合部分需要合并,合并时需取两区间左边界较小的值,右边界较大的值,然后存入结果数组中,最后输出结果数组

代码:

public int[][] merge(int[][] intervals) {
    LinkedList<int[]> res = new LinkedList<>(); // 创建一个链表用于存储合并后的区间结果
    Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); // 根据区间的起始位置排序输入的二维数组

    res.add(intervals[0]); // 将第一个区间添加到结果链表中作为起始

    // 遍历排序后的区间数组
    for (int i = 1; i < intervals.length; i++) {
        // 如果当前区间的起始位置小于等于结果链表中最后一个区间的结束位置
        if (intervals[i][0] <= res.getLast()[1]) {
            int start = res.getLast()[0]; // 获取结果链表中最后一个区间的起始位置
            int end = Math.max(intervals[i][1], res.getLast()[1]); // 计算当前区间与结果链表中最后一个区间合并后的结束位置
            res.removeLast(); // 移除结果链表中最后一个区间
            res.add(new int[] { start, end }); // 将合并后的区间添加到结果链表中
        } else {
            res.add(intervals[i]); // 如果当前区间不能与结果链表中最后一个区间合并,则直接添加当前区间到结果链表中
        }
    }

    return res.toArray(new int[res.size()][]); // 将结果链表转换为二维数组并返回
}
  1. 链表初始化:

    • LinkedList<int[]> res = new LinkedList<>();
      • 创建一个链表 res 用来存储合并后的区间结果。
  2. 排序:

    • Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
      • 根据二维数组 intervals 中每个子数组的第一个元素(区间的起始位置)进行排序。
  3. 添加第一个区间:

    • res.add(intervals[0]);
      • 将排序后的第一个区间添加到链表 res 中作为起始。
  4. 合并区间:

    • 使用 for 循环遍历排序后的区间数组 intervals
      • 如果当前区间的起始位置小于等于链表 res 中最后一个区间的结束位置,则说明这两个区间可以合并。
      • 计算合并后的起始位置为链表中最后一个区间的起始位置,合并后的结束位置为当前区间和链表中最后一个区间结束位置的较大值。
      • 更新链表 res 中最后一个区间为合并后的区间。
      • 如果当前区间不能与链表 res 中最后一个区间合并,则直接将当前区间添加到链表 res 中。
  5. 返回结果:

    • return res.toArray(new int[res.size()][]);
      • 将链表 res 转换为二维数组并返回,这个二维数组包含了所有合并后的区间。

三、单调递增的数字

题目:

当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。

示例 1:

输入: n = 10
输出: 9

示例 2:

输入: n = 1234
输出: 1234

示例 3:

输入: n = 332
输出: 299

思路:

如果n中的前一位数大于后一位数,则让前一位值减一,为了使其数得到的为最大,后面位的数字全部变为9

代码:

public int monotoneIncreasingDigits(int n) {
    // 将整数 n 转换为字符串
    String str = String.valueOf(n);
    // 将字符串转换为字符数组,方便后续操作
    char[] chars = str.toCharArray();
    // 记录递减位置的标志位,初始为字符串长度
    int flag = str.length();
    
    // 从字符串的倒数第二位开始向前遍历
    for (int i = str.length() - 1; i > 0; i--) {
        // 如果前一位比当前位大,说明需要进行调整
        if (chars[i - 1] > chars[i]) {
            // 将前一位减1
            chars[i - 1]--;
            // 更新递减位置的标志位
            flag = i;
        }
    }
    
    // 将从 flag 位置开始的所有字符设置为 '9',以确保最大化单调递增
    for (int i = flag; i < str.length(); i++) {
        chars[i] = '9';
    }
    
    // 将字符数组重新转换为整数并返回
    return Integer.parseInt(String.valueOf(chars));
}
  1. 字符串处理:

    • String str = String.valueOf(n);
      • 将输入的整数 n 转换为字符串 str,方便后续操作。
  2. 字符数组准备:

    • char[] chars = str.toCharArray();
      • 将字符串 str 转换为字符数组 chars,以便可以直接修改其中的字符内容。
  3. 递减位置标志初始化:

    • int flag = str.length();
      • flag 用于记录发生递减的位置,默认初始化为字符串的长度。如果发生递减,将更新为当前递减的位置。
      • 这里flag初始化为字符串长度还有个好处,若是初始化为0,如果本身的数就为单调递增(例如n=1234),此时在运行至 for (int i = flag; i < str.length(); i++) { chars[i] = '9'; } 时,就会把原有数字全部覆盖为9999,得到错误的结果,因此初始化为字符串长度避免了这种情况
  4. 递减位置处理:

    • 使用从字符串末尾开始的循环,比较每一位和它前一位的大小关系:
      • if (chars[i - 1] > chars[i]):如果前一位比当前位大,表示发生了递减。
      • chars[i - 1]--:将前一位减去1,以确保满足单调递增的要求。
      • flag = i;:更新 flag,记录最后一次发生递减的位置。
  5. 最大化单调递增:

    • 从 flag 位置开始,将所有字符设为 '9',因为从这个位置开始的所有位都已经被调整为符合单调递增的最大值。
  6. 返回结果:

    • return Integer.parseInt(String.valueOf(chars));
      • 将字符数组 chars 转换为字符串,然后再转换为整数,并将其作为结果返回。

四、监控二叉树

题目:

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例 1:

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。

思路:

首先定义状态

0为未被摄像头覆盖区域

1为安装了摄像头区域

2为被摄像头覆盖的区域

总共可分为四种情况

1、根节点的左右子节点都有覆盖(都为状态2)

此时根节点为未被覆盖状态0,返回0

2、左右子节点至少有一个没有被覆盖(至少一个为状态0)

此时根节点必须安装摄像头,才能保证子节点均被覆盖,返回1

3、左右子节点至少有一个装有摄像头(至少一个为状态1)

此时根节点为被覆盖状态,返回1

4、再遍历完所有子节点后,返回至第一层时的根节点无覆盖

此时需要在根节点加入一个摄像头,返回1

代码:

class Solution {
    int result = 0; // 全局变量,记录摄像头数量

    public int minCameraCover(TreeNode root) {
        if (traversal(root) == 0) { // 如果根节点需要安装摄像头
            result++; // 增加摄像头数量
        }
        return result;
    }

    // 后序遍历递归方法,返回当前节点的状态
    // 0:当前节点需要安装摄像头
    // 1:当前节点已经安装了摄像头
    // 2:当前节点被覆盖但不需要额外摄像头
    private int traversal(TreeNode root) {
        if (root == null) {
            return 2; // 空节点被覆盖
        }

        int left = traversal(root.left); // 遍历左子树
        int right = traversal(root.right); // 遍历右子树

        // 如果左右子树有任意一个需要安装摄像头
        if (left == 0 || right == 0) {
            result++; // 在当前节点安装摄像头
            return 1; // 返回1表示当前节点已经安装了摄像头
        }

        // 如果左右子树都被覆盖且无需额外摄像头
        if (left == 2 && right == 2) {
            return 0; // 返回0表示当前节点需要安装摄像头
        }

        return 2; // 其他情况,返回2表示当前节点被覆盖但不需要额外摄像头
    }
}
  1. result 是全局变量,用来记录整棵树需要安装的摄像头数量。
    • minCameraCover 方法是公共方法,用于计算整棵二叉树最少需要安装的摄像头数量。
      • 如果根节点需要安装摄像头,则增加 result 的计数。
      • 返回最终的 result 数量。
    • 后序遍历的递归方法,用于确定每个节点的状态。
    • 参数 root 是当前要处理的节点。
    • 返回值说明:
      • 0:当前节点需要安装摄像头。
      • 1:当前节点已经安装了摄像头。
      • 2:当前节点被覆盖但不需要额外摄像头。

今天的学习就到这里 

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值