代码随想录训练第二十六天|LeetCode452. 用最少数量的箭引爆气球、LeetCode435.无重叠区间、LeetCode763.划分字母区间

452. 用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``startx``end, 且满足 xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

示例 1:

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。

示例 2:

输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。

示例 3:

输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。

提示:

  • 1 <= points.length <= 105
  • points[i].length == 2
  • -231 <= xstart < xend <= 231 - 1

Related Topics

  • 贪心

  • 数组

  • 排序

思路

如何使用最少的弓箭呢?

直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少,那么有没有当前重叠了三个气球,我射两个,留下一个和后面的一起射这样弓箭用的更少的情况呢?

尝试一下举反例,发现没有这种情况。

那么就试一试贪心吧!局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。

算法确定下来了,那么如何模拟气球射爆的过程呢?是在数组中移除元素还是做标记呢?

如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。

但仔细思考一下就发现:如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remove气球,只要记录一下箭的数量就可以了。

以上为思考过程,已经确定下来使用贪心了,那么开始解题。

为了让气球尽可能的重叠,需要对数组进行排序

那么按照气球起始位置排序,还是按照气球终止位置排序呢?

其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。

既然按照起始位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。

从前向后遍历遇到重叠的气球了怎么办?

如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭

以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序)

452.用最少数量的箭引爆气球

可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。

public int findMinArrowShots(int[][] points) {
    //先进行数组排序
    // 根据气球直径的开始坐标从小到大排序
    Arrays.sort(points, (a, b) -> {
        return Integer.compare(a[0], b[0]);
    });
    int count = 1;
    //int right = points[0][1];
    for (int i = 1; i < points.length; i++) {
        //如果当前节点的左边界大于上一个节点的右边界
        //则需要新增一根箭
        if (points[i][0] > points[i - 1][1]) {
            count++;
        }
        //如果有重合部分
        //则更新当前区间的最小右边界
        else {
            points[i][1] = Math.min(points[i][1], points[i - 1][1]);
        }
    }
    return count;
}
  • 时间复杂度:O(nlog n),因为有一个快排
  • 空间复杂度:O(1),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间

注意:题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆,

所以代码中 if (points[i][0] > points[i - 1][1]) 不能是>=

总结

寻找重复的气球,寻找重叠气球最小右边界,其实都有代码技巧。

435.无重叠区间

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠

示例 1:

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:

输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

提示:

  • 1 <= intervals.length <= 105
  • intervals[i].length == 2
  • -5 * 104 <= starti < endi <= 5 * 104

Related Topics

  • 贪心

  • 数组

  • 动态规划

  • 排序

思路

相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?

其实都可以。主要就是为了让区间尽可能的重叠。

我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了

此时问题就是要求非交叉区间的最大个数。

这里记录非交叉区间的个数还是有技巧的,如图:

img

区间,1,2,3,4,5,6都按照右边界排好序。

当确定区间 1 和 区间2 重叠后,如何确定是否与 区间3 也重贴呢?

就是取 区间1 和 区间2 右边界的最小值,因为这个最小值之前的部分一定是 区间1 和区间2 的重合部分,如果这个最小值也触达到区间3,那么说明 区间 1,2,3都是重合的。

接下来就是找大于区间1结束位置的区间,是从区间4开始。那有同学问了为什么不从区间5开始?别忘了已经是按照右边界排序的了

区间4结束之后,再找到区间6,所以一共记录非交叉区间的个数是三个。

总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。

只要按照右边界进行排序,只要上一个的右边界,与下一个区间有重合的部分,说明需要去掉下一个区间

public int eraseOverlapIntervals(int[][] intervals) {
    if (intervals.length == 0) {
        return 0;
    }
    Arrays.sort(intervals, (a, b) -> {
        return a[1] - b[1];
    });
    //记录当前未交叉的区间数量
    int count = 1;
    //记录当前分割点,也就是有边界
    int end = intervals[0][1];
    for (int i = 1; i < intervals.length; i++) {
        //如果当期右边界,与当前区间的左边界没有重合的部分,则需要更改count,并且更新右边界
        if (end <= intervals[i][0]) {
            count++;
            end = intervals[i][1];
        }
        //如果有重合部分,则直接舍去当前区间,啥也不用干
    }
    return intervals.length - count;
}
  • 时间复杂度:O(nlog n) ,有一个快排
  • 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间

补充1

左边界排序可不可以呢?

也是可以的,只不过 左边界排序我们就是直接求 重叠的区间,count为记录重叠区间数。

public int eraseOverlapIntervals(int[][] intervals) {
    if (intervals.length == 0) {
        return 0;
    }
    //使用左边界进行排序
    Arrays.sort(intervals, (a, b) -> {
        return a[0] - b[0];
    });
    //记录重叠区间的数量
    int count = 0;
    //记录上一个区间的右边界
    int right = intervals[0][1];
    for (int i = 1; i < intervals.length; i++) {
        //没有重合部分
        if (right <= intervals[i][0]) {
            //更新右边界
            right = intervals[i][1];
        } else {
            //记录数量
            count++;
            //更新右边界
            right = Math.min(right, intervals[i][1]);
        }
    }
    return count;
}

补充2

本题其实和452.用最少数量的箭引爆气球 (opens new window)非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。

public int eraseOverlapIntervals(int[][] intervals) {
    if (intervals.length == 0) {
        return 0;
    }
    //使用右边界进行排序
    Arrays.sort(intervals, (a, b) -> {
        return a[1] - b[1];
    });
    //points不为空至少需要一支箭(也就是移除的区间数量)
    int count = 1;

    for (int i = 1; i < intervals.length; i++) {
        //如果没有重合部分
        if (intervals[i][0] >= intervals[i - 1][1]) {
            //此时需要加一支箭
            count++;
        } else {
            //修改当前右边界
            intervals[i][1] = Math.min(intervals[i][1], intervals[i - 1][1]);
        }
    }
    return intervals.length - count;
}

763.划分字母区间

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

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

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= s.length <= 500
  • s 仅由小写英文字母组成

Related Topics

  • 贪心

  • 哈希表

  • 双指针

  • 字符串

思路

一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索。

题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢?

如果没有接触过这种题目的话,还挺有难度的。

在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。

可以分为如下两步:

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

如图:

763.划分字母区间

public List<Integer> partitionLabels(String s) {
    //创建一个数组,找到
    int[] hash = new int[27];
    //统计每一个字符出现的最后位置
    for (int i = 0; i < s.length(); i++) {
        hash[s.charAt(i) - 'a'] = i;
    }
    List<Integer> res = new ArrayList<>();
    int left = 0;
    int right = 0;
    for (int i = 0; i < s.length(); i++) {
        //找到字符出现的最远边界
        //也就是更新字符出现的最远边界
        right = Math.max(right, hash[s.charAt(i) - 'a']);
        //如果到达了当前字符串的最远边界,则进行记录,并且更新区间
        if (i == right) {
            res.add(right - left + 1);
            left = i + 1;
        }
    }
    return res;
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1),使用的hash数组是固定大小
  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值