区间贪心2

news2025/1/17 0:49:53

问题引入

上一节我们学习了贪心的基本概念、基本思路以及证明方法,下面我们一起来学习区间类贪心类问题,这里我们学习的重点依然是贪心的策略和证明。

  算法原理区间贪心:

是指在区间上做贪心。区间贪心一般都是按左端点或者右端点排序,大胆假设,小心求证。当你觉得是贪心题的时候,千万不要轻易相信题目给的样例,尽量多找几组例子试试,样例验证成功后最好还要证明贪心算法是否成立。

例题引入凌乱的拓拓快noip了,拓拓同学很紧张!现在各大oj上有n个比赛,每个比赛的开始、结束的时间点是知道的。拓拓同学认为,参加越多的比赛,noip就能考的越好(假的)。所以,他想知道他最多能参加几个比赛。由于拓拓同学是个蒟蒻,如果要参加一个比赛必须善始善终,而且不能同时参加2个及以上的比赛。他想知道自己最多能参加几个比赛。输入数据不超过10610 6 。【问题分析】如果所有的比赛时间不冲突,那么就可以全部参加了,但并没有这么简单。如果两个比赛时间冲突,要分情况看待,冲突的比赛关系我们分成以下两种情况讨论(其他的比赛冲突都能转化成这两种情况)。

如下图所示:

(a) 一个比赛被另一个包含:两个比赛冲突,选择比赛1,因为比赛1先结束,这样后续比赛被占用时间的可能就少一些。

(b) 一个比赛和另一个比赛相交:还是选择比赛1,理由同上。最先选择参加哪一场比赛呢?根据分析,应该选择参加最先结束的那一场比赛。接下来,要选择能够参加的比赛中,最早结束的比赛(既然已经决定参加上一场比赛,那么所有和上一场冲突的比赛都不能参加了),直到没有比赛可以参加为止。这样可以保证不管在什么时间点之前,能够参加的比赛的数量都是最多的。因此贪心算法成立。这就是证明贪心算法的另一种方法:数学归纳法。每一步的选择都是到当前为止的最优解,一直到最后一步就成为了全局最优解。代码如下: 

#include <bits/stdc++.h>
using namespace std;
int n, ans, finish;  //finish记录上一次结束的比赛时间
struct contest{
    int l, r;  //l比赛开始时间,r比赛结束时间
} c[1000010];
bool cmp(contest a, contest b){  //按照结束时间从小到大排序
    return a.r < b.r;
}
int main()
{
    cin >> n;
    for(int i=1;i<=n;i++){
        cin >>c[i].l >> c[i].r;
    }
    sort(c+1, c+n+1, cmp);
    for(int i=1;i<=n;i++){
        //保证上一次比赛结束的时间 <= 第i场比赛的开始时间
        if(finish <= c[i].l){ 
            ans++;
            finish = c[i].r; //更新比赛结束的时间
        }
    }
    cout << ans;
    return 0;
}

本题中将所有比赛的结束时间排序,然后依次进行贪心:

如果能够参加这场比赛,就报名;如果这场比赛和上一场参加的比赛冲突,就放弃。 常见的区间贪心类问题一般可以分成四种不同的题型,下面我们一起来探讨下这四种经典的区间贪心模型。

经典模型一:区间选点问题

【例题1】 给定N(N≤ 1 0 5 10 5 )个闭区间[ 𝑎 𝑖 a i​, 𝑏 𝑖 b i​](闭区间是指包含左右两个端点的区间),请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。输出选择的点的最小数量。位于区间端点上的点也算作区间内。( − 1 0 9 −10 9 ≤ 𝑎 𝑖 a i​≤ 𝑏 𝑖 b i​≤ 1 0 9 10 9 ) 【问题分析】 因为不方便展示同一数轴上的区间,所以把区间上下平移了。举例如下: 

 

上述图片是给定了8个区间,分别编号1~8,应该如何选择我们的策略呢?

以下为两个区间之间可能的3种关系。(1)如果只有两个区间3区间和4区间,我们必须选择两个点使得每个区间内至少包含一个点;(2)如果只有两个区间1区间和2区间,那么我们肯定选择在1区间和2区间重合的部分放点;(3)如果只有两个区间6和7区间,我们还是选择在两个区间重合的地方放点。那么我们是是优先选择点在区间中间位置?还是优先选择点在区间开始位置?亦或是选择点在区间结束位置?很明显,我们应该尽量选择重合区间结束的位置,因为这样才能保证选择的点也能尽量在后面的区间中。思路:按右端点从小到大排序,然后顺次遍历这些区间,每次选取当前区间的右端点,然后略过含这个右端点的区间,直到遍历到下一个不含这个点的区间,再取这个区间的右端点,以此类推。证明:假设ans是最优解(正确答案),cnt是可行解(贪心答案),显然有ans≤cnt。反证法证明ans≥cnt。我们将这cnt个点所在那些区间取出来,显然它们是不相交的(若相交,则贪心求解的是cnt-1个点),得出最多有cnt个两两不相交的区间,覆盖每个区间至少需要cnt个点。因此ans≥cnt。综上ans=cnt,证明完毕。代码如下: 

