【题解】【洛谷P1315】【贪心】【递推】——[NOIP 2011 提高组] 观光公交

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

通往洛谷的传送门

题目描述

风景迷人的小城 Y 市,拥有 n n n 个美丽的景点。由于慕名而来的游客越来越多,Y 市特意安排了一辆观光公交车,为游客提供更便捷的交通服务。观光公交车在第 0 0 0 分钟出现在 1 1 1 号景点,随后依次前往 2 , 3 , 4 , ⋯   , n 2,3,4,\cdots,n 2,3,4,,n 号景点。从第 i i i 号景点开到第 i + 1 i+1 i+1 号景点需要 D i D_i Di 分钟。任意时刻,公交车只能往前开,或在景点处等待。

设共有 m m m 个游客,每位游客需要乘车 1 1 1 次从一个景点到达另一个景点,第 i i i 位游客在 T i T_i Ti 分钟来到景点 A i A_i Ai,希望乘车前往景点 B i B_i Bi A i < B i A_i<B_i Ai<Bi)。为了使所有乘客都能顺利到达目的地,公交车在每站都必须等待需要从该景点出发的所有乘客都上车后才能出发开往下一景点。

假设乘客上下车不需要时间。一个乘客的旅行时间,等于他到达目的地的时刻减去他来到出发地的时刻。因为只有一辆观光车,有时候还要停下来等其他乘客,乘客们纷纷抱怨旅行时间太长了。于是聪明的司机 ZZ 给公交车安装了 k k k 个氮气加速器,每使用一个加速器,可以使其中一个 D i − 1 D_i-1 Di1。对于同一个 D i D_i Di 可以重复使用加速器,但是必须保证使用后 D i ≥ 0 D_i\ge0 Di0

那么 ZZ 该如何安排使用加速器,才能使所有乘客的旅行时间总和最小?

输入格式

1 1 1 行是 3 3 3 个整数 n , m , k n,m,k n,m,k,每两个整数之间用一个空格隔开。分别表示景点数、乘客数和氮气加速器个数。

2 2 2 行是 n − 1 n-1 n1 个整数,每两个整数之间用一个空格隔开,第 i i i 个数表示从第 i i i 个景点开往第 i + 1 i+1 i+1 个景点所需要的时间,即 D i D_i Di

3 3 3 行至 m + 2 m+2 m+2 行每行 3 3 3 个整数 T i , A i , B i T_i,A_i,B_i Ti,Ai,Bi,每两个整数之间用一个空格隔开。第 i + 2 i+2 i+2 行表示第 i i i 位乘客来到出发景点的时刻,出发的景点编号和到达的景点编号。

输出格式

一个整数,表示最小的总旅行时间。

输入输出样例

输入 #1

3 3 2
1 4
0 1 3
1 1 2
5 2 3

输出 #1

10

说明/提示

【输入输出样例说明】

D 2 D_2 D2 使用 2 2 2 个加速器,从 2 2 2 号景点到 3 3 3 号景点时间变为 2 2 2 分钟。

公交车在第 1 1 1 分钟从 1 1 1 号景点出发,第 2 2 2 分钟到达 2 2 2 号景点,第 5 5 5 分钟从 2 2 2 号景点出发,第 7 7 7 分钟到达 3 3 3 号景点。

1 1 1 个旅客旅行时间 7 − 0 = 7 7-0=7 70=7 分钟。

2 2 2 个旅客旅行时间 2 − 1 = 1 2-1=1 21=1 分钟。

3 3 3 个旅客旅行时间 7 − 5 = 2 7-5=2 75=2 分钟。

总时间 7 + 1 + 2 = 10 7+1+2=10 7+1+2=10 分钟。

【数据范围】

对于 10 % 10\% 10% 的数据, k = 0 k=0 k=0

对于 20 % 20\% 20% 的数据, k = 1 k=1 k=1

对于 40 % 40\% 40% 的数据, 2 ≤ n ≤ 50 2 \le n \le 50 2n50 1 ≤ m ≤ 10 3 1 \le m \le 10^3 1m103 0 ≤ k ≤ 20 0 \le k \le 20 0k20 0 ≤ D i ≤ 10 0 \le D_i \le 10 0Di10 0 ≤ T i ≤ 500 0 \le T_i \le 500 0Ti500

对于 60 % 60\% 60% 的数据, 1 ≤ n ≤ 100 1 \le n \le 100 1n100 1 ≤ m ≤ 10 3 1 \le m \le 10^3 1m103 0 ≤ k ≤ 100 0 \le k \le 100 0k100 0 ≤ D i ≤ 100 0 \le D_i \le 100 0Di100 0 ≤ T i ≤ 10 4 0 \le T_i \le 10^4 0Ti104

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 10 3 1 \le n \le 10^3 1n103 1 ≤ m ≤ 10 4 1 \le m \le 10^4 1m104 0 ≤ k ≤ 10 5 0 \le k \le 10^5 0k105 0 ≤ D i ≤ 100 0 \le D_i \le 100 0Di100 0 ≤ T i ≤ 10 5 0 \le T_i \le 10^5 0Ti105

