华为OD机试 - 找出两个整数数组中同时出现的整数(Java 2025 A卷 100分)

在这里插入图片描述

华为OD机试 2024E卷题库疯狂收录中,刷题点这里

专栏导读

本专栏收录于《华为OD机试(JAVA)真题(E卷+D卷+A卷+B卷+C卷)》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。

一、题目描述

现在有两个整数数组,需要你找出两个

<think>我们已经引用了多个来源(引用[1]到[5]),这些来源都描述了同一个问题:计算最少面官数。问题要求是,给定每个面的开始时间S_i和结束时间E_i(S_i< E_i),以及每个面官最多能面的人次m。面是一对一的,且同一时间一个面官只能面一个人。面官完成一次面后可以立即进行下一场,但总人次不超过m。关键点:1.面时间段可能有重叠,所以需要多个面官。2.每个面官最多面m人次(注意:是场次,因为每场面一个人,所以场次即人次)。3.我们要求的是最少的面官数量。根据引用[4]中的C++代码,以及引用[1]到[5]中的描述,我们可以总结出解题思路:步骤:1.读取输入:m(每个面官最多面人次),n(面场次),然后n行,每行两个整数S_i和E_i(开始和结束时间)。2.将面场次按照结束时间排序(为什么要按结束时间?因为这样可以贪心地安排面官,结束时间早的面先安排)。3.维护一个数据结构(如最小堆)来记录每个面官的最后一次面的结束时间(或者每个面官已经安排的面结束时间列表),这样我们可以快速找到最早空闲的面官。4.遍历每一场面(按结束时间升序):对于当前面[s,e],我们尝在已有的面官中找一个空闲的(即该面官的最后一次面的结束时间<=当前面的开始时间s)。如果找到,就将当前面配给这个面官,并更新该面官的最后结束时间为当前面的结束时间e(并且计数+1)。如果没有找到空闲的面官,就需要新增一个面官,并将当前面配给他(结束时间为e)。5.在配完所有面后,我们还需要考虑每个面官的面场次不能超过m次。但是注意,在配过程中,我们只考虑了时间重叠,并没有考虑场次限制。因此,我们在配结束后,对于每个面官,如果他面了k场,那么我们需要将其拆成ceil(k/m)个面官(因为一个面官最多只能面m场)。然而,注意引用[4]的代码:它最后对每个面官的任务队列的大小k,计算ceil(k/m),然后累加。但是,这样处理可能导致结果偏大,因为实际上一个面官在不超过m场的情况下,可以连续面多场,而上述拆没有考虑是否可以重新组合?所以我们需要重新审视。但是,注意问题:面场次是连续的,面官在完成一场后可以立即开始下一场(只要时间不重叠)。因此,每个面官可以连续面多场,但不超过m场。而且,时间不重叠的场次可以配给同一个面官。因此,实际上我们的第一步配(按时间配面官)已经保证了时间不重叠的场次被同一个面官处理(但这里同一个面官处理的场次可能超过m,所以在最后需要拆)。引用[4]的代码步骤:(1)按结束时间排序(2)使用一个优先队列的vector(qvec),每个元素是一个优先队列(记录该面官已经安排的面的结束时间,按最大堆?因为priority_queue<int>默认是大顶堆,但这里需要最早结束的面官,所以代码中实际上不是这样用的,而是用每个优先队列的top?这里代码有误?)实际上,引用[4]的代码中,qvec的每个元素是一个优先队列(最大堆),记录该面官所有面的结束时间?但是后面在判断时,用qvec[j].top()(即该面官最后结束的时间)和当前面开始时间比较。这样正确吗?但重新析:对于每个面官,我们需要知道他最早空闲的时间?然而代码中使用了优先队列(最大堆)来存储该面官的结束时间,但是我们要找空闲时间(即结束时间最早)的面官,用最大堆并不方便(因为最大堆只能得到最大的结束时间)。所以这个设计可能有问题。再看代码中循环:for(inti=0;i<vec.size();i++){intflag=1;for(intj=0;j<qvec.size();j++){if(qvec[j].empty()||qvec[j].top()<=vec[i][0]){qvec[j].push(vec[i][1]);flag=0;break;}}if(flag==1){//新增面官priority_queue<int>q;q.push(vec[i][1]);qvec.push_back(q);}}这里,每个面官用一个优先队列(大顶堆)存储他所面的所有结束时间?然后判断条件是:该面官的最后一次面的结束时间(即最大结束时间)<=当前面的开始时间。这实际上是错误的,因为即使该面官之前面的结束时间都很早,但最后一次可能很晚,而中间可能有空闲时间,这样判断会错过空闲的面官。正确的做法应该是:我们关心每个面官的最后一次面的结束时间(因为面官在完成最后一次面后就可以立即开始新的面)。因此,我们应该记录每个面官的最后一次面的结束时间。然后,为了快速找到最早空闲的面官,我们可以用一个最小堆(优先级队列)来存储所有面官的最后一次面的结束时间。修正后的算法(经典方法):1.将面按开始时间排序(也可以按结束时间?经典活动安排问题中按结束时间排序,然后贪心配资源)?但是这里我们要求面官数量,经典方法是使用最小堆来维护面官的结束时间。2.具体步骤(参考会议室问题II):-按开始时间从小到大排序。-使用一个最小堆(存储面官的结束时间,堆顶是结束时间最小的面官,即最早空闲的)。-初始时,没有面官。-对于每个面[s,e]:如果堆为空,或者堆顶的结束时间>s(即当前面开始时,所有面官都还在忙),那么需要新面官,将e加入堆。否则,从堆中弹出堆顶(该面官的最后结束时间更新为e,因为他可以接着面这场),然后压入新的结束时间e(相当于该面官又面了一场,结束时间变成e)。-注意:这样处理之后,我们得到了每个面官面的场次(但还没有考虑m的限制)?不对,上述过程只计算了面官的数量(不考虑每人最多m场),因为一个面官可以连续面多场(只要时间不重叠)。但是,我们的问题还有一个限制:每个面官最多只能面m人次(场次)。所以,即使一个面官可以连续面多场(时间不重叠),但最多只能面m场。因此,我们在得到每个面官面的场次数后,需要按m拆吗?注意:不能简单拆,因为同一个面官的场次是连续的,而拆官会导致增加面官数量,而且同一个面官的多场面必须是时间连续的(下一场开始时间大于等于上一场的结束时间),所以我们在配时不能打乱场次顺序(即一个面官的场次时间不重叠且顺序安排)。然而,我们最初配面官的时候,只考虑了时间重叠(即一个面官不能同时进行两场),没有考虑场次上限m。所以我们需要在配后,对每个面官,如果场次超过m,就拆成多个面官(每个最多m场)。但是,拆后,这些场次的时间仍然要求不重叠,而由于原来同一个面官的场次时间是不重叠的,所以拆后也不会重叠(因为拆只是将一个面官的场次给多个面官,而场次的时间顺序不变)。因此,我们只需要在最后计算时,将每个面官的场次数k,按照ceil(k/m)来累加得到总的面官数。但是,注意:在面安排中,一个面官的场次必须是连续的(即下一场开始时间>=上一场的结束时间),我们在配的时候已经保证了这一点(因为我们在配时,是给空闲的面配,即当前面的开始时间>=该面官上一次的结束时间)。所以每个面官的场次时间不重叠且连续,因此满足连续面的条件。然而,我们最初配面官的时候,并没有限制同一个面官只能有m场,所以一个面官可能被配了超过m场(在拆开之前)。但最后我们通过拆,将超过m的部给新的面官(同一个面官不能超过m场,所以需要多个面官来担他的场次)。拆的方法:比如一个面官面了k场,那么我们只需要ceil(k/m)个面官来承担他的k场面(而且这些场次时间原本就不重叠,因此拆之后仍然可以满足时间不重叠,因为拆只是把连续的k场给ceil(k/m)个面官,每个面官负责连续的m场或少于m场)。但是,拆后,这些场次的时间安排是否仍然不重叠?注意:拆后,每个面官负责的场次是原来场次的连续子集(比如前m场给第一个面官,接下来的m场给第二个面官,以此类推)。由于原来这个面官的场次是连续的(即上一场的结束时间<=下一场的开始时间),所以拆后的每个面官的场次也是连续的,因此时间不重叠。因此,总面官数就是每个面官拆后的面官数之和。但是,这里存在一个问题:在配面官的时候,我们并不考虑场次限制,所以可能会导致一个面官被配了过多场次。这样拆是否最优?有没有可能避免拆导致的总人数增加?实际上,面官数量由两个因素决定:时间重叠(需要同时工作的面官数量)和每人场次限制(m)。因此,最终面官总数至少是这两个因数的最大值?我们无法避免拆,因为每人的场次限制是硬性的。但是,如果我们能够在配面官的时候就考虑场次限制,可能会减少总人数吗?注意:场次限制是总场次限制,而不是同一时间段内的场次限制。所以,如果我们已经知道了每个面官最多只能安排m场,那么在配过程中,当一个面官安排满了m场后,就不能再给他配了,需要给他“封顶”,然后新开面官。然而,这种方法可能很复杂,因为我们既要考虑时间重叠,又要考虑已经安排的场次。引用[4]中的代码采用了先配再拆的方式,而且代码最后将每个面官(按配时的组)的场次数按m进行拆(向上取整),然后累加。但注意,引用[4]中的代码在配时并没有记录场次的时间顺序,它只是记录了一个结束时间的堆(而且记录方式可能有误,前面我们指出来了)。因此,我们重新考虑:先按时间安排面官(得到每个面官负责的场次数,这里我们并不立即考虑场次上限),然后再根据场次上限m来拆。这样得到的面官总数会不会大于实际需要?考虑一下:拆后,同一个面官负责的连续m场,其实可以看作是一个虚拟的面官(在时间上连续,所以时间不重叠)。因此,这样拆不会影响时间上的安排(即不会增加时间冲突)。但是,这种拆可能会增加面官数量,因为一个原本安排了k场(k>m)的面官需要被拆成ceil(k/m)个。然而,我们没有办法避免这种拆,因为m的限制。但是,我们是否可以在配面官的时候就考虑m的限制?也就是说,一个面官最多只能安排m场,一旦满了就不能再安排。同时,我们记录每个面官已经安排的场次。这样,在安排新的面时,我们不仅要考虑时间空闲,还要考虑该面官是否还有名额(场次)。这样,当我们需要安排新的面时,我们在寻找空闲面官的条件上再加一个:该面官安排的场次还没有达到m。具体步骤(配阶段就考虑场次限制):1.将面按结束时间排序(为什么按结束时间?因为这样可以使面早结束的尽早释放面官。但是按开始时间也可以,我们需要重新考虑)。2.维护一个最小堆(面官),每个元素是(该面官的最后结束时间,已经安排的场次)。注意:我们这里关心的是面官的最后结束时间(以判断是否有空闲),以及场次(以判断是否还能安排)。3.遍历排序后的面(每个面[s,e]):首先,将所有最后结束时间<=s的面官都标记为可用(因为面官已经空闲)?但是注意,我们还要考虑场次限制:如果一个面官已经安排了m场,那么即使他空闲了,也不能再安排(因为他封顶了)。所以,我们可以维护两个堆:堆1(空闲堆):存储(最后结束时间)的堆(最小堆),表示空闲的面官(注意,这个堆中的面官都是场次未满的,而且已经空闲)。堆2(忙碌堆):存储(结束时间,面官编号)的最小堆,用于在面开始时间变化时释放面官(将他们加入堆1)。但是这样比较复杂。另一种方法(参考会议室的贪心):我们只维护一个忙碌堆(存储面官的结束时间和已安排场次),但是当有新的面开始时,我们先将所有结束时间<=当前开始时间的面官弹出(释放),然后这些面官是否就空闲了?但是注意,即使他们空闲,如果他们已经安排了m场,那么就不能再用了;如果未满,就可以加入空闲列表(最小堆,这里只需要结束时间?但是其实我们只关心有没有空闲,因为我们已经知道结束时间都<=s了,所以释放后的面官都是空闲的)。但是,我们还需要记录每个面官的已安排场次。具体:1.按开始时间排序面。2.维护一个最小堆(忙碌堆),按结束时间排序,元素为(结束时间,面官ID,已安排场次)?其实我们不需要ID,只需要结束时间和已安排场次。我们并不需要面官ID。3.另外,维护一个空闲列表(队列或者堆),存储面官(记录已安排场次)。注意:空闲面官没有结束时间,因为他们空闲,所以结束时间已经不重要了(他们空闲的时间就是当前时间)?但是当我们需要配面时,我们并不关心他们的结束时间(因为他们都空闲,都可以配),我们只关心他们的已安排场次(是否已满)。4.但是,空闲列表中可能有多个面官,我们优先选择已安排场次少的(这样能尽量平衡)?或者任意一个?因为我们需要最少的面官数量,所以我们希望尽量利用还没有满的面官(如果两个都没满,选谁都可以,因为最后都是要拆的?)。实际上,我们只关心面官是否空闲且未满。然而,引用[1]到[5]中,引用[4]的代码采用了先配再拆的方式,且不考虑场次限制在配过程中的影响。而最后再拆,可能会使得面官数量偏大?为什么?因为拆后,同一个面官连续面的多场被拆成多个面官,但是这些面官实际上还是连续的(时间不重叠),所以不会增加同时工作的面官数量,但是会增加总的面官数(因为一个人变成多个人)。所以这样计算出来的面官总数是满足要求的。但是,有没有可能通过不拆来减少面官总数?例如,原本一个面官面了k场(k>m),我们拆成ceil(k/m)个,那么他的面任务被给ceil(k/m)个面官。这ceil(k/m)个面官在时间上必须连续吗?注意,这些面是连续的(结束时间递增),所以拆后,每个面官负责连续的一段,因此不会有时间重叠。所以总面官数增加了。但是,如果我们不拆,而是在配的过程中避免一个面官超过m场,我们可以在配的时候,当面官满了m场就将他移除(不再使用),这样可以保证面官数量最少吗?实际上,两种方法都是正确的,但是先配再拆的方法比较简单,而且从引用[4]的代码和用例来看,它是通过了测的。因此,我们可以采用引用[4]的代码思路,但是我们需要修正其配算法(因为原代码用最大堆存储结束时间,而且按最大结束时间判断空闲,这是错误的)。修正引用[4]的算法:我们改成使用经典的最小堆方法(按结束时间排序),在配面官时不考虑场次限制,最后再按每个面官的场次拆成ceil(k/m)。步骤:1.将面场次按结束时间排序(注意:经典活动安排问题通常按结束时间排序,这样贪心可以最小化资源)?但是这里我们要求面官数量,使用最小堆维护面官的最后结束时间。2.初始化一个最小堆(用来存储每个面官的最后结束时间)。3.按开始时间重新排序(或者按结束时间排序?)注意:我们需要保证在配时,当前面开始时间大于等于面官的最后结束时间。为了最小化面官数量,经典方法是按开始时间排序,然后用最小堆存储结束时间。这里我们按开始时间排序。4.具体:sort(interviewsbystart_time)//按开始时间升序min_heap=[]#存储面官的最后结束时间foreachinterview[s,e]:ifmin_heapisnotemptyandmin_heap[0]<=s:#当前最早结束的面官已经空闲,可以复用heapq.heappop(min_heap)#弹出该面官(因为要给他配新面,结束时间会变)heapq.heappush(min_heap,e)#更新结束时间为当前面结束时间else:#没有面官空闲,需要新增heapq.heappush(min_heap,e)#此时堆的大小是初始面官数量(不考虑场次限制m的情况下)#但是每个面官负责的场次数我们不知道!因为我们没有记录每个面官负责了几场。所以,这种方法我们只知道面官的数量(堆的大小),但是不知道每个面官负责的场次数。因此无法按m进行拆。因此,我们需要知道每个面官负责的场次数。所以我们在堆中存储的元素就不能仅仅是一个结束时间,还要有场次数。同时,当一个面官被复用时,我们将其场次数加1。这样在最后,每个面官的场次数我们就知道了,然后计算ceil(k/m)之和。步骤:sort(interviewsbystart_time)min_heap=[]#每个元素是(结束时间,场次)?按结束时间排序的最小堆#但是当我们复用面官时,我们只能从堆中弹出结束时间最小的那个,然后将该面官更新结束时间(场次+1),再入堆。#但是注意:我们不需要记录每个人的场次?因为我们最后要计算ceil(k/m)之和,所以我们需要知道每个面官的总场次数。然而,堆中是动态变化的,同一个面官被复用多次(多次弹出和入堆),我们如何跟踪每个面官的场次?为了解决这个问题,我们可以在堆外部记录每个面官的场次(比如用一个数组),但是这样比较复杂。或者,我们不用堆,而是用另一种方法:模拟面配过程,记录每个面官的场次(用列表)。另一种方法(模拟):interviews按开始时间排序。面官列表:每个面官记录(最后结束时间,场次)for[s,e]ininterviews:在已有的面官列表中,查找一个最后结束时间<=s且场次<m的面官(如果有多个,我们可以选择结束时间最小的?或者任意选一个?为了最优,应该选择结束时间最小的?)如果找到,更新该面官:最后结束时间设为e,场次+1。如果没有找到:再尝找结束时间<=s但场次已经=m的面官?不能,因为达到m就不能用了。然后尝找结束时间>s的面官?也不能,因为他们在s时刻还没结束。因此,只能新增一个面官,最后结束时间设为e,场次=1。最后,面官列表的长度就是不考虑场次限制的吗?不对,因为我们新增面官的时候只考虑了场次不够的情况。然而,这种方法有一个问题:在新增面官时,我们只考虑已有面官中没有一个可以安排当前面(时间冲突或者场次已满)。但是,即使有面官场次已满,我们也不能再用;而即使有面官空闲(结束时间<=s)但场次已满,我们也不能用。所以我们需要新增。但是,这种方法是否最优?其实这里我们考虑了场次限制,所以最后我们不需要再拆,因为每个面官的场次都不超过m。而总面官数就是最后列表的长度。那这样会不会比先配再拆的数量少?因为先配再拆的方法,同一个面官可能被配多场,然后拆,可能会增加人数。比如:有5场面,一个面官连续面(时间不重叠)5场,那么如果m=2,按照拆需要ceil(5/2)=3个面官。而如果我们在配的时候就考虑m的限制,我们在第一个面官安排了2场后,他满了,那么后面3场需要新的面官,那么总共需要3个面官(第一个面官安排2场,第二个面官安排2场,第三个安排1场)?但是,这个例子中两种方法都是3个。再考虑一个例子:面安排(按开始时间排):[1,3],[2,4],[3,5],[4,6]m=2方法1(先配不考虑场次限制,然后拆):配:只有一个面官(因为面时间重叠,不能由同一个面官连续面?)不对,时间重叠:[1,3]和[2,4]重叠,所以需要至少2个面官(同时)。然后[3,5]可以和[1,3]这个面官(结束时间3<=3,所以可以安排给第一个面官)?这样第一个面官:[1,3]和[3,5],第二个面官:[2,4]和[4,6]。配后,每个面官2场,如果m=2,那么不需要拆,总面官数=2。但是,如果m=1,那么每个面官只能面1场,所以要拆,第一个面官的两场要两个官(负责[1,3]和[3,5]),第二个面官的两场也要两个官,共4个。方法2(配时考虑场次限制):当m=2时:面[1,3]:配面官1->(结束时间3,场次1)[2,4]:面官1没结束(3>2?但是2<3,所以面官1在时间2还没结束,所以不能配。需要新增面官2->(结束时间4,场次1)[3,5]:此时面官1已经结束(3<=3),所以可以复用,更新面官1为(5,2)[4,6]:面官2已经结束(4<=4),所以可以复用,更新面官2为(6,2)总面官2,因为m=2,每个面官都安排2场(未超过),所以不需要拆,总数=2。如果m=1:面[1,3]:面官1(结束时间3,1)(满)面[2,4]:面官1没结束,新增面官2(4,1)(满)面[3,5]:面官1结束(3<=3),但是他已经满(1>=1),所以不能复用,新增面官3(5,1)[4,6]:面官2结束(4<=4),但是满,新增面官4(6,1)总数4。所以两种方法在两种情况下结果一致。但是,如果面场次的时间安排允许一个面官连续面多场(即时间不重叠),而我们有同时的需求(时间重叠时用多个面官),两种方法都是正确的。现在,我们采用方法2(配时考虑场次限制),因为它直观且符合逻辑。具体算法(配时考虑场次限制和空闲):输入:m,面场次列表(每个是(s,e))步骤:1.将面场次按开始时间排序。2.维护一个最小堆(用于释放面官),元素为(结束时间,面官索引)[也可以不存储索引,但是我们需要知道每个面官的场次]。3.另外,维护一个空闲列表(队列)的面官,他们场次未满,且当前空闲(但是空闲列表中的面官,他们的结束时间都是小于等于当前面开始时间的,所以当我们按开始时间遍历时,可以在堆释放中获取)。4.但是为了方便,我们可以统一使用一个忙碌堆,然后按时间释放:即当我们处理一个面时,先将忙碌堆中所有结束时间<=当前面开始时间的面官弹出,放入一个备选池(这些面官现在是空闲的)。5.在备选池中,我们选择其中一个面官(选择任意一个,因为我们只关心他是否空闲且未满;或者选择结束时间最早的?其实任意选都可以,因为我们不关心结束时间了,他们已经空闲了)。6.但是备选池中的面官可能有场次已满的,所以我们在备选池中寻找场次未满的面官(如果备选池中有多个,可以任意选择一个)。7.如果找到,则将该面官从备选池中移除,更新其结束时间为当前面的结束时间,场次+1,然后放入忙碌堆。8.如果没有找到(说明备选池中没有空闲且未满的面官),则新增一个面官(结束时间=当前面的结束时间,场次=1),加入忙碌堆。9.注意:备选池中的面官都是空闲的(结束时间<=当前开始时间),但是可能场次已满,不能再用。然而,我们还需要考虑:一个面官场次已满后,就不再加入备选池(即使他被释放了)?在释放时,我们只释放时间,不管场次,所以释放的面官(忙碌堆中弹出)自然加入备选池。但是,备选池中的面官,我们使用的时候要检查场次,如果场次已满,则跳过。实现:interviews.sort(key=lambdax:x[0])#按开始时间升序#忙碌堆:最小堆,元素为(end_time,id,count)但是我们不关心id,所以可以存储(end_time,count)?但是弹出时,我们需要知道释放的面官场次是多少?所以我们存储(end_time,count)#但是,注意:我们如何唯一标识一个面官?我们不需要唯一标识,因为我们只需要知道释放的面官场次是多少。busy_heap=[]#按结束时间排序的最小堆,元素为(结束时间,count)free_pool=[]#备选池:存储面官,这里我们只关心他们的场次?但是不需要其他信息,因为我们已经知道他们空闲了,我们只需要count。所以我们只记录(count,...)吗?但我们要更新为新的结束时间和count+1,所以需要知道原来的count?但是释放时,我们只能从busy_heap得到(end_time,count)这样的元素,然后释放后,我们把它加入free_pool,free_pool中的元素如何记录count?其实,free_pool可以是一个列表,存储的元素是(count,end_time)?但我们只需要count(判断场次),和end_time无关(因为已经空闲了,end_time<=当前时间)。但是,当我们复用这个面官时,我们要设置新的结束时间(当前面的结束时间)和场次+1。所以我们需要记录场次。所以,free_pool中存储面官的场次?不对,我们还需要记录什么?其实,当我们从busy_heap中弹出时,我们得到一个面官的状态:结束时间和场次。如果场次小于m,我们就把他加入free_pool(实际加入的是场次);如果大于等于m,就丢弃?(不对,不可能大于m,因为每次加1,最大等于m,达到m后我们不再使用,所以释放后即使空闲也不再加入free_pool?但是,释放的面官(从busy_heap弹出)如果场次小于m,我们加入free_pool(表示空闲且未满),否则就不加入。但free_pool只需要记录数量?不,我们需要记录的是面官个体(每个面官有自己的场次)。所以,当我们从busy_heap中弹出一个元素(一个面官的状态:结束时间和场次),如果场次小于m,那么这个面官就进入free_pool,否则我们将其删除(不再使用)。free_pool中可以是一个队列(先进先出)?因为我们只需要从中取一个。因此,free_pool就是一个队列,存储的是面官的场次(整数)。当我们从busy_heap中弹出(end_time,count)时,如果count<m,我们就将count加入free_pool(这样free_pool中存储的都是场次未满的面官的场次数)。然后,当我们需要配面官时,我们从free_pool中取一个(任意取,比如队首),然后该面官将更新:场次=count+1,结束时间=当前面结束时间,然后加入busy_heap。但是,面官的唯一性?这里,我们只关心场次,不关心面官个体。实际上,我们不需要唯一标识,因为每个面官用他的场次状态和结束时间来代表。所以,我们可以将面官视为无状态的,只需要场次计数。然而,当一个面官被复用时,我们只知道他之前的场次(count),然后更新结束时间,场次+1,再入busy_heap。但是,free_pool中取出的场次count,和busy_heap中存储的场次,实际上是同一个面官的连续状态。但是,这种方法能保证正确吗?我们并不记录每个面官的历史状态,只是用free_pool存储了可用面官的场次,然后更新时用该场次+1,以及新的结束时间。注意:这里我们丢失了面官个体,所以如果一个面官被多次复用,我们只记得他上一次的状态?这样是可以的,因为我们每次都是弹出然后更新再入堆。具体步骤:free_pool=deque()#存储等待中的面官的场次(count)busy_heap=[]#最小堆,元素为(结束时间,count)注意:count不能超过m-1(因为达到m的面官不会被加入free_pool,所以busy_heap中不会有count>=m的面官?不对,因为我们在free_pool中存储的是count,然后复用后会更新为count+1,所以如果count+1>=m,下一次释放后就不会再进free_pool)interviews=sorted(interviews,key=lambdax:x[0])fors,eininterviews:#释放:将忙碌堆中所有结束时间<=s的面官弹出whilebusy_heapandbusy_heap[0][0]<=s:end_time,count=heapq.heappop(busy_heap)ifcount< m:#将该面官加入free_pool(记录当前场次count)free_pool.append(count)iffree_pool:#有可用面官:从free_pool中取出一个,然后更新count=free_pool.popleft()#更新:将该面官加入忙碌堆,结束时间=e,场次=count+1heapq.heappush(busy_heap,(e,count+1))else:#没有可用面官,新增一个面官(初始场次=1)heapq.heappush(busy_heap,(e,1))#最后,我们需要将忙碌堆中所有面官弹出,但是注意:在遍历结束后,还在忙碌堆中的面官,我们也要释放?但问题是要计算面官总数(包括所有使用过的)。注意,在配过程中,每新增一个面官(进入busy_heap),并且最后我们只需要知道总共有多少个面官(即busy_heap中元素个数+free_pool中元素个数)吗?不,我们只关心有多少个面官被使用过?但是free_pool中的面官是曾经忙碌过现在空闲的,而busy_heap中的是忙碌的,所以两者都是使用过的。#总面官数:就是整个过程中我们创建了多少个面官?我们没有一个单独的计数。#实际上,每一个面官要么在busy_heap中,要么在free_pool中,或者在之前因为满员被丢弃了(count>=m,从busy_heap弹出时没有加入free_pool)。所以,我们的统计应该是在程序中维护一个面官总数?或者,总面官数等于:释放后进入free_pool的面官数(然后被复用)+忙碌堆中的面官数+在释放时被丢弃的面官数?不,我们在复用面官时,free_pool中减少,但busy_heap中增加,所以同时存在于两个池子是不可能的。#但要注意:一个面官从busy_heap弹出后,如果count<m,我们会加入free_pool,否则丢弃。而free_pool中的面官在后续的面中会被复用(更新后加入busy_heap),然后再被释放(可能再次加入free_pool或被丢弃)。所以,面官是重用的,我们不会在程序中额外记录创建了多少个。#所以,总面官数,就是过程中出现的最大free_pool.size()+busy_heap.size()?不对,因为面官在这两个池子间转移。我们发现,其实无法通过最后的状态得到总面官数,因为面官是重用的。#因此,我们需要在过程中计数:每当我们新增一个面官(即free_pool为空,只能新增时),我们就将总面官数+1。注意:从free_pool中复用的面官是已有的,不会增加总数。#修改算法:free_pool=deque()busy_heap=[]interviews=sorted(interviews,key=lambdax:x[0])total=0#总面官数fors,eininterviews:#释放阶段whilebusy_heapandbusy_heap[0][0]<=s:item=heapq.heappop(busy_heap)end_time,count=itemifcount< m:free_pool.append(count)iffree_pool:count=free_pool.popleft()heapq.heappush(busy_heap,(e,count+1))else:total+=1heapq.heappush(busy_heap,(e,1))#注意:最后,我们还需要处理busy_heap中剩余的面官?但是total已经统计了所有新增的面官,所以不需要额外处理。#最后,total就是答案。验证用例:用例:m=2,面场次:[[1,2],[2,3],[3,4],[4,5]]按开始时间排序:[[1,2],[2,3],[3,4],[4,5]]total=0,free_pool=[],busy_heap=[]面[1,2]:释放阶段:busy_heap为空,跳过。free_pool为空->新增,total=1,push(2,1)->busy_heap=[(2,1)]面[2,3]:释放阶段:检查busy_heap[0]=(2,1)->2<=2,弹出,count=1<2->加入free_pool:free_pool=[1]free_pool不为空:弹出count=1,更新为(3,2)push->busy_heap=[(3,2)](最小堆)面[3,4]:释放阶段:busy_heap[0]=(3,1)?不,现在busy_heap只有一个面官,结束时间3>3?等于3,所以弹出:3<=3->弹出,count=2(不小于m=2),所以不加入free_pool(直接丢弃)。现在free_pool=[],所以新增,total=2,push(4,1)->busy_heap=[(4,1)]面[4,5]:释放阶段:busy_heap[0]=(4,1)->4<=4,弹出,count=1<2->加入free_pool:free_pool=[1]free_pool不为空,弹出count=1,更新为(5,2)push->busy_heap=[(5,2)]最后,total=2。但是,这个面序列没有时间重叠,所以理论上一个面官就够了(然后拆成ceil(4/2)=2个)?但是,根据题目,面官在面完上一场后可以立即面下一场。所以在配时,wecanusethesameinterviewerforconsecutiveinterviews.However,ouralgorithmproduced2interviewers,buttheoptimalintermsofminimizingthetotalnumberofinterviewersisceil(4/2)=2.所以结果正确。再验证引用[2]中的用例:输入:m=2,n=5,面时间:[1,2],[2,3],[3,4],[4,5],[5,6]输出:3interviews=sortedbystart_time:[[1,2],[2,3],[3,4],[4,5],[5,6]]配过程:[1,2]:新增total=1,busy_heap=[(2,1)][2,3]:释放:2<=2,弹出(2,1)加入free_pool->free_pool=[1];free_pool弹出1->更新push(3,2)->busy_heap=[(3,2)][3,4]:释放:busy_heap[0]=(3,2)->3<=3?等于,弹出->count=2>=2?等于,不加入free_pool。free_pool为空->新增total=2,push(4,1)->busy_heap=[(4,1)][4,5]:释放:busy_heap[0]=(4,1)->4<=4,弹出,count=1<2,加入free_pool=[1]free_pool弹出1->更新push(5,2)->busy_heap=[(5,2)][5,6]:释放:busy_heap[0]=(5,2)->5<=5,弹出,count=2>=2,不加入free_pool.free_pool为空,新增total=3,push(6,1)->busy_heap=[(6,1)]total=3,正确。用例2:m=3,n=3,interviews=[[1,2],[2,3],[3,4]][1,2]:total=1,busy_heap=[(2,1)][2,3]:释放:弹出(2,1)->count=1<3->free_pool=[1]free_pool非空->弹出1->push(3,2)->busy_heap=[(3,2)][3,4]:释放:弹出(3,2)->count=2<3->free_pool=[2]free_pool非空->弹出2->push(4,3)->busy_heap=[(4,3)]total=1用例3:m=3,n=3,interviews=[[5,10],[8,35],[1,3]]按开始时间排序:[[1,3],[5,10],[8,35]][1,3]:total=1,busy_heap=[(3,1)][5,10]:释放:busy_heap[0]=(3)<=5,弹出,count=1<3->free_pool=[1]复用:free_pool.pop->1,push(10,2)->busy_heap=[(10,2)][8,35]:释放:busy_heap[0]=(10,2)->10>8,不释放。free_pool=[]->新增total=2,push(35,1)输出total=2.所以,算法正确。总结算法:publicstaticintsolve(intm,int[][]interviews){if(interviews==null||interviews.length==0)return0;//按开始时间排序Arrays.sort(interviews,(a,b)->a[0]-b[0]);//忙碌堆:最小堆,按结束时间排序。元素是int[],{end_time,count}PriorityQueue<int[]>busyHeap=newPriorityQueue<>((a,b)->a[0]-b[0]);//free_pool:可用面官的场次(count)队列Queue<Integer>freePool=newLinkedList<>();inttotal=0;for(int[]inter:interviews){ints=inter[0],e=inter[1];//释放while(!busyHeap.isEmpty()&&busyHeap.peek()[0]<=s){intcount=busyHeap.poll()[1];if(count< m){freePool.offer(count);}}if(!freePool.isEmpty()){intc=freePool.poll();busyHeap.offer(newint[]{e,c+1});}else{total++;busyHeap.offer(newint[]{e,1});}}returntotal;}然而,这里有一个潜在问题:在释放阶段,我们可能会弹出多个面官。其中,可能包含同一个面官吗?不会,因为我们每次push进busy_heap的都是一个独立的面官(的状态)。但是,上述算法会漏掉一种情况:当多个面官在面开始前都已经结束,但我们在释放阶段只弹出一个(堆顶)?不对,我们用一个while循环,所以会弹出所有结束时间<=s的面官。因此,该算法正确。但是,引用[4]的代码最后用了一个res累加ceil(k/m),而我们的算法直接返回total(面官总数)而且每个面官的场次都不超过m(所以在过程中面官不会被拆)。所以我们的算法和引用[4]的方法不同,但都是正确的。另外,注意:我们的算法在面结束后,busy_heap中还有一些面官,我们在循环中只处理了释放面官到free_pool,但最后统计了total(total是累计的新增面官数)。在最后的面之后,我们不再需要释放,而且不再需要配,所以total就是所需面官总数。因此,我们推荐使用这种方法(配时考虑场次限制)。但是,华为OD真题中,引用[4]的代码可能也是正确的,因为它通过了用例。所以我们也可以提供引用[4]的方法,ifyouareinterested.由于用户要求华为OD的真题思路,我们这里两种方法都可以介绍,但以方法2(即上述实时释放+复用+场次限制)为主。注意:引用[4]的代码中,最后是采用将每个面官(按配时的组)的场次数k,ceil(k/m),然后求和。但是itsallocationphaseisbasedonend_timesortedandusingavectorofpriorityqueues.However,thatallocationmaybeincorrect(asdiscussed).Sowerecommendtheabovemethod.最终选择:由于引用[4]的代码通过了用例,所以它的方法在华为OD中是被接受的。但我们也要指出,itsallocationphasemightnotbethe最优andtheremightbeabettersolution(liketheoneabove).根据用户要求,我们给出解题思路:1.贪心算法:按开始时间排序,使用一个小顶堆(优先队列)来维护面官的结束时间和已面场次。2.对于每一场面,先释放堆中所有结束时间早于等于当前面开始时间的面官(释放后如果该面官面场次未达到m,则加入空闲池)。3.从空闲池中取出一个面官(若存在),将其面场次+1,然后更新其结束时间为当前面结束时间,并放入堆中;若空闲池为空,则新增一名面官。代码实现(Java):importjava.util.*;publicclassMain{publicstaticvoidmain(String[]args){Scannersc=newScanner(System.in);intm=sc.nextInt();intn=sc.nextInt();int[][]interviews=newint[n][2];for(inti=0;i< n;i++){interviews[i][0]=sc.nextInt();interviews[i][1]=sc.nextInt();}System.out.println(minInterviewers(m,interviews));}publicstaticintminInterviewers(intm,int[][]interviews){if(interviews==null||interviews.length==0)return0;Arrays.sort(interviews,(a,b)->a[0]-b[0]);//按开始时间升序//忙碌堆:小顶堆,存储[结束时间,面场次]PriorityQueue<int[]>busyHeap=newPriorityQueue<>((a,b)->a[0]-b[0]);//空闲池:队列,存储面官已有的面场次(这些面官当前空闲)Queue<Integer>freePool=newLinkedList<>();inttotal=0;for(int[]inter:interviews){ints=inter[0],e=inter[1];//释放:将忙碌堆中所有end_time<=s的面官弹出while(!busyHeap.isEmpty()&&busyHeap.peek()[0]<=s){intcount=busyHeap.poll()[1];if(count< m){freePool.offer(count);}}if(!freePool.isEmpty()){intcount=freePool.poll();busyHeap.offer(newint[]{e,count+1});}else{total++;busyHeap.offer(newint[]{e,1});}}returntotal;}}注意:该代码在最后一个面结束后,没有释放,但不影响结果,因为我们只需要total计数。这个算法的时间复杂度为O(nlogn),排序O(nlogn),每个面场次进堆一次、出堆一次,每次O(logn),而free_pool的操作为O(1)。因此,我们给出这种算法思路。如果用户需要其他语言,我们可以相应调整,但思路同上。相关问题:1.如果每个面官最多面m人,但不是连续的,即可以不连续面,那么算法有何改动?2.如果面时间有重叠,但面官面人次无限制,如何求解?3.华为OD中还有哪些相似的贪心算法题目?注意:上面的问题不是用户本次询问的问题,但可以作为相关。</think> ### 华为OD:最少面官数算法思路 本问题是一个经典的贪心算法应用场景,核心是处理区间调度问题。给定多场面的开始/结束时间和每个面官最多面人次(m),要求计算最少的面官数量。面是一对一的,同一时间一个面官只能面一人,但面官完成一场后可立即开始下一场(只要时间不重叠),且每人总面场次不超过m。 #### 算法思路(贪心算法) 1. **排序预处理**: 将所有面场次按**开始时间升序**排序。这样便于按时间顺序逐一安排面,确保处理重叠时间段时的高效性。 2. **维护数据结构**: - **忙碌堆(最小堆)**:存储元素 `[结束时间, 已面场次]`,按结束时间排序(小顶堆),用于跟踪正在忙碌的面官状态。 - **空闲池(队列)**:存储面官的**当前已面场次**(小于m的场次),表示空闲且可复用的面官。 - **总面官数计数器**:初始为0,新增面官时递增。 3. **遍历每一场面**: - **释放空闲面官**: 检查忙碌堆中所有结束时间 ≤ 当前面开始时间的面官: - 若其场次 < m,弹出后加入空闲池(标记为空闲可用)。 - 若其场次 = m,弹出后直接丢弃(已达上限,不可复用)。 - **配当前面**: - **空闲池非空**:取池中任意面官复用(更新结束时间为当前面结束时间,场次+1,重新入忙碌堆)。 - **空闲池空**:新增面官(结束时间设为当前面结束时间,场次=1,入忙碌堆),总计数器+1。 4. **输出结果**: 遍历完成后,总计数器即为最少面官数。 #### 关键点说明 - **贪心策略优先级**:优先复用空闲面官(场次未满),避免不必要的新增。 - **时间复杂度**:O(n log n),源于排序和堆操作。 - **空间复杂度**:O(n),堆和队列占用空间。 - **处理重叠区间**:通过最小堆保证最早释放的面官优先被复用。 - **场次限制**:在复用和释放时动态检查场次上限(m),确保要求。 #### 示例验证 1. **输入**:`m=2`, 面场次:`[1,2], [2,3], [3,4], [4,5]` **输出**:2(复用1个面官至场次上限后,需新增1个)。 **过程**: - `[1,2]` → 新增1号面官(忙碌堆:`(2,1)`, 总数=1)。 - `[2,3]` → 释放1号(场次<2,入空闲池),复用1号(更新为`(3,2)`, 入堆)。 - `[3,4]` → 释放1号(场次=2,丢弃),新增2号(堆:`(4,1)`, 总数=2)。 - `[4,5]` → 释放2号(场次<2,入池),复用2号(更新为`(5,2)`)。 2. **输入**:`m=2`, 面场次:`[1,2], [2,3], [3,4], [4,5], [5,6]` **输出**:3(3场连续复用后需拆解,共需3人)[^2]。 #### 参考代码(Java) ```java import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int m = sc.nextInt(); int n = sc.nextInt(); int[][] interviews = new int[n][2]; for (int i = 0; i < n; i++) { interviews[i][0] = sc.nextInt(); interviews[i][1] = sc.nextInt(); } System.out.println(minInterviewers(m, interviews)); } public static int minInterviewers(int m, int[][] interviews) { Arrays.sort(interviews, (a, b) -> a[0] - b[0]); // 按开始时间排序 PriorityQueue<int[]> busyHeap = new PriorityQueue<>((a, b) -> a[0] - b[0]); // 忙碌堆: [结束时间, 场次] Queue<Integer> freePool = new LinkedList<>(); // 空闲池: 存储可用场次 int total = 0; for (int[] inter : interviews) { int s = inter[0], e = inter[1]; // 释放空闲面官 while (!busyHeap.isEmpty() && busyHeap.peek()[0] <= s) { int count = busyHeap.poll()[1]; if (count < m) freePool.offer(count); } // 配当前面 if (!freePool.isEmpty()) { int count = freePool.poll(); busyHeap.offer(new int[]{e, count + 1}); } else { total++; busyHeap.offer(new int[]{e, 1}); } } return total; } } ``` #### 相关拓展问题 1. **如何优化以处理更大规模的数据?** 可引入并发处理或段调度策略,但核心仍是贪心算法的高效实现[^4]。 2. **若不限制面官场次数(仅限同时段一人),算法如何简化?** 转化为**会议室安排问题**,直接使用最小堆跟踪结束时间,堆大小即面官数[^1][^3]。 3. **若面官可并行面多场(非一对一),如何求解?** 问题变为**最大区间重叠覆盖**,需用差数组或扫描线算法[^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值