2024牛客多校3A Bridging the Gap 2

希望更丰富的展现?来我搭建的网站看看

Problem

n n n 个人乘船过河,该船容纳人的上限为 R R R,并且需要至少 L L L 个人才能操作。每次过河时所有人都需划船,使船上所有人的耐力值减 1 1 1。最初每个人的耐力值为 h i h_i hi

判断是否所有人都能过河。

1 ≤ L < R ≤ n ≤ 5 × 1 0 5 1\le L<R\le n\le 5\times 10^5 1L<Rn5×105

1 ≤ h i ≤ 5 × 1 0 5 1\le h_i\le 5\times 10^5 1hi5×105

Solution

每个人都需要花费至少一点体力来过河。有多余的体力的话可以用于划来回,花费两点耐力当船夫。所以一个人最多可以当 a i = ⌊ h i − 1 2 ⌋ a_i=\lfloor\frac{h_i-1}{2}\rfloor ai=2hi1 次船夫。

贪心,每次过河可以送 R − L R-L RL 个人过河,最后一趟 R R R 个人全部下船,所以至少需要来回 T = ⌈ n − R R − L ⌉ T=\lceil\frac{n-R}{R-L}\rceil T=RLnR 次来回,每个来回都需要 L L L 名船夫。

现在问题转化为:每次从 a i a_i ai 中选 L L L 个数字全部减 1 1 1,问能否进行 T T T 次这样的操作?

直接上结论:如果 ∑ i = 1 n min ⁡ ( a i , T ) ≥ T × L \sum_{i=1}^n\min(a_i,T)\ge T\times L i=1nmin(ai,T)T×L,则可以。反之不能。

该条件的必要性是很明显的:对于每个 a i a_i ai,只有 ≤ T \le T T 的部分才是可能有效的。至少要所有 a i a_i ai 的有效部分 min ⁡ ( a i , T ) \min(a_i,T) min(ai,T) 之和超过操作 T T T 次的总消耗 T × L T\times L T×L 才有可能有用。

该条件的充分性用下述贪心证明:

贪心:每次选择最大的 L L L a i a_i ai 进行操作。

此时操作一次结束之后的数组为 a i ′ a_i^\prime ai,对于 a i ′ a_i^\prime ai 还需要进行 T − 1 T-1 T1 次操作,条件 ∑ i = 1 n min ⁡ ( a i , T ) ≥ T × L \sum_{i=1}^n\min(a_i,T)\ge T\times L i=1nmin(ai,T)T×L 等价于 ∑ i = 1 n min ⁡ ( a i ′ , T − 1 ) ≥ ( T − 1 ) × L \sum_{i=1}^n\min(a_i^\prime,T-1)\ge (T-1)\times L i=1nmin(ai,T1)(T1)×L

继续等价于:执行了 T − 1 T-1 T1 次操作之后的 a i ( T − 1 ) a_i^{(T-1)} ai(T1) 需要满足 ∑ i = 1 n min ⁡ ( a i ( T − 1 ) , 1 ) ≥ L \sum_{i=1}^n\min(a_i^{(T-1)},1)\ge L i=1nmin(ai(T1),1)L,也就是此时需要在 a i ( T − 1 ) a_i^{(T-1)} ai(T1) 中需要至少有 L L L 个正数,才能满足可以进行一次操作。这个条件显然是充分必要的。

由数学归纳法可知正确性。

时间复杂度 O ( n ) O(n) O(n)

Code

#define N 500010

LL n,L,R,T;
LL h[N],a[N];

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cout.precision(10);
	int t=1;
//	cin>>t;
	while(t--)
	{
		cin>>n>>L>>R;
		for(int i=1;i<=n;i++) cin>>h[i],a[i]=(h[i]-1)/2;
		T=(n-R+(R-L-1))/(R-L);//ceil
		LL sum=0;
		for(int i=1;i<=n;i++)
		{
			sum+=min(T,a[i]);
		}
		if(sum<T*L) cout<<"No"<<endl;
		else cout<<"Yes"<<endl;
	}
	return 0;
}

Extra

这个问题总感觉很常见呢?但是居然没有想过该如何解决。

每次从 a i a_i ai 中选 L L L 个数字全部减 1 1 1,问能否进行 T T T 次这样的操作?

