P1887【NOIP2012 day2】借教室
时间限制 : 40000 MS 空间限制 :重点内容 128000 KB
问题描述
在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来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,第二行输出需要修改订单的申请人编号。
样例输入
4 3
2 5 4 3
2 1 3
3 2 4
4 2 4
样例输出
-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。
情不自禁地感叹
有些代码,你没看懂的时候以为是写代码的人脑壳有Trouble
看懂之后,就开始怀疑自己脑壳有没有Trouble了
题解
首先我们说一种新的(对我来说是新的)建树方式
已知数的左边界l,右边界r
那么对于该线段树,它的编号就是(l+r)|(l!=r)
解释
这个建树方式非常巧妙
因为我们知道,线段树的分段的区间是不停地二分的
所以任意两段不同的线段,他们的左边界和右边界之和(l+r)一定不相同
证明一下(此处线段长度>=4)
假设某线段边界(l+r)
那么它二分之后,子线段的边界之和
左线段=l+(l+r)/2 < l+r
右线段=(l+r)/2+1+r > l+r
同理易得,子线段的子线段同样满足上述规律
但是有一种情况比较特殊,就是线段和线段的中点的(l+r)是有可能相等的
那么此时,我们就需要一种区分方式
这个时候,就用到了(l+r)|(l!=r)
l==r时表示这是点
那么此时 l+r为偶数,(1!=r)==0
(l+r)|(l!=r)一定为偶数
l!=r时表示这是线段
那么此时 l+r 无论为奇数还是偶数,(l!=r)==1
(l+r)|(l!=r)一定为奇数
此时不同的线段/点的标号也一定不一样(自己参考上面的方法证明)
这样子,线段(点)的标号就可以使用 (l+r)|(l!=r)表示
那么我们要开的数组大小仅为线段长度的2倍
然后就是十分简单的线段树操作了
我们定义emp(empty的缩写)为此时的空房间的数量,lazy(代码中的wait)为滞留的操作
如何定义线段的emp呢??
因为当这一段当中有一个点的emp<0时这段线段就有房间被订满了
我们希望emp能够表示出没有房间的状态
所以我们将线段的emp定位整个线段中最小的空房间数
于是,当某一次修改(订购房间)的操作覆盖了[l,r]的区间时
由于一整段线段的值都要减去订购的房间数(暂定为a)
所以我们将[l,r]的lazy定为(a),表示减去的数量为a
同时,由于我们定义了emp为线段中最小的空房间数
所以emp-a<0时,表示空房间数最小的那一天的空房间已经被过度定购了(不满足题意的可满足解)
我们就可以快速地判断出这一段区间是否能够满足要求
另外,当订购的区间只与[l,r]有交集时,我们就选择先把lazy当中的操作下达下去,再细分线段进行操作(详见代码)
同时要注意,及时地更新大区间的emp
注意
第一点
原本的建树方式即使使用的是相同的思路和解法,也会超时
因为无论是声明结构体,还是不停地通过计算子区间序号和调用子区间的边界,在巨大的数据范围下都是十分庞大的操作
而此处的建树方法能够只通过(l+r)|(l!=r)来计算区间的序号,大大减少了调用次数
同时这种方法也更节省空间
第二点
尽量使lazy数组的操作可叠加
因为可叠加的lazy数组可以一次处理多次下达的指令,时间复杂度和运算量会大大减少(主要是时间复杂度前面的那个常数大大减小了)
第三点(总结)
线段数最大的优势在于
我们在大区间即可确定修改某一个点的值是否合法
要尽量使用这个优点,高效地进行判断
lazy数组最大地优势在于累加操作
对于整段区间的操作都可以累加
直到某个操作只对交集部分操作时才往下进行操作
在写lazy时也要尽量使用这个优势,尽量累加次数,而非简单地推迟一次操作
最后附上代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#define mid (l+r>>1)
using namespace std;
inline int input()
{
char c=getchar();int o;
while(c>57||c<48)c=getchar();
for(o=0;c>47&&c<58;c=getchar())o=(o<<1)+(o<<3)+c-48;
return o;
}
int ll,rr,ADD;
int emp[2001234],lazy[2001234];
void BT(int l,int r)
{
int ori=(l+r)|(l!=r);
if(l<r)
{
int ls=(l+mid)|(l!=mid),rs=(mid+1+r)|(mid+1!=r);
BT(l,mid);
BT(mid+1,r);
emp[ori]=min(emp[ls],emp[rs]);//更新大区间的emp
}
else emp[ori]=input();//所有的点都是从小到大依次被细分到的
}
bool add(int l,int r)
{
int ori=(l+r)|(l!=r);
bool res=1;
if(ll<=l&&r<=rr)
if(emp[ori]<ADD)return 0;//在大区间判断修改的合法性
else
{
emp[ori]-=ADD;
lazy[ori]+=ADD;//累加lazy操作
return 1;
}
//当操作涉及的区域只与[l,r]的子区间相关时
int ls=(l+mid)|(l!=mid),rs=(mid+1+r)|(mid+1!=r);
if(lazy[ori]>0)//下达lazy操作
{
emp[ls]-=lazy[ori];
lazy[ls]+=lazy[ori];
emp[rs]-=lazy[ori];
lazy[rs]+=lazy[ori];
lazy[ori]=0;
}
if(l!=r)//细分区间进行操作
{
if(ll<=mid)res=(res&add(l,mid));
if(rr>mid)res=(res&add(mid+1,r));
}
emp[ori]=min(emp[ls],emp[rs]);//更新emp的值
return res;
}
int main()
{
int n=input(),m=input();
BT(1,n);
for(int i=1;i<=m;i++)
{
ADD=input();ll=input();rr=input();
if(add(1,n)==0){printf("-1\n%d",i);return 0;}
}
printf("0");
return 0;
}