【ACWing】算法基础课-第六章贪心-区间问题

目录

905.区间选点

908.最大不相交区间数量

906.区间分组

907.区间覆盖


905.区间选点

题目: 

给定N个闭区间[ai, bi], 请你在数轴上选择尽量少的点, 使得每个区间内至少包含一个选出的点.

输出选择的点的最小数量

位于区间端点上的点也算作区间内.

输入格式

第一行包含整数N, 表示区间数

接下来N行, 每行包含两个整数ai, bi, 表示一个区间的两个端点

输出格式

输出一个整数, 表示所需的点的最小数量.

数据范围

1 <= N <= 105

-109 <= ai <= bi <= 109

输入样例

3
-1 1
2 4
3 5

输出样例

2

思路: 

贪心的规律就是: 每次只看眼前的选择, 选择一个局部最优解, 最后再得到全局的最优解

  1. 本题将每个区间按右端点从小到大排序.

  2. 从前往后依次枚举每个区间, 分情况讨论: 

    如果当前区间中已经包含点, 则直接pass

    否则, 选择当前区间的右端点

    最优解是当前所有方案里的最小值.

代码:

代码来自acwing y总, 加上了注释, 方便复习

#include <iostream>
#include <algorithm>
//贪心-区间选点
using namespace std;

const int N = 100010; // 最多有10w个区间

int n;

struct Range//定义一个结构体,表示一个区间
{
    int l, r;//每个区间的左端点和右端点
    //bool operator<(const node &x) const {} 是一个结构内嵌比较函数, {}内进行运算符"<"的重载
    bool operator<(const Range &W) const//由于要排序, 重载一下小于号
    {
        return r < W.r;//重载小于号<, r < W.r表示按照右端点r从小到大排序
    }
} range[N]; //定义的结构体就是range[N]

int main() {
    scanf("%d", &n);//输入区间的个数
    // 读入n个区间的端点
    for (int i = 0; i < n; i++) {
        int l, r;//读取的左右端点
        scanf("%d%d", &l, &r);//读取输入
        range[i] = {l, r};//读取左右端点, 放到定义过的结构体
        //也可以直接写: scanf("%d%d", &range[i].l, &range[i].r);
        // range[i].l表示 输入的第i+1个区间的左端点,range[i].r表示右端点
    }

    // 给所有区间进行排序
    sort(range, range + n);
    // res表示当前选择的点的数量,ed表示上一个点的下标
    int res = 0, ed = -2e9;//一开始一个点都没选, 所以ed先赋成负无穷
    //枚举所有的区间
    for (int i = 0; i < n; i++)
        if (range[i].l > ed) // 如果当前区间的左端点 严格>ed(上一个点)
        {
            res++;//点数更新
            ed = range[i].r;//当前选择点的下标更新成当前区间的右端点
        }

    printf("%d\n", res);//最后把结果返回

    return 0;
}

代码中的结构内嵌比较函数 bool operator<(const node &x) const {}, 我也是这里头一次见, 可能说的有不清楚的, 引一个讲的比较清楚的链接:

结构体内嵌比较函数bool operator < (const node &x) const {}__Phoebe__的博客-CSDN博客_bool operator<https://blog.csdn.net/li5566123/article/details/123611568

908.最大不相交区间数量

题目: 

给定N个闭区间[ai, bi], 请你在数轴上选择若干区间, 使得选中的区间之间互不相交(包括端点).

输出可选取区间的最大数量.

输入格式

第一行包含整数N, 表示区间数,

接下来N行, 每行包含两个整数ai, bi, 表示一个区间的两个端点

输出格式

输出一个整数, 表示可选取区间的最大数量.

数据范围

1 <= N <= 105

-109 <= ai <= bi <= 109

输入样例

3
-1 1
2 4
3 5

输出样例

2

思路:

