Description
我们需要处理接下来n天的借教室信息,其中第i天学校有ri个教室可供租借。共有m份订单,每份订单用三个正整数描述,分别为dj,sj,tj,表示某租借者需要从第sj天到第tj天租借教室(包括第sj天和第tj天),每天需要租借dj个教室。
我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供dj个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第sj天到第tj天中有至少一天剩余的教室数量不足dj个。
Analysis
线段树
分治专题,却看不出分治的解法。在线算法,把每天都当成一个单位,针对每次借教室的情况,修改一段区间,直到出现负区间,很明显的最小值线段树。O(mlogn)
加上快读之后只得了95分,通过查阅资料(某谷题解),我了解到了线段树标记永久化这个思想:修改后的懒惰标记不spread,直接在update时加上自己的懒惰标记,这样能节省常数时间。这个优化太正确了,我怎么没有想到呢。
Code
#include <bits/stdc++.h>
#define MAXN 1000000
int n,m,r[MAXN],num[MAXN];
int read(){
int f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
f=(ch=='-')?-1:f;
ch=getchar();
}
int get=0;
while(ch>='0'&&ch<='9'){
get=(get<<1)+(get<<3)+ch-'0';
ch=getchar();
}
return get;
}
class segment_tree{
private:
class leaf{
public:
int data,l,r,minus;
#define data(i) tree[i].data
#define l(i) tree[i].l
#define r(i) tree[i].r
#define minus(i) tree[i].minus
}tree[4*MAXN];
void update(int i){
data(i)=std::min(data(i<<1)-minus(i<<1),data(i<<1|1)-minus(i<<1|1));
}
public:
void build(int i,int l,int r,int num[]){
l(i)=l;
r(i)=r;
int mid=(l+r)/2;
if(l==r)
data(i)=num[mid];
else{
build(i<<1,l,mid,num);
build(i<<1|1,mid+1,r,num);
update(i);
}
}
int query(int i,int l,int r){
if(l(i)>r||r(i)<l)
return 0x3f3f3f3f;
if(l(i)>=l&&r(i)<=r)
return data(i)-minus(i);
return std::min(query(i<<1,l,r),query(i<<1|1,l,r))-minus(i);
}
void change(int i,int l,int r,int d){
if(l(i)>r||r(i)<l)
return;
if(l(i)>=l&&r(i)<=r){
minus(i)+=d;
return;
}
change(i<<1,l,r,d);
change(i<<1|1,l,r,d);
update(i);
}
}tree;
int main(){
freopen("classroom.in","r",stdin);
freopen("classroom.out","w",stdout);
n=read();
m=read();
for(int i=1;i<=n;i++)
r[i]=read();
tree.build(1,1,n,r);
for(int i=1;i<=m;i++){
int d,s,t;
d=read();
s=read();
t=read();
tree.change(1,s,t,d);
if(tree.query(1,1,n)<0){
std::cout<<"-1"<<std::endl;
std::cout<<i<<std::endl;
return 0;
}
}
std::cout<<"0"<<std::endl;
return 0;
}
二分查找
这题确实可以二分,离线算法。因为此订单序列越往右无效的可能性越大,一旦出现无效订单,之后订单更是无效,满足二分查找性质。但是,二分查找的复杂度为O(logn),处理的订单总数为(n/2+n/4+..+1)=O(n),那么对每个订单的处理复杂度要不大于O(logn)才行,可以想到线段树,但是那就没意思了。看了一下zjx解题报告,用的是前缀和优化,这个优化我在一本算法书上见过,具体哪本忘了。
具体优化方法:利用订单区间修改的性质,新建一个数组,以减法为例,将处理的起点附上-d,终点的下一位附上+d,这样此数组的每个点的前缀和即为每个教室的变动值,修改是O(1)的算法,而判断是处理完所有订单后的一次O(n)循环,最大复杂度为O(n+nlog2n)。
Code
#include <bits/stdc++.h>
int n,m,ans,sub,room[1000010],d[1000010],s[1000010],t[1000010],treat[1000010];
int read(){
int f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
f=(ch=='-')?-1:f;
ch=getchar();
}
int get=0;
while(ch>='0'&&ch<='9'){
get=(get<<1)+(get<<3)+ch-'0';
ch=getchar();
}
return get;
}
int search(int l,int r){
if(l==r-1)
return (l==m)?0:r;
int mid=(l+r)/2;
if(sub<mid)
for(int i=sub+1;i<=mid;i++)
treat[s[i]]-=d[i],treat[t[i]+1]+=d[i];
else
for(int i=mid+1;i<=sub;i++)
treat[s[i]]+=d[i],treat[t[i]+1]-=d[i];
sub=mid;
int sum=0;
for(int i=1;i<=n;i++){
sum+=treat[i];
if(room[i]+sum<0)
return search(l,mid);
}
return search(mid,r);
}
int main(){
freopen("classroom.in","r",stdin);
freopen("classroom.out","w",stdout);
n=read();
m=read();
for(int i=1;i<=n;i++)
room[i]=read();
for(int i=1;i<=m;i++)
d[i]=read(),s[i]=read(),t[i]=read();
int ans=search(0,m+1);
if(!ans)
std::cout<<"0"<<std::endl;
else
std::cout<<"-1"<<std::endl<<ans<<std::endl;
return 0;
}