NKOJ-1887 借教室 【NOIP2012 day2】

来源:互联网 发布:0.32双丝并绕数据 编辑:程序博客网 时间:2024/05/24 06:43

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;}
原创粉丝点击