[NOIP2012 提高组] 借教室
题目描述:
在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来 n 天的借教室信息,其中第 i 天学校有 ri 个教室可供租借。共有 m 份订单,每份订单用三个正整数描述,分别为 dj,sj,tj,表示某租借者需要从第 sj 天到第 tj 天租借教室(包括第 sj 天和第 tj 天),每天需要租借 dj 个教室。
我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供 dj 个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第 sj 天到第 tj 天中有至少一天剩余的教室数量不足 dj 个。
现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。
输入格式
第一行包含两个正整数 n,m,表示天数和订单的数量。
第二行包含 n 个正整数,其中第 i 个数为 ri,表示第 i 天可用于租借的教室数量。
接下来有 m 行,每行包含三个正整数 dj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。
每行相邻的两个数之间均用一个空格隔开。天数与订单均用从 1 开始的整数编号。
输出格式
如果所有订单均可满足,则输出只有一行,包含一个整数 0。否则(订单无法完全满足)
输出两行,第一行输出一个负整数 -1,第二行输出需要修改订单的申请人编号。
输入输出样例
输入 #1:
4 3 2 5 4 3 2 1 3 3 2 4 4 2 4
输出 #1:
-1 2
说明/提示
【输入输出样例说明】
第 1份订单满足后,4天剩余的教室数分别为 0,3,2,3。第 2 份订单要求第 2天到第 4 天每天提供3个教室,而第 3 天剩余的教室数为2,因此无法满足。分配停止,通知第 2 个申请人修改订单。
【数据范围】
对于10%的数据,有1≤ n,m≤ 10;
对于30%的数据,有1≤ n,m≤1000;
对于 70%的数据,有1 ≤ n,m ≤ 10^5;
对于 100%的数据,有1≤n,m≤10^6,0≤ri,dj≤10^9,1≤sj≤tj≤n。
NOIP 2012 提高组 第二天 第二题
2022.2.20 新增一组 hack 数据
思路:
一、暴力简述
首先我们不难看出,这道题————并不是一道多难的题,因为显然,第一眼看题目时便很容易地想到暴力如何打:枚举每一种订单,然后针对每一种订单,对区间内的每一天进行修改(做减法),直到某一份订单使得某一天剩下的教室数量为负数,即可得出结果。
先小小的评析一下吧:凡是能打出几近正解的暴力题,都不是难题!(蒟蒻可以骗到50+的不就是水题吗qwq)但是,显然枚举形式的暴力会很慢,期望的时间复杂度约为
可能会更快一些(但没卵用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但是有时元数据和关系数据不是很好辨别或者产生角色反演啊)
但是,理解了前缀和&差分,并不代表肯定能做到模板题:毕竟,思想只能是辅助工具啊
三、关于二分答案
一般来说,二分是个很有用的优化途径,因为这样会直接导致减半运算,而对于能否二分,有一个界定标准:状态的决策过程或者序列是否满足单调性或者可以局部舍弃性。 而在这个题里,因为如果前一份订单都不满足,那么之后的所有订单都不用继续考虑;而如果后一份订单都满足,那么之前的所有订单一定都可以满足,符合局部舍弃性,所以可以二分订单数量。
四、正解!
首先,要明白如为什么要用区间差分而不是区间前缀和:因为这个题每次操作针对的对象都是原本题目中给的元数据,而不是让求某个关系,所以采用差分。
其次,要知道差分会起到怎样的作用:因为diff数组决定着每个元数据的变化大小、趋势,所以,当我们想要针对区间操作时,钱可以转化成对diff数组操作:
diff[l[i]]+=d[i];
diff[r[i]+1]-=d[i];//d[i]是指每天要借的教室数
因为后面的元数据都由之前的diff数组推导出来,所以改变diff[i]就相当于改变i之后的每一个值,并通过重新减去改变的量,达到操作区间的目的。
then,我们需要想明白策略:从第一份订单开始枚举,直到无法满足或者全枚举完结束。
最后,一点提示,我下面的标程是通过比大小来判断是否满足,而不是作差判负数————能不出负数就别出负数。
完整代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1000011;
ll r[N],sum[N],ID=0,d[N],s[N],t[N],a[N];
ll n,m;
bool pd(ll mid){
memset(sum,0,sizeof(sum));
for(ll i=1;i<=mid;i++){
sum[s[i]]+=d[i];
sum[t[i]+1]-=d[i];
}
for(ll i=1;i<=n;i++){
a[i]=a[i-1]+sum[i];
if(a[i]>r[i])
return false;
}
return true;
}
int main(){
scanf("%d%d",&n,&m);
bool f=true;
for(ll i=1;i<=n;i++)
cin>>r[i];
for(ll i=1;i<=m;i++)
cin>>d[i]>>s[i]>>t[i];
ll x=1,y=m;
if(pd(m)){
cout<<"0"<<'\n';
exit(0);
}
else{
while(x<y){
ll mid=(x+y)/2;
if(pd(mid))
x=mid+1;
else
y=mid;
}
}
cout<<"-1"<<'\n'<<x<<'\n';
return 0;
}
总结:
在这道题里面,我们看到了差分及前缀和的应用,既然差分与前缀和都行,那么我们可以热烈欢迎线段树(树状貌似也还行?)做法……
题目链接:
[NOIP2012 提高组] 借教室 - 洛谷https://www.luogu.com.cn/problem/P1083#sub