假如换一种问法:

每次从 a i a_i ai 中选 L L L 个数字全部减 1 1 1,问最多能进行多少次这样的操作?

一种很显然的做法就是可以二分答案,时间复杂度为 O ( n log ⁡ ( ∑ a L ) ) O(n\log(\frac{\sum a}{L})) O(nlog(La)),时间复杂度其实是还能接受的。但是赛时 langod 就是这抱着“先求出最多能够进行的操作数量再与 T T T 进行比较”的想法,找到了一种直接求出“最大操作数”的算法,时间复杂度为 O ( n log ⁡ ( n ) ) O(n\log (n)) O(nlog(n))。算法是正确的,但是过程稍微复杂一点:

  1. a i a_i ai 升序排序。
  2. [ n − L + 1 , n ] [n-L+1,n] [nL+1,n] 的左右插入无限高的挡板,现在 [ n − L + 1 , n ] [n-L+1,n] [nL+1,n] 变成了一段容器的底部。
  3. 假想 [ 1 , n − L ] [1,n-L] [1,nL] 这一段“液化”了,也就是可以流动,其面积为 w a t e r = ∑ i = 1 n − L a i \displaystyle water=\sum_{i=1}^{n-L} a_i water=i=1nLai
  4. [ 1 , n − L ] [1,n-L] [1,nL] 这一段“液体”全部倒在 [ n − L + 1 , n ] [n-L+1,n] [nL+1,n] 段上。液体会优先填补靠左的更低的部分,逐渐向上抬高水面的同时向右覆盖。
  5. 最后液体上表面距离地面的高度 H H H 即为答案。

正确性可由上结论推导。假设水只覆盖了 [ n − L + 1 , j ] [n-L+1,j] [nL+1,j] 段,长度为 j − ( n − L + 1 ) j-(n-L+1) j(nL+1),高度为 H H H,则现在我们需证明这一段能够进行 H H H​ 次操作。

由于 ∑ i = 1 n min ⁡ ( a i , T ) ≥ T × L ⇒ 可进行 T 次操作 \sum_{i=1}^n\min(a_i,T)\ge T\times L\Rightarrow 可进行T次操作 i=1nmin(ai,T)T×L可进行T次操作

∀ i ∈ [ 1 , j ] , a i ≤ H \forall i\in[1,j],a_i\le H i[1,j],aiH,所以有
$$
\begin{align}
\sum_{i=1}^j \min(a_i,H)=& \sum_{i=1}^j a_i\
=&[n-L+1,j]段的面积\
=&H\times [j-(n-L+1)]

\end{align}
$$

∑ i = 1 j min ⁡ ( a i , H ) = ∑ i = 1 j a i = [ n − L + 1 , j ] 段的面积 = H × [ j − ( n − L + 1 ) ] \sum_{i=1}^j \min(a_i,H)=\sum_{i=1}^j a_i=[n-L+1,j]段的面积=H\times [j-(n-L+1)] i=1jmin(ai,H)=i=1jai=[nL+1,j]段的面积=H×[j(nL+1)]

具体在模拟计算时可以尝试用 w a t e r water water 从下到上逐渐一层层的填补空缺部分,直到无法向右上溢为止。

#define N 500010

LL n,L,R,T;
LL h[N],a[N];
LL ans;
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cout.precision(10);
	int t=1;
//	cin>>t;
	while(t--)
	{
		cin>>n>>L>>R;
		for(int i=1;i<=n;i++) cin>>h[i],a[i]=(h[i]-1)/2;
		T=(n-R+(R-L-1))/(R-L);
		
		
		sort(a+1,a+n+1);
		a[n+1]=1000000000ll;
		LL water=0;
		for(int i=1;i<=n-L;i++) water+=a[i];
		for(int i=1;i<=L;i++)
		{
			int j=i+(n-L);
			if(water>=(a[j+1]-a[j])*i)
			{
				water-=(a[j+1]-a[j])*i;
			}
			else
			{
				ans=a[j];
				ans+=water/i;
				break;
			}
		}
//		cout<<ans<<endl;
		if(ans>=T) cout<<"Yes"<<endl;
		else cout<<"No"<<endl;
		
	}
	return 0;
}
  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值