1.思路解析

    不难看出,此题考察贪心。贪心,一般要将具体的问题情境转化为数学模型,我们首先看看要求答案等于什么,使用数学式子表达出来:
a n s = ∑ i = 1 m ( 到达 B i 的时刻 − T i ) ans=\sum_{i=1}^m(到达B_i的时刻-T_i) ans=i=1m(到达Bi的时刻Ti)
    那么显而易见,想要让ans最小,只需要让 ∑ 到达 B i 的时刻 \sum到达B_i的时刻 到达Bi的时刻最小即可。我们需要通过操作加速器达到此效果。


    先讲一个显而易见的结论,就是只有把所有使用加速器的 k k k次机会用完才可以达到最优方案。证明很简单,每一次使用加速器虽然不一定会让答案更优,但也不会让答案更差。在后续代码编写中,若发现使用加速器无法对答案产生影响,那么就直接跳出循环。这里先不多做探讨。


    最开始想到的贪心策略可能是,记录每一条路经过的人数,找出经过人数最多的路,然后直接对这条路使用一次加速器。但是这样的贪心策略是错的,因为还有公交车到达时间的限制,所以这个修改产生的影响不一定是实际上产生的影响。但是这个思路可以借鉴,需要将公交车到达时间这个限制考虑进来。


    接下来查看有什么变量因素会影响第i个人到达 B i B_i Bi的时刻,一个是汽车离开 A i A_i Ai(发车)时间(起始时间),以及 A i 到 B i A_i到B_i AiBi这一段旅程的路径长度总和 ∑ j = A i B i − 1 D j \sum_{j=A_i}^{B_i-1}D_j j=AiBi1Dj

    操作加速器,会导致上述两个因素同时改变。因此应该考虑使用至少两个数组动态地维护这两个信息。记leave[i]表示公交车离开站点i的时间。

    如何计算leave[i]?很明显,leave[i]=max(最晚到达站点i的人的到达时间,公交到达站点i的时间)。很明显,前者可以通过预处理处理出来,记为last[i]。后者需要动态调整,记为arr[i]。如何计算arr[i]呢?我们可以在数据输入完之后做一个处理。考虑递推,arr[i]应该就等于leave[i-1]+D[i-1],其中D[i]表示第 i i i段路的长度。上面的leave[i]就应该等于max(last[i],arr[i])

    上面的arrleave数组看似是相互依赖计算的,会陷入“死循环”,实际上arr[i]依赖的是leave[i-1]leave[i]依赖的是arr[i],并不会冲突。

    如此,我们就应该在计算arr[i]的同时计算出leave[i-1]。还有一个细节,arr[1]=0。这就是递推边界,可以理解为公交车在0时刻到达站点1。但是因为全局变量自动初始化为0,这里就没有初始化了。

    使用一个循环i=[1,n-1],我们不计算arr[i],而是计算arr[i+1],这样的好处是不会出现许多i-1,当然,底下给出的两段代码是等价的,你也可以使用更容易理解的下面的版本,如下:(有一些细节请读者自行注意)

// arr[1]=0是已知条件 
	for(int i=1;i<n;i++){ // 计算leave,arr数组 
		leave[i]=max(arr[i],last[i]);
		arr[i+1]=leave[i]+D[i];
	}