这个区间不相交的问题也可以对应实际问题中的时间, 比如选时间不重合的课.

  1. 将每个区间按右端点从小到大排序.

  2. 从前往后依次枚举每个区间, 分情况讨论:

    如果当前区间中已经包含点, 则直接pass

    否则, 选择当前区间的右端点

    最后累加的端点数量之和就是最大的不相交的区间的数量.

证明:

这个结果是可以证明的: 假设最优解是ans个不相交区间, 以上思路选出区间数为cnt个, 证明ans = count:

1. 证明ans>=count

按照上面选出的cnt个区间是没有交集的, 顺序的, 一种可行方案

而答案是所有可行方案里的最大值, 所以最优解ans >= count

2. 证明ans<=count

按照以上思路同时会选择出来count个点, 每个区间都至少包含一个选择的点,

假设 ans> count, 那么可以选择出来比count更多个不相交的区间, 最多有 ans 个两两不交的区间,至少需要 ans 个点才能将这些区间全部覆盖,

然而实际上只需要 cnt 个点就能覆盖全部区间。则假设不成立, ans <= cnt

3. 证得 ans = count

为什么最大不相交区间数==最少覆盖区间点数呢?

(这一问来自:[区间问题] 最大不相交区间数量(区间问题+贪心)_Ypuyu的博客)

  • 因为如果几个区间能被同一个点覆盖,说明他们相交了

  • 所以有几个点就是有几个不相交区间。两个问题的本质是等价的

代码:

来自acwing 我添加了注释

#include <iostream>
#include <algorithm>
//贪心-908.最大不相交区间数量
using namespace std;

const int N = 100010; // 最多有10w个区间

int n;

struct Range//定义一个结构体,表示一个区间
{
    int l, r;//每个区间的左端点和右端点
    //bool operator<(const node &x) const {} 是一个结构内嵌比较函数, {}内进行运算符"<"的重载
    bool operator<(const Range &W) const//由于要排序, 重载一下小于号
    {
        return r < W.r;//重载小于号<, r < W.r表示按照右端点r从小到大排序
    }
} range[N]; //定义的结构体就是range[N]

int main()
{
    scanf("%d", &n);//输入区间的个数
    // 读入n个区间的端点
    for (int i = 0; i < n; i++) {
        int l, r;//读取的左右端点
        scanf("%d%d", &l, &r);//读取输入
        range[i] = {l, r};//读取左右端点, 放到定义过的结构体
        //也可以直接写: scanf("%d%d", &range[i].l, &range[i].r);
        // range[i].l表示 输入的第i+1个区间的左端点,range[i].r表示右端点
    }

    // 给所有区间进行排序
    sort(range, range + n);
    // res表示当前选择的点的数量,ed表示上一个点的下标
    int res = 0, ed = -2e9;//一开始一个点都没选, 所以ed先赋成负无穷
    //枚举所有的区间
    for (int i = 0; i < n; i++)
        if (range[i].l > ed) // 如果当前区间的左端点 严格>ed(上一个点)
        {
            res++;//点数更新
            ed = range[i].r;//当前选择点的下标更新成当前区间的右端点
        }

    printf("%d\n", res);//最后把结果返回

    return 0;
}

906.区间分组

题目: 

给定N个闭区间[ai, bi], 请你将这些区间分成若干组, 使得每组内部的区间两两之间(包括端点)没有交集, 并使得组数尽可能小.

输出最小组数.

输入格式

第一行包含整数N, 表示区间数,

接下来N行, 每行包含两个整数ai, bi, 表示一个区间的两个端点

输出格式

输出一个整数, 表示最小组数.

数据范围

1 <= N <= 105

-109 <= ai <= bi <= 109

输入样例

3
-1 1
2 4
3 5

输出样例

2

思路:

