JOISC2017 手持ち花火 (Sparklers) 题解

题目链接: Atcoder ; LOJ ; UOJ

题目大意:有 \(N\) 人站在一条数轴上。他们人手一个烟花,每人手中的烟花都恰好能燃烧 \(T\) 秒。每个烟花只能被点燃一次。

1 号站在原点,\(i (1 \leq i \leq N)\) 号到 \(1\) 号的距离为 \(X_i\)。保证 \(X_1 = 0\),且 \(X_1, X_2, \cdots, X_N\) 单调递增 (可能有人位置重叠)。

开始时, \(K\) 号的烟花刚开始燃烧,其他人的烟花均未点燃。他们的点火工具坏了,只能用燃着的烟花将未点燃的烟花点燃。当两人位置重叠且其中一人手中的烟花燃着时,另一人手中的烟花就可以被点燃。忽略点火所需时间。

求至少需要以多快的速度跑,才能点燃所有人的烟花 (此时可能有些人的烟花已经熄灭了)。速度必须是一个非负整数。


题解:最大值最小,直接考虑二分答案。现在的主要问题是怎么写出一个高效的 check 。

首先,容易发现定然是所有的人向 \(K\) 跑,如果跑到一起的话,那么不着急点燃,待到上一个人快要燃尽时再点燃,容易发现这样一定是不劣的。

那么题目就变成了所有人都需要向中间跑,如果两个人重合的话那么就给火把的燃烧时间增加 \(T\) ,问能否使所有人最终都重合。

这样的话我们可以得到一个 \(O(N^2\log X)\) 的做法,也就是通过 DP 来判断,DP 的状态设计就是 \(f_{l,r}\) 代表区间 \([l,r]\) 中能否全部重合至同一点,因为所有的可行的位置在 \([x_r-vt\cdot (r-l),x_l+vt\cdot (r-l)]\) 中,所以可以直接 DP 判断。

接下来我们考虑优化这一个 \(O(n^2)\) 的算法,然而,貌似没有什么可以优化的地方……

换一种思路,我们考虑贪心,让我们将 \(x_r-vt\cdot (r-l) \leq x_l+vt\cdot (r-l)\) 的式子移项,得到 \(x_r-2vtr \leq x_l-2vtl\)

那么到这里就有一个非常显然的思路就是令 \(a_i = x_i-2vti\) ,然后根据这个来贪心。

我们先贪心地考虑每一次走到目前能够得知一定不劣的解,更详细地说就是令当前的区间为 \([l,r]\) (这里只考虑向左扩展的情况,向右扩展同理)找到一个 \(a_i\) ,使得 \(a_i\geq a_l\)\(\forall a_j\in [i,l], a_j\geq a_r\) ,那么我们就可以贪心地令 \(l\) 变为 \(i\) ,容易证明答案并不会更劣。

那么我们假设用这种方式所扩展出来的极长的区间为 \([L,R]\) ,容易发现,如果我们不存在 \(a_i \geq a_L\) (同样的,这里也是仅考虑左端扩展的情况),那么我们接下来一定会是一个单调递减的 \(a_i\) (我们把连续选取的左端扩展缩到一起),反过来的话就同样是递增了,所以我们从 \([l,n]\) 开始,向内部缩小,如果可以缩小到 \([L,R]\) 那么这种方法可行,否则不可行,这样的话我们就可以得到一个 \(O(n)\) 的 check 的做法了,那么我们就可以在 \(O(n\log X)\) 的时间复杂度内解决这个问题。

update: 现在会证明了,就是如果有一种方案一定可以拆成若干个连续上升和连续下降,并且可以不断的调转方向来求出,然而在做过一次之后不可能再次调转方向了,所以这么做是正确的。

但是上面的方法证明起来非常麻烦(最起码我并没有想到一个比较容易的证明方式),如果你还有耐心的话,可以看一下下面的证明,这里换了一种方式来思考使得这种证明变得显然易见。