// arr[1]=0是已知条件 
	for(int i=2;i<=n;i++){ // 计算leave,arr数组 
		leave[i-1]=max(arr[i-1],last[i-1]);
		arr[i]=leave[i-1]+D[i-1];
	}

    至于另外一个影响因素,如果使用加速器的话是会动态变化的,即使使用前缀和也不好维护。(还是因为公交车到达时间这个限制)。接下来先思考一下如何利用“加速”,也就是说,加速的本质是什么?它会带来什么影响?

    氮气加速器每次让某个 D i D_i Di减少 1 1 1(但不能小于 0 0 0)。如果我们在第 i i i段路加速 1 1 1分钟,会发生什么?

  • a r r [ i + 1 ] arr[i+1] arr[i+1]会提早 1 1 1分钟。
  • 如果 a r r [ i + 1 ] ≥ l a s t [ i + 1 ] arr[i+1]≥last[i+1] arr[i+1]last[i+1],说明之前公交车到站后不用等人,那么这个“提早”会继续传到下一段: l e a v e [ i + 1 ] leave[i+1] leave[i+1]也提早 1 分钟,继而 a r r [ i + 2 ] arr[i+2] arr[i+2]也提早 1 1 1分钟……
  • 但如果 a r r [ i + 1 ] < l a s t [ i + 1 ] arr[i+1]<last[i+1] arr[i+1]<last[i+1],公交车到了 i + 1 i+1 i+1也要等到 l a s t [ i + 1 ] last[i+1] last[i+1]才能走,那么前面节省的 1 1 1分钟就被等待“吃掉”了,对后续所有站点都不再有影响

    换句话说,某段路加速的效益,会沿着线路一直往后传导,直到遇见第一个“车等人”的站点为止(或者一直传到终点)。

    因此,在某段路上使用一次加速器,其收益等于从该段路的下一个景点开始,直到第一个“需要等人”的站点为止,沿途所有下车人数之和。


    大体思路现在就出来了。我们来说一说相关代码细节。

    如何计算第一个“需要等人”的站点,也可以使用递推,只不过是从后往前递推,记nxt[i]表示从站点i开始第一个需要等人的站点编号,很容易想到递推公式nxt[i]=(arr[i+1]>last[i+1])?nxt[i+1]:i+1;(若当前站点的下一个站点不需要等人就继承下一个站点的nxt值,反之下一个站点就是后面第一个需要等人的站点),请读者思考为何不能从前往后递推。

    然后是计算“下车人数之和”,看到这个“和”很容易想到前缀和。令off[i]为在站点i下车的人数,在输入时就预处理了。然后在使用加速器之前做一个前缀和处理即可。

    现在还有一个问题:使用加速器之后,相应的arrleavenxt会发生改变,怎么办?很简单,每用完一次加速器重新计算即可。

    一个小细节:答案ans如何计算?先让ans减去所有 T i T_i Ti然后随着加速器的使用动态更新就行。


    接下来总结算法步骤:

  1. 读入数据,预处理lastoff数组和ans
  2. arrleave数组进行预处理,对off数组进行前缀和操作。
  3. 更新nxt数组,然后循环每一段路,找出使用一次加速器后“收益最大”的道路编号和对应收益。然后更新arrleave数组。
  4. 重复步骤 3 3 3 ,直到使用加速器不会再产生收益或 k k k 次加速器使用机会用完即可。

    读者可以先尝试据此编写代码。具体细节请看AC代码。

2.AC代码

#include<bits/stdc++.h>
using namespace std;
#define MAXN 1010
#define MAXM 10010
#define int long long 
// last[i]表示站点i最晚来的人,arr[i]表示公交车到站点i的时间
// leave[i]表示公交车离开站点i的时间(=max(last[i],arr[i])) 
int n,m,k,D[MAXM],last[MAXM],arr[MAXM],leave[MAXM],off[MAXN];
int ans,nxt[MAXN];
// off[i]表示在站点i下车的人数的前缀和
// nxt[i]表示道路i改变最远能够影响到的站点
struct node{
	int t,b,e;
}a[MAXM];
void init(){
	scanf("%lld%lld%lld",&n,&m,&k);
	for(int i=1;i<n;i++)scanf("%lld",&D[i]);
    for(int i=1;i<=m;i++){
    	scanf("%lld%lld%lld",&a[i].t,&a[i].b,&a[i].e);
    	last[a[i].b]=max(last[a[i].b],a[i].t);
    	off[a[i].e]++;ans-=a[i].t; // 先提前减去 
	}
}
void before_work(){
	// arr[1]=0是已知条件 
	for(int i=1;i<n;i++){ // 计算leave,arr数组 
		leave[i]=max(arr[i],last[i]);
		arr[i+1]=leave[i]+D[i];
	}
	for(int i=1;i<=n;i++){ // 计算初始ans并且对off进行前缀和 
		ans+=arr[i]*off[i];
		off[i]+=off[i-1];
	}
}
void work(){
	while(k--){
		nxt[n]=n;
		for(int i=n-1;i>=1;i--) // 计算nxt数组 
		    nxt[i]=(arr[i+1]>last[i+1])?nxt[i+1]:i+1;
		int p=-1,num=0; // p=-1表示目前还没有找到能产生收益的
		// p表示收益最小道路编号, 
		for(int i=1;i<n;i++){ // 使用一次加速机会 
			if(D[i]==0)continue; // 细节 
			int tmp=off[nxt[i]]-off[i];
			if(tmp>num){p=i;num=tmp;}
		}
		if(p==-1)break;
		D[p]--;
		// arr[1]=0是已知条件 
	    for(int i=p;i<n;i++){ // 计算leave,arr数组 
		    leave[i]=max(arr[i],last[i]);
		    arr[i+1]=leave[i]+D[i];
	    }
	    ans-=num;
	}
}
signed main(){
    init();
	before_work();
	work();
	printf("%lld",ans);
	return 0;
}

最后,制作不易,希望大家多多点赞收藏,关注下微信公众号,谢谢大家的关注,您的支持就是我更新的最大动力!
公众号上会及时提供信息学奥赛的相关资讯、各地科技特长生升学动态、还会提供相关比赛的备赛资料、信息学学习攻略等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝胖子教编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值