看样例, [-1,1] [2,4]一组, [3,5]一组, 结果是2组

  1. 将所有区间按左端点从小到大排序

  2. 从前往后处理每个区间, 判断:

    判断能否将其放到某个现有的组中:

    比较 L[i] (新的区间的左端点) 和 Max_r (存的某一组的右端点的最大值)

    1. 如果L[i] > Max_r, 说明不存在这样的组可以放入, 则开新组, 然后将其放进去

    2. 如果L[i] <= Max_r, 说明存在这样的组可以放入, 区间和原有的组是有交集的, 将其放到这个组, 并更新当前组的Max_r (如果有多个组满足条件, 就随便挑一个放进去, 但事实上应该不可能, 如果多个组满足条件, 说明那两个组应该合并)

证明:

ans是结果, 即所有cnt中的最小组数, 所有合法方案中的最小值

cnt是按照以上的算法得到的组的数量, 也就是其中一种合法的方案。

  1. 证明ans <= cnt

    因为cnt是一种合法的方案的组数, ans是所有合法方案中的最小值, 所以ans <= cnt成立

  2. 证明ans >= cnt

    举例子: 一个特殊时刻, 比如我们开第cnt个组时, 需要先看前面的cnt-1个组, 发现当前区间和前面的cnt-1个组都有交集, 因此我们需要新开一个组, 将当前区间放进去, 新开的组就是第cnt个组.

    设置当前区间的左端点是L[i], 因为当前区间和前cnt-1个组都有交集, 有L[i]点相交, 因此算上当前区间, 就可以找到cnt个区间包含L[i]点.

    那么这cnt个区间有公共点L[i], 无论怎么分, 这cnt个区间都不会分到一组中, 因此所有可行方案组的数量是>= cnt, ans >= cnt 成立

  1. 证得 ans == cnt

这道题对于数据结构上的选择也要考虑,用最小堆,也就是优先队列来存储每一个组的最右端点。

代码

代码来自acwing 还有链接:ACWing算法基础课-区间分组_weixin_44086832的博客-CSDN博客_acwing区间分组https://blog.csdn.net/weixin_44086832/article/details/122959920?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%8C%BA%E9%97%B4%E5%88%86%E7%BB%84&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-2-122959920.nonecase&spm=1018.2226.3001.4187

 我加了下注释~

#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010;

int n;

struct Range {//定义一个结构体,表示一个区间
    int l, r;//每个区间的左端点和右端点
    //重载小于号
    bool operator<(const Range &W) const {
        return l < W.l;//按照左端点从小到大排序
    }

} range[N];

int main() {
    scanf("%d", &n);//输入区间的个数

    for (int i = 1; i < n; i++)//读取n个区间的左右端点
    {
        int l, r;
        scanf("%d%d", &l, &r);//读取左右端点
        range[i] = {l, r};//赋值给range区间
    }

    // 给n个区间进行排序
    sort(range, range + n);

    // 使用小根堆维护所有组的右端点的最大值 Max_r,
    priority_queue<int, vector<int>, greater<int> > heap;
    // 依次处理每一个区间
    for (int i = 0; i < n; i++)
    {
        auto r = range[i];//取出区间, 用r表示
        // 如果小根堆为空(一个组都没有), 或者最小的右端点(堆顶)仍然比当前区间的左端点大(没任何交点)
        if (heap.empty() || heap.top() >= r.l) {//空代表没有组
            // 创建新组, 区间右端点就是新组的Max_r, 存入
            heap.push(r.r);
        } else {
            //否则的话, 当前区间可以加入这一组, 当前区间的r就成为这一组最右端点,更新

            heap.pop();//删掉原最小值
            heap.push(r.r);//
        }
    }

    printf("%d\n", heap.size());//堆里的端点个数就是组数

    return 0;
}

907.区间覆盖

题目:

给定N个闭区间[ai, bi], 以及一个线段区间[s,t], 请你选择尽量少的区间, 将指定线段区间完全覆盖.

输出最小区间数, 如果无法完全覆盖则输出-1.

输入格式

第一行包含两个整数s和t, 表示给定线段区间的两个端点.

第二行包含整数N, 表示给定区间数.

