贪心算法之区间调度问题.md 13.5 KB
Newer Older
B
brucecat 已提交
1
# 贪心算法之区间调度问题
L
labuladong 已提交
2

3 4
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
5
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
6 7 8 9
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
10
![](https://labuladong.github.io/algo/images/souyisou1.png)
11

L
labuladong 已提交
12
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.0;[第 13 期刷题打卡](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名!另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
13 14 15



L
labuladong 已提交
16 17 18 19 20 21
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [435. Non-overlapping Intervals](https://leetcode.com/problems/non-overlapping-intervals/) | [435. 无重叠区间](https://leetcode.cn/problems/non-overlapping-intervals/) | 🟠
| [452. Minimum Number of Arrows to Burst Balloons](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/) | [452. 用最少数量的箭引爆气球](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/) | 🟠
22 23 24

**-----------**

L
labuladong 已提交
25 26 27 28 29 30 31 32
什么是贪心算法呢?贪心算法可以认为是动态规划算法的一个特例,相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质),但是效率比动态规划要高。

比如说一个算法问题使用暴力解法需要指数级时间,如果能使用动态规划消除重叠子问题,就可以降到多项式级别的时间,如果满足贪心选择性质,那么可以进一步降低时间复杂度,达到线性级别的。

什么是贪心选择性质呢,简单说就是:每一步都做出一个局部最优的选择,最终的结果就是全局最优。注意哦,这是一种特殊性质,其实只有一部分问题拥有这个性质。

比如你面前放着 100 张人民币,你只能拿十张,怎么才能拿最多的面额?显然每次选择剩下钞票中面值最大的一张,最后你的选择一定是最优的。

L
labuladong 已提交
33
然而,大部分问题明显不具有贪心选择性质。比如打斗地主,对手出对儿三,按照贪心策略,你应该出尽可能小的牌刚好压制住对方,但现实情况我们甚至可能会出王炸。这种情况就不能用贪心算法,而得使用动态规划解决,参见前文 [动态规划解决博弈问题](https://labuladong.github.io/article/fname.html?fname=动态规划之博弈问题)
L
labuladong 已提交
34 35 36

### 一、问题概述

L
labuladong 已提交
37 38 39
言归正传,本文解决一个很经典的贪心算法问题 Interval Scheduling(区间调度问题),也就是力扣第 435 题「无重叠区间」:

给你很多形如 `[start, end]` 的闭区间,请你设计一个算法,**算出这些区间中最多有几个互不相交的区间**
L
labuladong 已提交
40 41

```java
L
labuladong 已提交
42
int intervalSchedule(int[][] intvs);
L
labuladong 已提交
43 44 45 46
```

举个例子,`intvs = [[1,3], [2,4], [3,6]]`,这些区间最多有 2 个区间互不相交,即 `[[1,3], [3,6]]`,你的算法应该返回 2。注意边界相同并不算相交。

L
labuladong 已提交
47
这个问题在生活中的应用广泛,比如你今天有好几个活动,每个活动都可以用区间 `[start, end]` 表示开始和结束的时间,请问你今天**最多能参加几个活动呢**?显然你一个人不能同时参加两个活动,所以说这个问题就是求这些时间区间的最大不相交子集。
L
labuladong 已提交
48 49 50 51 52 53 54 55 56

### 二、贪心解法

这个问题有许多看起来不错的贪心思路,却都不能得到正确答案。比如说:

也许我们可以每次选择可选区间中开始最早的那个?但是可能存在某些区间开始很早,但是很长,使得我们错误地错过了一些短的区间。或者我们每次选择可选区间中最短的那个?或者选择出现冲突最少的那个区间?这些方案都能很容易举出反例,不是正确的方案。

正确的思路其实很简单,可以分为以下三步:

L
labuladong 已提交
57 58 59
1、从区间集合 `intvs` 中选择一个区间 `x`,这个 `x` 是在当前所有区间中**结束最早的**`end` 最小)。

2、把所有与 `x` 区间相交的区间从区间集合 `intvs` 中删除。
L
labuladong 已提交
60

L
labuladong 已提交
61
3、重复步骤 1 和 2,直到 `intvs` 为空为止。之前选出的那些 `x` 就是最大不相交子集。
L
labuladong 已提交
62

L
labuladong 已提交
63
把这个思路实现成算法的话,可以按每个区间的 `end` 数值升序排序,因为这样处理之后实现步骤 1 和步骤 2 都方便很多,如下 GIF 所示:
L
labuladong 已提交
64

L
labuladong 已提交
65
![](https://labuladong.github.io/algo/images/interval/1.gif)
L
labuladong 已提交
66

L
labuladong 已提交
67
现在来实现算法,对于步骤 1,由于我们预先按照 `end` 排了序,所以选择 `x` 是很容易的。关键在于,如何去除与 `x` 相交的区间,选择下一轮循环的 `x` 呢?
L
labuladong 已提交
68

L
labuladong 已提交
69 70 71
**由于我们事先排了序**,不难发现所有与 `x` 相交的区间必然会与 `x``end` 相交;如果一个区间不想与 `x``end` 相交,它的 `start` 必须要大于(或等于)`x``end`

![](https://labuladong.github.io/algo/images/interval/2.jpg)
L
labuladong 已提交
72 73 74 75 76 77 78

看下代码:

```java
public int intervalSchedule(int[][] intvs) {
    if (intvs.length == 0) return 0;
    // 按 end 升序排序
Z
zhangxian 已提交
79
    Arrays.sort(intvs, new Comparator<int[]>() {
L
labuladong 已提交
80
        public int compare(int[] a, int[] b) {
L
labuladong 已提交
81
            return a[1] - b[1];
L
labuladong 已提交
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
        }
    });
    // 至少有一个区间不相交
    int count = 1;
    // 排序后,第一个区间就是 x
    int x_end = intvs[0][1];
    for (int[] interval : intvs) {
        int start = interval[0];
        if (start >= x_end) {
            // 找到下一个选择的区间了
            count++;
            x_end = interval[1];
        }
    }
    return count;
}
```

### 三、应用举例

L
labuladong 已提交
102
下面再举例几道具体的题目应用一下区间调度算法。
L
labuladong 已提交
103

L
labuladong 已提交
104
首先是力扣第 435 题「无重叠区间」问题:
L
labuladong 已提交
105

L
labuladong 已提交
106 107 108 109 110 111 112 113 114
输入一个区间的集合,请你计算,要想使其中的区间都互不重叠,至少需要移除几个区间?函数签名如下:

```java
int eraseOverlapIntervals(int[][] intvs);
```

其中,可以假设输入的区间的终点总是大于起点,另外边界相等的区间只算接触,但并不算相互重叠。

比如说输入是 `intvs = [[1,2],[2,3],[3,4],[1,3]]`,算法返回 1,因为只要移除 `[1,3]` 后,剩下的区间就没有重叠了。
L
labuladong 已提交
115 116 117 118 119 120 121 122 123 124

我们已经会求最多有几个区间不会重叠了,那么剩下的不就是至少需要去除的区间吗?

```java
int eraseOverlapIntervals(int[][] intervals) {
    int n = intervals.length;
    return n - intervalSchedule(intervals);
}
```

L
labuladong 已提交
125
再说说力扣第 452 题「用最少的箭头射爆气球」,我来描述一下题目:
L
labuladong 已提交
126

L
labuladong 已提交
127 128 129 130 131 132 133 134 135
假设在二维平面上有很多圆形的气球,这些圆形投影到 x 轴上会形成一个个区间对吧。那么给你输入这些区间,你沿着 x 轴前进,可以垂直向上射箭,请问你至少要射几箭才能把这些气球全部射爆呢?

函数签名如下:

```java
int findMinArrowShots(int[][] intvs);
```

比如说输入为 `[[10,16],[2,8],[1,6],[7,12]]`,算法应该返回 2,因为我们可以在 `x` 为 6 的地方射一箭,射爆 `[2,8]``[1,6]` 两个气球,然后在 `x` 为 10,11 或 12 的地方射一箭,射爆 `[10,16]``[7,12]` 两个气球。
L
labuladong 已提交
136 137 138

其实稍微思考一下,这个问题和区间调度算法一模一样!如果最多有 `n` 个不重叠的区间,那么就至少需要 `n` 个箭头穿透所有区间:

L
labuladong 已提交
139
![](https://labuladong.github.io/algo/images/interval/3.jpg)
L
labuladong 已提交
140 141 142

只是有一点不一样,在 `intervalSchedule` 算法中,如果两个区间的边界触碰,不算重叠;而按照这道题目的描述,箭头如果碰到气球的边界气球也会爆炸,所以说相当于区间的边界触碰也算重叠:

L
labuladong 已提交
143
![](https://labuladong.github.io/algo/images/interval/4.jpg)
L
labuladong 已提交
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162

所以只要将之前的算法稍作修改,就是这道题目的答案:

```java
int findMinArrowShots(int[][] intvs) {
    // ...

    for (int[] interval : intvs) {
        int start = interval[0];
        // 把 >= 改成 > 就行了
        if (start > x_end) {
            count++;
            x_end = interval[1];
        }
    }
    return count;
}
```

L
labuladong 已提交
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
接下来可阅读:

* [贪心算法之跳跃游戏](https://labuladong.github.io/article/fname.html?fname=跳跃游戏)



<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [一个方法解决三道区间问题](https://labuladong.github.io/article/fname.html?fname=区间问题合集)
 - [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法)
 - [剪视频剪出一个贪心算法](https://labuladong.github.io/article/fname.html?fname=剪视频)
 - [如何运用贪心思想玩跳跃游戏](https://labuladong.github.io/article/fname.html?fname=跳跃游戏)
 - [扫描线技巧:安排会议室](https://labuladong.github.io/article/fname.html?fname=安排会议室)

</details><hr>



183 184


185
**_____________**
186

L
labuladong 已提交
187
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
188 189

![](https://labuladong.github.io/algo/images/souyisou2.png)
L
labuladong 已提交
190 191


E
Ed 已提交
192 193
======其他语言代码======

B
brucecat 已提交
194 195 196 197
[435. 无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/)

[452.用最少数量的箭引爆气球](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons)

B
BruceCat 已提交
198 199
### python
Edwenc 提供 第435题的python3 代码:
E
Ed 已提交
200

B
BruceCat 已提交
201
```python  
E
Ed 已提交
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        ###  思路是首先找到不重叠的区间的个数
        ###  然后再用总个数减去不重叠个数  
        ###  获得的就是  需要移除的个数

        #  首先获得区间的个数  为0的话就不用移除
        n = len(intervals)
        if n==0:
            return 0

        #  按照每个区间的右端点值进行排序
        sorted_list = sorted( intervals , key=lambda x: x[1] )

        #  不重叠区间个数至少是1
        count = 1

        #  end是所有不重叠的区间中  最大的右端点
        #  end的初始值即是sorted_list[0]的右端点
        end = sorted_list[0][1]

E
Ed 已提交
223
        #  从1开始往后找  因为0在上面已经取过了
E
Ed 已提交
224 225 226
        for i in range(1,n):
            #  start是当前区间左端点值
            start = sorted_list[i][0] 
E
Ed 已提交
227
            #  如果当前左端点比最大右端点都大了(可能相等)  
E
Ed 已提交
228 229 230 231 232 233 234
            #  说明两区间不重叠  count+1  再更新end     
            if start>=end:
                count += 1
                end = sorted_list[i][1]

        #  最后返回的是  需要移除的区间个数
        return n-count
B
brucecat 已提交
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
```



### javascript

**区间调度实现**

```js
var intervalSchedule = function (intvs) {
    if (intvs.length === 0) return 0;
    // 按end升序排序
    intvs.sort((a, b) => {
        if (a[1] < b[1])
            return -1;
        else if (a[1] > b[1])
            return 1;
        else return 0;
    })

    // 至少有一个区间不相交
    let count = 1;

    // 排序后,第一个区间就是 x
    let x_end = intvs[0][1];
    for (let interval of intvs) {
        let start = interval[0];
        if (start >= x_end) {
            // 找到下一个选择的区间了
            count++;
            x_end = interval[1];
        }
    }
    return count;
}
```

**第435题 无重叠区间**

```js
/**
 * @param {number[][]} intervals
 * @return {number}
 */
var eraseOverlapIntervals = function (intervals) {
    let n = intervals.length;
    // 我们已经会求最多有几个区间不会重叠了,那么剩下的不就是至少需要去除的区间吗?
    return n - intervalSchedule(intervals);
};

var intervalSchedule = function (intvs) {
    if (intvs.length === 0) return 0;
    // 按end升序排序
    intvs.sort((a, b) => {
        if (a[1] < b[1])
            return -1;
        else if (a[1] > b[1])
            return 1;
        else return 0;
    })

    // 至少有一个区间不相交
    let count = 1;

    // 排序后,第一个区间就是 x
    let x_end = intvs[0][1];
    for (let interval of intvs) {
        let start = interval[0];
        if (start >= x_end) {
            // 找到下一个选择的区间了
            count++;
            x_end = interval[1];
        }
    }
    return count;
}
```

**第452题 用最少数量的箭引爆气球**

```js
/**
 * @param {number[][]} points
 * @return {number}
 */
var findMinArrowShots = function (intvs) {
    if (intvs.length === 0) return 0;
    // 按end升序排序
    intvs.sort((a, b) => {
        if (a[1] < b[1])
            return -1;
        else if (a[1] > b[1])
            return 1;
        else return 0;
    })

    // 至少有一个区间不相交
    let count = 1;

    // 排序后,第一个区间就是 x
    let x_end = intvs[0][1];
    for (let interval of intvs) {
        let start = interval[0];
        if (start > x_end) {
            // 找到下一个选择的区间了
            count++;
            x_end = interval[1];
        }
    }
    return count;
};
```