P1315 [NOIP 2011 提高组] 观光公交

题目传送门

我的博客-欢迎来访

个人认为是个好题。另外,推荐一下这篇题解,看着这篇题解学明白的。


首先,我们要求的是最小的 \(\sum\limits_{i=1}^{m}{arr_{to_i}}-tim_{i}\),其中 \(arr_i\) 表示大巴到达景点 \(i\) 的时间,\(to_i\) 表示游客 \(i\) 的目的地,\(tim_i\) 表示游客 \(i\) 的出发时间。

既然 \(tim_i\) 是固定的,所以我们要想办法让 \(\sum\limits_{i=1}^{m}{arr_{to_i}}\) 尽可能小。

如果不考虑乘客和加速器的话,就让公交正常开,那么这会是时间关于景点的函数图像:

P1315_1_1

如果加上乘客的影响的话,那么到了部分景点后车还要等乘客,图像会有部分的抬升,并形成多个断点:

(如果一个景点 \(i\) 被称作断点,那么 \(arr_i \le lst_i\),其中 \(lst_i\) 表示最晚到达该出发点的游客是几点到的)

P1315_2_1

P1315_3_1

以下称两个断点中间的区域为一个区间。

加上加速器的情况有点复杂。如果在某个位置使用了加速器的话,那么从它开始,到它所在区间的末尾,所有的点的到达时间都会-1。而后面的区间,由于制约因素是最晚的游客的到达时间,所以并不对它们起作用。

P1315_4_1

然后我们就会发现,在一个区间里,如果我们决定对它使用一个加速器,那么越早使用的话,受益的游客越多。所以对于一个区间,我们尽可能会往前选择使用加速器的边:

P1315_5_1

当然,也有一种可能,使用完加速器后出现了新的断点:

P1315_6_2

我们使用一个加速器的时候,当然是希望它造福尽可能多的游客,也就是希望 \(\sum\limits_{i=1}^{m}{arr_{to_i}}\) 减小的最多。所以我们使用一个加速器的时候,挨个考虑每个区间,取那个最合适的地方用掉就好。

但是我们如何考虑“造福多少游客”呢?我们记 \(des_i\) 表示以 \(i\) 为目的地的游客有多少。显然,如果在 \(pos\)\(pos+1\) 的边使用加速器,并且当前区间靠右的断点是 \(r\),它就能使答案减 \(\sum\limits_{i=pos+1}^{r-1}{des_i}\)

于是,我们重复这个过程 \(k\) 次,用掉 \(k\) 个加速器后统计答案即可。时间复杂度 \(O(kn)\)

代码:

P1315
#include<bits/stdc++.h>
#define int long long
using namespace std;

inline int read(){
	int x=0,f=1;char c=getchar();
	while(c<48){
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}

const int N=1e3+3;
const int M=1e4+4;
const int K=1e5+5;
int n,m,k,d[N],lst[N],des[N],to[M],tim[M],arr[N],cut[N],cnt;
//cut[i]:第i个发现的断点是几号
//cnt:cut数组的指针
//其余变量名同题解 

signed main(){
	n=read(),m=read(),k=read();
	for(int i=1;i<n;i++){
		d[i]=read();
	}
	//输入&预处理tim,lst 
	for(int i=1;i<=m;i++){
		tim[i]=read();int l=read();to[i]=read();
		lst[l]=max(lst[l],tim[i]);
		des[to[i]]++;
	}
	int TIM=0;
	//模拟法处理arr,cut 
	for(int i=1;i<=n;i++){
		arr[i]=TIM;
		TIM=max(TIM,lst[i]);
		if(arr[i]<=lst[i]){
			cut[++cnt]=i;
		}
		TIM+=d[i];
	}
	while(k--){
		int mx=-1,gai=0;
		for(int i=1;i<=cnt;i++){
			int pos=cut[i];
			while(!d[pos]){
				//注意有些路段是不可以使用加速器的,要接着往后跳 
				pos++;
				if(arr[pos]<=lst[pos]||pos>=n){
					//如果跳到断点还是找不到合适的边时,那该区间就用不了加速器了 
					pos=-20100325;//这串数字懂得都懂 
					break;
				}
			}
			if(pos==-20100325){
				continue;
			}
			//这里是在模拟 如果当前这个地方用加速器,能使答案减多少 
			int loc=pos+1,res=0;
			while(1){
				res+=des[loc];
				if(arr[loc]<=lst[loc]||loc>=n){
					break;
				}
				//警示后人:要判断当前点是否进了下一个边界,越过了就必须跳出,否则可以接着往下找。
				//例如样例里, 不判的话loc=2直接跳3,进入下一个区间了,不合规的 
				loc++;
			}
			if(res>mx){
				mx=res,gai=pos;
			}
		}
		d[gai]--;//就决定减这里了 
		//这里是在进行一个减的过程 
		while(++gai){
			arr[gai]--;
			if(arr[gai]==lst[gai]){
				//出现了新的断点 
				cut[++cnt]=gai;
			}
			if(arr[gai]<lst[gai]||gai>=n){
				//否则跳到了原有断点上 
				break;
			}
		}
	}
	//统计答案 
	int ans=0;
	for(int i=1;i<=m;i++){
		ans+=arr[to[i]]-tim[i];
	}
	printf("%lld",ans);
	return 0;
}

还有一些实现细节值得注意(或者说,我当时阅读题解的一些问题):

(以下仅为个人理解)

1.为什么断点的判定可以取等?个人认为取等的话出现新断点时,方便判断是旧断点还是新断点,并且如果它的 \(arr\) 减了 1 的话,它的制约因素是 \(lst\),还是无法造福它。

2.为什么我们找使用加速器的点时从断点右侧的边开始试,而不是断点左侧的那条边?因为你找那条边没啥用,就算用了加速器,制约因素是游客到达时间,不会造福后面的点。但是断点右侧的边可以造福后面的点。

3.有关代码里的警示后人:这个纯属我当时写的时候脑抽,我应该先判断当前这个点是不是断点再往后跳,否则如果先往后跳再判断点的话,有可能直接越过一个断点统计答案去了。

4.如果我们决定让 \(d_{pos}--\),那么造福的是 \(pos+1\) 之后的该区间的点。

如果你觉得这篇题解讲的还不错的话,不妨点点赞呀 qwq

posted @ 2025-11-05 09:28  qwqSW  阅读(10)  评论(0)    收藏  举报