接下来N行, 每行包含两个整数ai, bi, 表示一个区间的两个端点.

输出格式

输出一个整数, 表示所需最少区间数

如果无解, 则输出-1

数据范围

1 <= N <= 105

-109 <= ai <= bi <= 109

-109 <= s <= t <= 109

输入样例

1 5 //目标区间 [s,t]
3 //区间数3
-1 3 //区间端点
2 4 
3 5

输出样例

2

思路:

思路来自acwing~ 

  1. 先将所有区间按照左端点从小到大排序 struct Range

  2. 从前往后依次枚举每个区间, 选择能够覆盖左端点 s 的区间中, 右端点最大的一个

    然后将 s 更新成右端点的最大值, 继续向后

证明做法的正确性:

证明:

ans是最后的结果, 即最优解, cnt是记录的一组合理的区间的数量

  1. 证明Ans <= cnt

    ans是最优解, 是所有可行解中最小的, cnt是一种可行解, 所以ans <= cnt 成立

  2. 证明ans >= cnt

    反证法, 假设存在一个方案 ans < cnt, 那么对比两个方案找到第一个选择不同的区间, 说明cnt方案的某个选中区间是比ans的同个左端点的选定区间是短的, 说明不存在, 因为都是选一个右端点长的区间. 所以不成立, ans >= cnt.

  3. 以上可证得ans = cnt

    或者也可以说: cnt可以逐个区间去替换为ans内的区间, 直接证得ans = cnt

    (但我感觉这里不需要任何证明的, 按照思路求得的结果本来就是最小值)

代码:

代码来自acwing y总, 和博客:

ACWing算法基础课-区间覆盖_weixin_44086832的博客-CSDN博客_acwing区间覆盖icon-default.png?t=M85Bhttps://blog.csdn.net/weixin_44086832/article/details/122959965?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%8C%BA%E9%97%B4%E8%A6%86%E7%9B%96&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-4-122959965.nonecase&spm=1018.2226.3001.4187我自己添加的注释

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;

struct Range {
    int l, r;

    bool operator<(const Range &W) const {
        return l < W.l;
    }
} range[N];

int main() {
    int st, ed;//目标区间的起止点
    scanf("%d%d", &st, &ed);//输入目标区间端点
    scanf("%d", &n);//输入区间数量

    for (int i = 0; i < n; i++)//枚举每个区间
    {
        int l, r;
        scanf("%d%d", &l, &r);//输入区间的左右端点
        range[i] = {l, r};//赋给range
    }
    //给所有区间排序
    sort(range, range + n);

    //res记录选择的区间数量
    int res = 0;
    //success标记是否成功得到一个方案
    bool success = false;
    //从前往后依次考虑每个区间 一共n个区间
    for (int i = 0; i < n; i++) {
        //双指针算法扫描一边
        int j = i, r = -2e9;//r记录当前start的最大右端点
        //用j扫描 比较,选择右端点更大的区间
        while (j < n && range[j].l <= st)//range[j].l <= st 表示起始点在st左边的线段
        {
            r = max(r, range[j].r);//选更靠右的
            j++;//继续向后, 直到扫描全部
        }

        // 如果最大右端点小于st, 所有区间都在目标区间的左侧, 则无解
        if (r <= st) {
            res = -1;
            break;
        }
        //每次跳出一次while循环, 代表选中了一个区间, 就 res++
        res++;

        //如果这个选中的区间的右端点直接比目标区间还靠右, 直接全部覆盖目标区间了, 就完成了
        if (r >= ed) {
            success = true;
            break;
        }

        // 更新st, 每次选中一个区间, 把目标区间新起点更新为上一个选中的区间的右端点
        st = r;
        i = j - 1;//更新i, 因为j指针扫描的都是原起始点左边的, 之后换了起始点就不必再扫描了直接跳过
    }

    //判断有没有成功找到方案
    if (!success) res = -1;

    printf("%d\n", res);//输出结果

    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_65293439/article/details/128414653