AtCoder agc007_d

洛谷题目页面传送门 & AtCoder题目页面传送门

(1)根数轴,Shik初始在位置(0)。数轴上有(n)只小熊,第(i)只在位置(a_i)。Shik每秒可以向左移动(1)个单位长度、原地不动或向右移动(1)个单位长度。Shik第(1)次到某个小熊的位置(s)秒后,小熊会在原地生产(1)个金币,Shik必须再次到达此小熊的位置才能收集金币。求Shik收集完所有金币后到达位置(m)所花的最小秒数。

(ninleft[1,10^5 ight],a_iin(0,m),a_i<a_{i+1})

考虑最优情况下,Shik的路线会是怎样的。如果Shik经过了一些小熊,回头收集了这些小熊中后面一部分的金币,然后继续往终点走,留下前面一部分金币等到以后再收,这样肯定不是最优的(感性理解)。可以推出若一个小熊的金币被收了,那么它前面的所有小熊的金币都被收了,即被收了金币的小熊序列是所有小熊序列的一个前缀。考虑将某时刻的前缀分成若干个区间,每个区间内的小熊都是在一次回头中被收了金币的,于是我们就可以DP了。

(dp_i)表示Shik收完了前(i)个小熊的金币并回到了位置(a_i)所花的最小秒数。不妨设(a_0=0)为起点。那么显然边界是(dp_0=0),目标是(dp_n+m-a_n)。转移的话,枚举当前被收了金币的小熊前缀被划分的最后一个区间的左端点的前一个小熊(j),即最后一次回头之前([1,j])已经被收了。那么最后一次回头收的是([j+1,i])。显然,路线是这样的:先将前(j)个小熊的金币收完,回到位置(a_j),然后(a_j o a_i)经过([j+1,i])使它们生产金币,然后(a_i o a_{j+1})回头到第(j+1)个小熊,等待若干秒直到第(j+1)个小熊生产金币,然后(a_{j+1} o a_i)依次收完([j+1,i])的金币并回到位置(a_i)。那么状态转移方程就很好列了:

[dp_i=min_{j=0}^{i-1}{dp_j+(a_i-a_j)+(a_i-a_{j+1})+maxleft(0,s-2(a_i-a_{j+1}) ight)+(a_i-a_{j+1})} ]

[dp_i=min_{j=0}^{i-1}{dp_j+3a_i-a_j-2a_{j+1}+maxleft(0,s-2a_i+2a_{j+1} ight)} ]

暴力转移显然是(mathrm O!left(n^2 ight))的,于是考虑优化。注意到方程里有个(max)很不好处理,于是分类讨论,分成(s-2a_i+2a_{j+1}ge0)(s-2a_i+2a_{j+1}<0)(2)种。此时方程变为了:(化简后)

[dp_i=min!left(min_{jin[0,i),s-2a_i+2a_{j+1}ge0}{dp_j+a_i-a_j+s},min_{jin[0,i),s-2a_i+2a_{j+1}<0}{dp_j+3a_i-a_j-2a_{j+1}} ight) ]

将关于决策变量(j)的放到一起,关于状态变量(i)的和常量放到一起,得

[dp_i=min!left(min_{jin[0,i),2a_{j+1}ge 2a_i-s}{(dp_j-a_j)+(a_i+s)},min_{jin[0,i),2a_{j+1}<2a_i-s}{(dp_j-a_j-2a_{j+1})+3a_i} ight) ]

(2)(min)的条件里的(2a_{j+1})显然有单调性,所以(2)(min)取的(j)都构成区间。特殊地,对于第(2)(min),是前缀,即左端点为(0)的区间。又因为(2a_i-s)也有单调性,所以第(1)(min)的区间左端点单调递增,对于每个(i),这个左端点可以two-pointers求出。于是对于第(1)(min)维护单调队列,对于第(2)(min)维护前缀最小值,(mathrm O(n))

下面贴代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long//防爆int 
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=100000;
int n/*小熊个数*/,m/*终点*/,s/*小熊被Shik碰到至生产金币之间的秒数*/;
int a[N+1];//小熊的位置 
int dp[N+1];//dp[i]表示Shik收完了前i个小熊的金币并回到了位置a[i]所花的最小秒数 
int q[N],head,tail;//对于第1个min维护的单调队列 
signed main(){
	cin>>n>>m>>s;
	for(int i=1;i<=n;i++)cin>>a[i];
	q[tail++]=0;//i=1,j=0满足2a[j+1]>=2a[i]-t,归第1个min,于是压入单调队列 
	int now=-1/*第2个min取的j构成的区间(前缀)的右端点*/,mn=inf/*当前的前缀最小值*/;
	for(int i=1;i<=n;i++){
		while(now+1<i&&2*a[now+2]<2*a[i]-s)now++,mn=min(mn,dp[now]-a[now]-2*a[now+1]);//将now往后移 
		while(head<tail&&q[head]<=now)head++;//维护单调队列,弹出过时元素 
		while(head<tail&&dp[q[tail-1]]-a[q[tail-1]]>=dp[i-1]-a[i-1])tail--;//维护单调队列队尾严格单调递增性 
		q[tail++]=i-1;//将j=i-1入队 
		dp[i]=min(dp[q[head]]-a[q[head]]+a[i]+s,mn+3*a[i]);//状态转移方程 
	}
	cout<<dp[n]+m-a[n]<<"
";//目标 
	return 0;
}
原文地址:https://www.cnblogs.com/ycx-akioi/p/AtCoder-agc007-d.html