显然相邻的两个人相遇的最短时间是 \(\frac{x_{i+1}-x_i}{2v}\) ,我们也必定会让这两个人在花费这么长的时间相遇,否则肯定不优,那么我们可以把原问题看成两个序列,我每一次以取出一个序列的头部,将剩余时间(初始为 \(T\) )减去这个时间,然后获得 \(T\) 的额外时间,询问是否存在能够将两个序列全部取完并且保证中间任意时刻剩余时间都非负的方案。

我们可以考虑将这个序列分成若干段,每一段表示为一个二元组 \((c,v)\) 表示这一段时间消耗的时间和以及能够获得的总时间数并且使得 \(v-c\geq 0\),并且满足这一段区间是极小的极小的定义为这一段区间中不存在一个区间使得这个区间的前缀满足这个性质,显然我们每一次只会消去一整组而不是一个前缀。

那么如果我们没有通过这种方式解决这个问题,那么一定存在一个后缀使得它的每一个前缀的 \(v-c\) 都小于 \(0\) ,并且我们也可以知道最后的剩余时间,所以我们可以考虑时光倒流,这样的话问题就变成了每一次取先消耗 \(T\) 个单位的时间然后在获得 \(\frac{x_{i+1}-x_i}{2v}\) 的时间,由该后缀的性质我们可以知道不可能再次反过来做一遍了。

容易发现这两种做法在本质上是相同的,如果大家有关于第一个做法直接的证明可以与我讨论,这题的时间复杂度为 \(O(n\log X)\)

代码:

#include <cstdio>
void read(int &a){
	a=0;
	char c=getchar();
	while(c<'0'||c>'9'){
		c=getchar();
	}
	while(c>='0'&&c<='9'){
		a=(a<<1)+(a<<3)+(c^48);
		c=getchar();
	}
}
typedef long long ll;
const int Maxn=100000;
int n,k,t;
int x[Maxn+5];
ll a[Maxn+5];
bool check(int v){
	for(int i=1;i<=n;i++){
		a[i]=x[i]-2ll*v*t*i;
	}
	if(a[1]<a[n]){
		return 0;
	}
	int L=k,R=k;
	for(int i=k-1;i>0;i--){
		if(a[i]>=a[L]){
			L=i;
		}
	}
	for(int i=k+1;i<=n;i++){
		if(a[i]<=a[R]){
			R=i;
		}
	}
	int l=k,r=k;
	bool flag=1;
	while(flag){
		flag=0;
		int tmp_l=l;
		while(tmp_l>L&&a[tmp_l-1]>=a[r]){
			tmp_l--;
			if(a[tmp_l]>=a[l]){
				l=tmp_l;
				flag=1;
				break;
			}
		}
		int tmp_r=r;
		while(tmp_r<R&&a[tmp_r+1]<=a[l]){
			tmp_r++;
			if(a[tmp_r]<=a[r]){
				r=tmp_r;
				flag=1;
				break;
			}
		}
		if(!flag){
			break;
		}
	}
	if(l!=L||r!=R){
		return 0;
	}
	l=1,r=n;
	flag=1;
	while(flag){
		flag=0;
		int tmp_l=l;
		while(tmp_l<L&&a[tmp_l+1]>=a[r]){
			tmp_l++;
			if(a[tmp_l]>=a[l]){
				l=tmp_l;
				flag=1;
				break;
			}
		}
		int tmp_r=r;
		while(tmp_r>R&&a[tmp_r-1]<=a[l]){
			tmp_r--;
			if(a[tmp_r]<=a[r]){
				r=tmp_r;
				flag=1;
				break;
			}
		}
		if(!flag){
			break;
		}
	}
	if(l!=L||r!=R){
		return 0;
	}
	return 1;
}
int main(){
	read(n),read(k),read(t);
	for(int i=1;i<=n;i++){
		read(x[i]);
	}
	int left=0,right=(1ll*x[n]+(t<<1)-1)/(t<<1);
	while(left<right){
		int mid=(left+right)>>1;
		if(check(mid)){
			right=mid;
		}
		else{
			left=mid+1;
		}
	}
	printf("%d\n",left);
	return 0;
}
posted @ 2020-10-14 20:49  with_hope  阅读(281)  评论(0编辑  收藏  举报