#include <bits/stdc++.h>
using namespace std;
struct Range {
    int l, r;
} range[100010];
bool cmp(Range a, Range b) {  // 按照右端点从小到大排序
    return a.r < b.r;
}
int main() {
    int n, ans = 0, ed = INT_MIN;  //ed表示上一个点的位置
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> range[i].l >> range[i].r;
    }
    sort(range, range + n, cmp);
    for (int i = 0; i < n; i++) {
        if (range[i].l > ed) {
            ed = range[i].r;  // 在当前区间的右端点放一个点
            ans ++ ;
        }
    }
    cout << ans;
}

经典模型二:最大不相交区间数量问题

【例题2】给定N个闭区间[𝑎𝑖a i​ ,𝑏𝑖b i​ ],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。输出可选取区间的最大数量。(数据范围和上一题一致)【问题分析】有以下三种策略:① 优先选择区间长度较小的?② 优先选择开始位置靠前的?③ 优先选择结束位置靠前的?大胆假设:选择右端点最靠前的区间,这样可能对后面区间的影响最小。证明:假设ans是最优解(正确的答案),表示最多不相交区间的数量;cnt是可行解,表示用贪心算法求出cnt个不相交的区间,显然有ans≥cnt。反证法证明ans≤cnt。假设ans>cnt,由最优解的含义知,最多有ans个不相交的区间,因此至少需要ans个点才能覆盖所有区间,而根据贪心算法知,只需cnt个点就能覆盖全部区间,且cnt<ans,这与上面分析“至少需要ans个点才能覆盖所有区间”相矛盾,故ans≤cnt。综上所述ans=cnt。思路:首先将每个区间按照右端点从小到大进行排序。ed表示当前放置在数轴上的点的位置,开始初始化为无穷小,表示没有放置,此时数轴上没有点之后依次遍历排序好的区间。如果区间的左端点大于当前放置点的位置,说明当前点无法覆盖区间,则把点的位置更新成该区间的右端点,表示在该区间的右端点放置一个新的点,同时更新点的个数。代码如下: 

#include <bits/stdc++.h>
using namespace std;
struct Range {
    int l, r;
} range[100010];
bool cmp(Range a, Range b) {
    return a.r < b.r;
}
int main() {
    int n, ans = 0, ed = INT_MIN;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> range[i].l >> range[i].r;
    }
    sort(range, range + n, cmp);
    for (int i = 0; i < n; i++) {
        if (range[i].l > ed) {
            ed = range[i].r;
            ans ++ ;
        }
    }
    cout << ans;
}

 我们会发现,和上一题的代码一样。为什么最大不相交区间数等同于最少区间点数呢?因为如果几个区间能被同一个点覆盖,说明他们相交了,所以有几个点就是有几个不相交区间。

经典模型三:区间分组问题

【例题3】给定N个闭区间[𝑎𝑖a i​ ,𝑏𝑖b i​ ],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。输出最小组数。(数据范围同上)【问题分析】按照之前两题的经验,我们大胆假设:右端点排序。但是你稍作思考就会想到:假设有如下图所示的4个区间,按照右端点排序是1、2、3、4,进行分组得到的结果 

 

 

因此按照右端点排序会找到一个反例,假设不成立。接下来我们考虑左端点排序行不行?思路:左端点排序,从前往后处理每个区间,判断能否将其放到某个现有的组中。如果不存在,则开新组放入,如果存在,则放进该组并更新该组的最大右端点。证明:假设ans是最优解,表示最少需要ans个组;cnt是可行解,表示算法找到cnt个组,显然有ans≤cnt。反证法证明ans≥cnt。考虑在开辟第cnt个组的过程:算法此时已经开辟了cnt−1个组,组内区间两两不相交,组间区间有交集,且当前遍历区间的左端点均小于各组所有区间中最大的右端点,即当前遍历的区间一定与各组区间相交,此时必须要开辟新的组放入新的区间,因此至少需要cnt个组,即ans≥cnt。综上所述ans=cnt。代码如下:

