[題解]P1083 [NOIP2012 提高組] 借教室

Sinktank發表於2024-07-04

[題解]P1083 [NOIP2012 提高組] 借教室

解法\(1\):線段樹 - \(O((n+m)\log n)\)

比較直觀的一種做法,但是可能需要卡一下輸入(這裡沒卡也過了,但要注意輸入是\(10^6\)級的,為了保險一定要加)。

#include<bits/stdc++.h>
#define lc (x<<1)
#define rc ((x<<1)|1)
#define int long long
using namespace std;
int n,m,a[1000010],minn[4000010],tag[4000010];
void update(int x){
	minn[x]=min(minn[lc],minn[rc]);
}
void ch(int x,int v){
	tag[x]+=v;
	minn[x]+=v;
}
void pushdown(int x){
	if(tag[x]){
		ch(lc,tag[x]);
		ch(rc,tag[x]);
		tag[x]=0;
	}
}
void build(int l,int r,int x){
	if(l==r){
		minn[x]=a[l];
		return;
	}
	int mid=(l+r)>>1; 
	build(l,mid,lc);
	build(mid+1,r,rc);
	update(x);
}
void modify(int a,int b,int v,int l,int r,int x){
	pushdown(x);
	if(a<=l&&r<=b){
		ch(x,v);
		return;
	}
	int mid=(l+r)>>1;
	if(a<=mid) modify(a,b,v,l,mid,lc);
	if(b>mid) modify(a,b,v,mid+1,r,rc);
	update(x);
}
int query(int a,int b,int l,int r,int x){
	pushdown(x);
	if(a<=l&&r<=b) return minn[x];
	int mid=(l+r)>>1,ans=LLONG_MAX;
	if(a<=mid) ans=min(ans,query(a,b,l,mid,lc));
	if(b>mid) ans=min(ans,query(a,b,mid+1,r,rc));
	return ans;
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	build(1,n,1);
	for(int i=1;i<=m;i++){
		int v,l,r;
		cin>>v>>l>>r;
		modify(l,r,-v,1,n,1);
		if(query(1,n,1,n,1)<0){
			cout<<"-1\n"<<i<<"\n";
			return 0;
		}
	}
	cout<<"0\n";
	return 0;
}

解法\(2\):二分答案 - \(O((n+m)\log m)\)

二分列舉申請的編號,對於列舉出的編號。透過差分,計算出處理完當前編號後,每一天剩餘的教室數量。如果最終結果存在負數,則r=mid,否則l=mid+1。因為我們要找的是使存在負數的最小編號。

#include<bits/stdc++.h>
#define int long long
#define N 1000010
#define M 1000010 
using namespace std;
int n,m,a[N],b[N],tb[N],c[M],ld[M],rd[M];
bool check(int d){
	for(int i=1;i<=n;i++) tb[i]=b[i];
	for(int i=1;i<=d;i++) tb[ld[i]]-=c[i],tb[rd[i]+1]+=c[i];
	for(int i=1;i<=n;i++) tb[i]+=tb[i-1];
	for(int i=1;i<=n;i++) if(tb[i]<0) return 1;
	return 0;
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		b[i]=a[i]-a[i-1];
	}
	for(int i=1;i<=m;i++) cin>>c[i]>>ld[i]>>rd[i];
	int l=1,r=m;
	bool flag=0;
	while(l<r){
		int mid=(l+r)>>1;
		if(check(mid)){//不合法 
			flag=1;
			r=mid;
		}else{
			l=mid+1;
		}
	}
	if(!flag) cout<<"0\n";
	else cout<<"-1\n"<<l<<"\n";
	return 0;
}

解法\(3\) - \(O(n+m)\)

總體思路就是用一個指標\(j\),初始為\(m\)。對於每一天,判斷完成操作\(1\sim j\)後是否仍然合法。如果不合法了,就把\(j\)往前挪,同時撤回操作,直到該天合法為止。

最後,如果\(j=m\),則說明所有操作都是合法的,輸出0

否則,我們需要輸出導致不合法的第一個操作。而\(j\)表示的是合法的最後一個操作,輸出\(j+1\)即可。

此演算法的優勢在於,列舉的每一天不需要從最後開始移動指標,而是從上一天的位置開始移動,大大減少了冗餘操作。

注意到\(j\)是隻減不增的,所以while迴圈一共最多執行\(m\)次。而for一共執行\(n\)次,所以複雜度是\(O(n+m)\)

#include<bits/stdc++.h>
#define N 1000010
#define M 1000010
#define int long long
using namespace std;
int n,m,a[N],c[M],l[M],r[M];
int cf[N];
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=m;i++){
		cin>>c[i]>>l[i]>>r[i];
		cf[l[i]]+=c[i],cf[r[i]+1]-=c[i];
	}
	int j=m,sum=0;
	//sum表示完成操作1~j後,第i天一共用去多少教室
	for(int i=1;i<=n;i++){
		sum+=cf[i];//cf[i]表示完成操作j之後,第i天一共用去多少個教室
		while(sum>a[i]){//用去的>已有的,不合法,需要撤回操作
			cf[l[j]]-=c[j],cf[r[j]+1]+=c[j];
			if(l[j]<=i&&i<=r[j]) sum-=c[j];
			j--;
		}
	}
	if(j==m) cout<<"0\n";
	else cout<<"-1\n"<<j+1<<"\n";
	return 0;
}

相關文章