借教室
在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来nn天的借教室信息,其中第ii天学校有r_iri个教室可供租借。共有mm份订单,每份订单用三个正整数描述,分别为d_j,s_j,t_jdj,sj,tj,表示某租借者需要从第s_jsj天到第t_jtj天租借教室(包括第s_jsj天和第t_jtj天),每天需要租借d_jdj个教室。
我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供d_jdj个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第s_jsj天到第t_jtj天中有至少一天剩余的教室数量不足d_jdj个。
现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。
输入输出格式
输入格式:
第一行包含两个正整数n,mn,m,表示天数和订单的数量。
第二行包含nn个正整数,其中第ii个数为r_iri,表示第ii天可用于租借的教室数量。
接下来有mm行,每行包含三个正整数d_j,s_j,t_jdj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。
每行相邻的两个数之间均用一个空格隔开。天数与订单均用从11开始的整数编号。
输出格式:
如果所有订单均可满足,则输出只有一行,包含一个整数00。否则(订单无法完全满足)
输出两行,第一行输出一个负整数-1−1,第二行输出需要修改订单的申请人编号。
一、暴力简述
首先我们不难看出,这道题————并不是一道多难的题,因为显然,第一眼看题目时便很容易地想到暴力如何打:枚举每一种订单,然后针对每一种订单,对区间内的每一天进行修改(做减法),直到某一份订单使得某一天剩下的教室数量为负数,即可得出结果。
先小小的评析一下吧:凡是能打出几近正解的暴力题,都不是难题!(蒟蒻可以骗到50+的不就是水题吗qwq)但是,显然枚举形式的暴力会很慢,期望的时间复杂度约为
O(m \times n)O(m×n),
可能会更快一些(但没卵用qwq)
二、思想详述
让我们开动脑筋想一下:每张订单其实就可以看作是一个区间(操作),左右区间分别为开始时间和结束时间,所以这不就是一个区间操作吗——首选线段tree啦!但是笔者在这里并不打算介绍线段树,因为虽然线段tree操作方便、复杂度低,但是——————我不会写啊qwq!(逃
并且总感觉你考试的时候撸一个线段树模板的时间完全可以多打两个暴力啊qwq(虽然暴力也不一定对)
所以,选择引入一种好理解、好实现的算法:差分数组
在介绍差分之前,需要介绍前缀和思想
(qwq此处当然只会讲一维线性的前缀和啦)
我们有一组数(个数小于等于一千万),并且有一大堆询问——给定区间l、r,求l、r之间所有数之和(询问个数小于等于一千万)
此处暴力肯定不行啊(O(NQlength)),那么我们来观察前缀和是怎么做的:用sum[i]来存储前i个数的和,然后用sum[r]-sum[l-1]来表示l~r之间所有数的和。(l-1原因是l~r只看要包含l)而sum数组便可以通过简单的递推求出来
代码核心:
for(int i=1;i<=n;i++)
{cin>>a[i];sum[i]=sum[i-1]+a[i];}
for(int i=1;i<=q;i++)
{cin>>l>>r;cout<<sum[r]-sum[l-1]<<" ";}
而所谓的差分数组,即是前缀和数组的逆运算:
我们给定前i个数相邻两个数的差(1<=i<=n),求每一项a[i](1<=i<=n)。
此时无非就是用作差的方式求得每一项,此时我们可以有一个作差数组diff,diff[i]用于记录a[i]-a[i-1],然后对于每一项a[i],我们可以递推出来:
for(int i=1;i<=n;i++)
{cin>>diff[i];a[i]=diff[i]+a[i-1];}
for(int i=1;i<=n;i++)
{cout<<a[i];}
到这儿,我们可以看出来,前缀和是用元数据求元与元之间的并集关系,而差分则是根据元与元之间的逻辑关系求元数据,是互逆思想(qwq但是有时元数据和关系数据不是很好辨别或者产生角色反演啊)
但是,理解了前缀和&差分,并不代表肯定能做到模板题:毕竟,思想只能是辅助工具啊
三、关于答案二分
一般来说,二分是个很有用的优化途径,因为这样会直接导致减半运算,而对于能否二分,有一个界定标准:状态的决策过程或者序列是否满足单调性或者可以局部舍弃性。 而在这个题里,因为如果前一份订单都不满足,那么之后的所有订单都不用继续考虑;而如果后一份订单都满足,那么之前的所有订单一定都可以满足,符合局部舍弃性,所以可以二分订单数量。
四、终于要bb正解了!
首先,要明白如为什么要用区间差分而不是区间前缀和:因为这个题每次操作针对的对象都是原本题目中给的元数据,而不是让求某个关系,所以采用差分。
其次,要知道差分会起到怎样的作用:因为diff数组决定着每个元数据的变化大小、趋势,所以,当我们想要针对区间操作时,钱可以转化成对diff数组操作:
diff[l[i]]+=d[i];
diff[r[i]+1]-=d[i];//d[i]是指每天要借的教室数
因为后面的元数据都由之前的diff数组推导出来,所以改变diff[i]就相当于改变i之后的每一个值,并通过重新减去改变的量,达到操作区间的目的。
then,我们需要想明白策略:从第一份订单开始枚举,直到无法满足或者全枚举完结束。
最后,一点提示,我下面的标程是通过比大小来判断是否满足,而不是作差判负数————能不出负数就别出负数,否则容易基佬紫(re)/手动滑稽
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 const int maxn=1e6+7; 6 int n,m,head,tail,mid; 7 int r[maxn],d[maxn],s[maxn],t[maxn],tmp[maxn],sum[maxn]; 8 bool pan(int x){ 9 memset(tmp,0,sizeof(tmp)); 10 memset(sum,0,sizeof(sum)); 11 for(int i=1;i<=x;i++){ 12 tmp[s[i]]+=d[i];tmp[t[i]+1]-=d[i]; 13 } 14 for(int i=1;i<=n;i++){ 15 sum[i]=sum[i-1]+tmp[i]; 16 if(sum[i]>r[i]) return false; 17 } 18 return true; 19 } 20 int main(){ 21 cin>>n>>m; 22 for(int i=1;i<=n;i++) cin>>r[i]; 23 for(int i=1;i<=m;i++) cin>>d[i]>>s[i]>>t[i]; 24 if(pan(m)){ 25 cout<<0<<endl;return 0; 26 } 27 int head=1,tail=m-1; 28 while(head<=tail){ 29 int mid=(head+tail)/2; 30 if(pan(mid)) head=mid+1; 31 else tail=mid-1; 32 } 33 cout<<-1<<endl<<head<<endl; 34 return 0; 35 }