#include <bits/stdc++.h>
using namespace std;
struct Range {
    int l, r;
} range[100010];
int arr[100010], idx;
bool cmp(Range a, Range b) {  //按照左端点排序
    return a.l < b.l;
}
int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> range[i].l >> range[i].r;
    }
    sort(range, range + n, cmp);
    for (int i = 0; i < n; i++) { // n个区间
        bool f = 1; // 默认需要开辟新空间
        for (int j = 0; j < idx; j++) {
            if (arr[j] < range[i].l) { // 第i个区间可以放到第j组
                arr[j] = range[i].r; // 放到第j组,更新j组的右端点
                f = 0;
                break;
            }
        }
        if (f) { //f是1,需要开辟新空间
            arr[idx] = range[i].r;
            idx++;
        }
    }
    cout << idx;
}

在代码求解最小组数时用了双重for循环,时间复杂度为O(n2),提交代码可能会超时。可以用二叉堆(优先队列)优化程序,时间复杂度变为O(nlogn),由于还没讲过这种数据结构,我们还是以理解贪心的思路为主。

经典模型四:区间覆盖问题

【例题4】给定N个闭区间[𝑎𝑖a i​ ,𝑏𝑖b i​ ]以及一个线段区间[s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。输出最少区间数,如果无法完全覆盖则输出−1。【问题分析】如下图所示,我们来分析这一题的思路 

对于线段区间[s,t],最多的选法就是全部选择,得到答案5个区间。但是题目要求最少的区间数覆盖掉[s,t],所以我们可以选择[a1,b1]、[a3,b3]、[a5,b5],答案就是最少的区间数3个。为什么不选[a2,b2]呢,很明显我们选择[a1,b1]会更好。为什么不选[a4,b4]呢,很明显[a3,b3]和[a5,b5]已经覆盖掉[a4,b4]了。我们可以这样考虑求解过程,当选择了[a1,b1],此时我们将线段区间的s更新为b1;然后再去选择下一个可选的区间[a3,b3],继续将s更新为b3;这时[a4,b4]与[a5,b5]都能覆盖[s,t](的一部分),按照上述规则,我们优先选择b更靠后的区间[a5,b5],最终得到答案3个。于是我们可以大胆假设:首先将所有区间按左端点从小到大排序,之后从前往后依次枚举每个区间,在所有能覆盖start的区间中选择右端点最大的区间,并将start更新成右端点的最大值。证明思路正确性(选学):假设ans是最优解,表示最少需要ans个区间;cnt是可行解,表示算法找到cnt个区间满足覆盖,显然有ans≤cnt。采用反证法证明ans≥cnt。假设最优解由k个区间组成,各区间按右端点从小到大排序,右端点依次记为a1,a2,…,ak,显然一定有t≤ak,否则最优解没有覆盖到线段区间[s,t]的右端点t,不满足覆盖条件;同理,假设算法求得m(m>k)个区间,各区间按右端点从小到大排序,右端点依次记为b1,b2,…,bk,…,bm,显然一定有bk<t≤bm。t≤bm是为了满足覆盖条件;bk<t是因为bk不是最后一个区间的端点,如果bk≤t,则:考虑最优解和贪心算法各自获得的第1个区间的右端点a1和b1,由于贪心算法选取右端点距离起点s最大的区间,故一定有a1≤b1;贪心算法又以b1为起点,找一个右端点距离b1最大的区间,最优解选取的第2个区间的右端点a2不可能超过b2,否则存在一个区间的长度a2−a1大于b2−b1,这与贪心算法矛盾,故一定有a2≤b2;同理一定有ak≤bk,由于bk<t,故ak<t,这说明最优解没有覆盖线段区间[s,t],矛盾;故ans≥cnt。综上所述ans=cnt。代码如下: 

#include <bits/stdc++.h>
using namespace std;
struct Range {
    int l, r;
} range[100010];
bool cmp(Range a, Range b) { // 按照左端点从小到大排序
    return a.l < b.l;
}
int main() {
    int n, s, t, ans = 0;
    cin >> s >> t >> n;
    for (int i = 0; i < n; i++) {
        cin >> range[i].l >> range[i].r;
    }
    sort(range, range + n, cmp);
    for (int i = 0; i < n; i++) {  //第16行
        // maxr表示所有包含s的区间中右端点的最大值
        int maxr = INT_MIN, j;
        for (j = i; j < n && range[j].l <= s; j++) {  //第19行
            maxr = max(maxr, range[j].r);
        }
        // 中间某次覆盖不了 或者 所有区间的最右边都<t,均为无解
        if (maxr < s || (i == n - 1 && maxr < t)) {
            ans = -1;
            break;
        }
        ans++;
        if (maxr >= t) { //得到解
            break;
        }
        s = maxr; //更新区间起点为当前选择区间的右端点
        i = j - 1; //19行j++和将执行的16行i++会多加1,所以下标变为j-1
    }
    cout << ans;
}

注意理解i=j-1这一行。因为19行跳出循环的时候j++会多算一次,实际上结束19行的for循环后我们选择的是第j-1个区间,之后可以从第j个区间开始选,但是16行的i++会在i=j-1赋值后执行一遍,所以这里赋值为j-1。兵无常势,水无常形,贪心算法非常考验同学们的思维。学习贪心算法没有固定的程序模板,因此请看官们记住贪心的学习方法:大胆假设,小心求证;多练习,多思考,多总结。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1967923.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最短路问题中的朴素版Dijkstra算法

最短路问题的朴素版Dijkstra算法 题目 最短路问题需要用到下面的算法&#xff08;n代表点的数量&#xff0c;m代表边的数量&#xff09; 朴素版和堆优化版的Dijkstra算法的区别是&#xff0c;朴素版比较适合稠密图&#xff0c;堆优化版适合稀疏图&#xff0c;稠密图代表它的边…

模型优化—动量梯度下降

一、mini-batch 梯度下降&#xff08;gradient descent&#xff09;&#xff1a; SGD&#xff08;stochastic GD&#xff09;随机梯度下降&#xff1a;对一个样本做梯度下降 batch梯度下降&#xff1a;使用所有样本做梯度下降&#xff08;做一次又叫epoch&#xff09; mini…

人工智能算法工程师(高级)课程9-自然语言处理之词嵌入的介绍与代码详解

大家好,我是微学AI,今天给大家介绍一下人工智能算法工程师(高级)课程9-自然语言处理之词嵌入的介绍与代码详解。 词嵌入是一种将文本中的词语转换为数值向量的技术,广泛应用于自然语言处理领域。它通过将词语映射到多维向量空间,使得相似意义的词语在向量空间中距离较近,从…

【无标题配置jdk环境和tomcat环境

一&#xff0e;接着昨天的发布vue项目 npm run serve 构建项目 npm run build ls ls dist/ vim dist/index.html [rootweb eleme_web]# cd /usr/local/nginx/conf/ [rootweb conf]# ls 将静态的项目移动到nginx中 [rootweb nginx]# cd conf.d/ [rootweb conf.d]# ls…

用Python打造精彩动画与视频,3.3 添加音频和简单效果

3.3 添加音频和简单效果 在本节中&#xff0c;我们将学习如何使用 MoviePy 库为视频添加音频和一些简单的效果。这些操作可以让你的视频更具吸引力和个性化。 准备工作 首先&#xff0c;确保你已经安装了 MoviePy 和 pydub 库。你可以通过以下命令安装&#xff1a; pip ins…

Qt 实战(2)搭建开发环境 | 2.4、查看 Qt 源码

文章目录 一、查看 Qt 源码1、获取 Qt 源码2、添加源码路径3、配置定位器4、查看源码 前言&#xff1a; Qt 是一个跨平台的 C 图形用户界面应用程序开发框架&#xff0c;广泛应用于开发 GUI 程序以及非 GUI 程序&#xff0c;如控制台工具和服务器。查看 Qt 的源码不仅可以帮助你…

故障案例:网络访问慢

现象描述 FW作为中间设备的场景下&#xff0c;用户访问网页慢&#xff0c;报文延时大等。 相关告警与日志 相关告警 无 相关日志 ARP/4/ARP_DUPLICATE_IPADDR 原因分析 图1 网络访问慢故障定位思路 丢包 报文在网络链路上传输时&#xff0c;可能会有部分报文在链路中被丢…

用深度学习改进乳腺癌MRI诊断| 文献速递--AI辅助的放射影像疾病诊断

Title 题目 Improving breast cancer diagnostics with deep learning for MRI 用深度学习改进乳腺癌MRI诊断 01 文献速递介绍 乳腺磁共振成像&#xff08;MRI&#xff09;是一种检测乳腺癌的高度敏感的方式&#xff0c;报告的敏感性超过80%。传统上&#xff0c;其在筛查…

【算法】动态规划-斐波那契数列模型

目录 1、第N个泰波那契数 1.1 算法原理讲解 1.1.1 状态表示 1.1.2 状态转移方程 1.1.3 初始化 1.1.4 填表顺序 1.1.5 返回值 1.2 代码实现 1.3 空间优化 2、三步问题 2.1 算法原理讲解 2.1.1 状态表示 2.1.2 状态转移方程 2.1.3 初始化 2.1.4 填表顺序 2.1.5 返…

(四十一)大数据实战——spark的yarn模式生产环境部署

前言 Spark 是一个开源的分布式计算系统。它提供了高效的数据处理能力&#xff0c;支持复杂的数据分析和处理任务&#xff0c;是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。Spark Core&#xff1a;实现了Spark的基本功能&#xff0c;包含任务调度、内存管理、错误…

上线前端系统

上线一个静态的前端系统&#xff08;续&#xff09; 在eleme服务器上 启动服务 启动rpcbind [rooteleme-static ~]# systemctl restart rpcbind 启动nfs [rooteleme-static ~]# systemctl restart nfs 重启服务 启动smb [rootstatic-server img]# systemctl start smb…

SQL数据库模糊查询指定的字符的表资料(CHARINDEX)

1.目的 MSG栏位里面有很多组合内容的字符信息&#xff0c;需要进行模糊查询。 2.问题 正常使用LIKE 语句可以通用大部分的查询需求&#xff0c;但是遇到部分的特殊字符&#xff0c;例如&#xff1a;[] 资料是存在数据资料中&#xff0c;但是查询反馈的结果是没有内容&#xf…

二刷代码随想录训练营Day 16|513.找树左下角的值、112.路径总和、106.从中序与后序遍历序列构造二叉树

1.找到左下角的值 513. 找树左下角的值 - 力扣&#xff08;LeetCode&#xff09;代码随想录 (programmercarl.com) 代码&#xff1a; class Solution { public:int maxDepth INT_MIN;int result;// 深度最大&#xff0c;确保是最后一行 先遍历左孩子再遍历右孩子 确保是左下…

进程地址空间,零基础最最最详解

目录 建议全文阅读&#xff01;&#xff01;&#xff01; 建议全文阅读&#xff01;&#xff01;&#xff01; 建议全文阅读&#xff01;&#xff01;&#xff01; 一、什么是地址空间 1、概念 2、主要组成部分 3、特点和作用 &#xff08;1&#xff09;虚拟化&#xf…

Java并发—volatile关键字

在这篇文章Java并发—Java内存模型以及线程安全-CSDN博客多次提及volatile关键字&#xff0c;这是一个非常重要的概念&#xff0c;主要用于多线程编程中&#xff0c;它确保了变量的可见性和禁止指令重排序&#xff0c;但不保证原子性&#xff0c;下面详细解释volatile关键字的作…

未来3-5年,哪些工作会被AI取代

一篇由高盛经济学家约瑟夫布里格斯 &#xff08;Joseph Briggs&#xff09;和德维西科德纳尼 &#xff08;Devesh Kodnani&#xff09;撰写的报告指出&#xff0c;全球预计将有3亿个工作岗位被生成式AI取代。 报告称&#xff1a;“最近出现的生成式人工智能将降低劳动力成本和…

​宁德时代:续航还剩多少?

车企价格战打到供应商&#xff0c;连续增利不增收。 今天我们看宁德时代的增长电池续航还剩多少&#xff1f; 巨头长成&#xff0c;就要面临增长瓶颈。“宁王”24年中报公布&#xff0c;业绩喜忧参半。二季度营收869.96亿&#xff0c;同比下滑13.18%&#xff0c; 已经是宁德时…

冠军之选:奥运冠军青睐的游泳耳机款式大公开

在最新一届的夏季奥林匹克运动会中&#xff0c;泳池边的激烈竞争再次点燃了全球观众的热情。游泳运动员们&#xff0c;以惊人的速度和毅力&#xff0c;一次又一次地刷新纪录&#xff0c;向世人展示了人类极限的无限可能。而在这些运动员备战的过程中&#xff0c;有一个细节或许…

吴恩达老师机器学习-ex5

有借鉴网上部分博客 首先&#xff0c;我先使用该数据集&#xff0c;通过线性回归的方法&#xff0c;做了一个预测问题 import numpy as np import scipy.io as sio import matplotlib.pyplot as plt from scipy.optimize import minimize#读取数据 path "./ex5data1.ma…

Spine 核心功能入门

核心功能入门 本文主旨是整理我在入手学习 spine 时的流程&#xff0c;以及对于基本功能的理解和常规 2D 动画实现的思路。 意在整理出一个简要的入门 spine 的流程&#xff0c;以及对于一些高阶功能的应用的思考。 本文基于 https://zh.esotericsoftware.com/ 官网教程进行思…