【LOJ 2392】「JOISC 2017 Day 1」烟花棒(二分答案,贪心)

本篇博客介绍了如何解决一道关于烟花传递的算法问题,涉及二分查找、双指针技巧和数组处理。通过分析题目,确定算法思路,利用二分查找确定最小速度,并使用双指针在数组中进行操作,最后给出具体代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

题目描述

题目传送门 & 洛谷题目传送门

题目译自 JOISC 2017 Day1 T3「手持ち花火 / Sparklers」

N N N 人站在一条数轴上。他们人手一个烟花,每人手中的烟花都恰好能燃烧 T T T 秒。每个烟花只能被点燃一次。
1 1 1 号站在原点, i i i ( 1 ≤ i ≤ n ) (1\leq i\leq n) (1in) 1 1 1 号的距离为 。保证 X 1 = 0 X_1 = 0 X1=0 X 1 , X 2 , . . . , X N X_1,X_2,...,X_N X1,X2,...,XN 单调不降(可能有人位置重叠)。
开始时, K K K 号的烟花刚开始燃烧,其他人的烟花均未点燃。他们的点火工具坏了,只能用燃着的烟花将未点燃的烟花点燃。当两人位置重叠且其中一人手中的烟花燃着时,另一人手中的烟花就可以被点燃。忽略点火所需时间。
求至少需要以多快的速度跑,才能点燃所有人的烟花(此时可能有些人的烟花已经熄灭了)。速度必须是一个非负整数

输入格式

第一行有三个整数 N , K , T N,K,T N,K,T,用空格分隔。
在接下来的 N N N 行中,第 i i i ( 1 ≤ i ≤ N ) (1 \leq i \leq N) (1iN) 有一个整数 X i X_i Xi

输出格式

一个整数,表示要想点燃所有人的烟花,全程中最大速度的最小值。

样例

样例输入 1
3 2 50
0
200
300
样例输出 1
2
样例解释 1
开始时, 1 1 1 号向右, 2 2 2 号向左, 3 3 3 号向左。
50 50 50 秒后, 2 2 2 号传火给 1 1 1 号。随后, 1 1 1 号和 3 3 3 号继续移动。
又过了 25 25 25 秒, 1 1 1 号传火给 3 3 3 号。

样例输入 2
3 2 10
0
200
300
样例输出 2
8
样例解释 2
开始时, 1 1 1 号向右, 2 2 2 号向右, 3 3 3 号向左。
3 3 3 秒后, 2 2 2 号停止移动。
又过了 6.5 6.5 6.5 秒, 3 3 3 号到达 2 2 2 号所在位置, 3 3 3 号停止移动。
又过了 0.5 0.5 0.5 秒, 2 2 2 号传火给 3 3 3 号。
又过了 9 9 9 秒, 3 3 3 号传火给 1 1 1 号。

样例输入 3
20 6 1
0
2
13
27
35
46
63
74
80
88
100
101
109
110
119
138
139
154
172
192
样例输出 3
6

数据范围与提示

对于 30 % 30\% 30% 的数据, N ≤ 20 N \le 20 N20
对于 50 % 50\% 50% 的数据, N ≤ 1000 N \le 1000 N1000
对于 100 % 100\% 100% 的数据, 1 ≤ K , N ≤ 1 0 5 , 1 ≤ T ≤ 1 0 9 , 0 ≤ X i ≤ 1 0 9 ( 1 ≤ i ≤ N ) , X 1 = 0 , { X N } 1\le K, N \le 10^5, 1\le T\le 10^9, 0\le X_i\le 10^9 (1\le i\le N), X_1 = 0, \{X_N\} 1K,N105,1T109,0Xi109(1iN),X1=0,{XN} 单调递增。

思路

非常玄妙的一道题。

算法非常显然,答案具有可二分性,于是考虑二分答案,所以我们只需要判断当速度为 v v v 时是否可以点燃所有烟花棒,关键就在于我们如何写这样的一个 O ( n ) O(n) O(n) 的 check 函数。

很显然,每个人都应该往手里有火的那个人的方向跑。注意到两人相遇后不用立即传火。现在,我们给定一个很重要的性质:每个人遇到手上有火的那个人之后,如果火还没灭,那么他一定会跟着手上有火的人,知道另外一个人手上的火熄灭,此时再传给这个手上没有火的人。

下面,我们证明,立即传火是不优的:

首先,如果两个人传火后,移动方向相同,显然不传火一定更优,所以只需考虑两人相反移动的情况。

不妨令原来手上有火的那个人向右走,另一个人向左走。

考虑两人相遇后立即传火。假设他们相遇的点在 x x x,手上的火还剩 t t t 秒,那么他们能够扩展的区间为: [ x − T v , x + c v ] [x-Tv, x+cv] [xTv,x+cv] v v v 为速度, T T T 见题目描述,为燃烧时间)。

而如果两个人相遇后不传火,那么他们能够扩展的区间为: [ x + c v − T v , x + c v ] [x+cv-Tv,x+cv] [x+cvTv,x+cv],即 [ x − ( T − c ) v , x + c v ] [x-(T-c)v, x+cv] [x(Tc)v,x+cv],看似比上面扩展的区间小,但其实其他点也跟着一起运动了 c c c 秒,而向左扩展的区间少了 c c c 秒,根据相对运动,他们的距离和上面没有变化,由此得证。

于是,我们得到了很重要的一个结论:如果两个人相遇,我们就把其视作火把燃烧时间延伸了 T T T

我们还发现,由于每个人的速度相同,所以在移动方向相同时,两个人之间的距离和相对位置是不会发生改变的,只有两个人相向而行时,距离才会变小,且花费时间不会受之前的影响,于是我们可以将问题转化。

我们记录两个数组 a , b a,b a,b,其中 a i = X i + 1 − X i 2 × v , ( i < k ) a_i = \frac{X_{i+1}-X_i}{2\times v}, (i < k) ai=2×vXi+1Xi,(i<k) b i = X i − X i − 1 2 × v , ( i > k ) b_i= \frac{X_i-X_{i-1}}{2\times v},(i>k) bi=2×vXiXi1,(i>k)

现在,我们要删除这两个数组。两个数组内部要有序删除,其中, a a a 数组要按照 k − 1 ∼ 1 k-1\sim 1 k11 的顺序删除, b b b 数组要按照 k + 1 ∼ n k+1\sim n k+1n 的顺序删除。每次删掉一个 a i a_i ai b i b_i bi 后,就会先减少 a i a_i ai b i b_i bi 的时间,并获得 T T T 的时间,要求最后的剩余时间非负。

贪心的选择当前收益更大的策略显然是错误的。我们考虑一直删除一个数组,直到收益非负时,就可以收手了。也就是说,我们把数组分成若干段,每一段任意前缀的收益都为负,只有整组都删的收益为非负(这里 a a a 数组 k − 1 k-1 k1 为前, 1 1 1 为后)。显然,我们每次删都会删掉一整组数,而非在中途改变方向。对于每一个组,我们可以用一个二元组 ( p , v ) (p,v) (p,v) 表示删完这一个组需要 p p p 秒,可以获得 v v v 秒时间。(维护方法详见代码)

就这样,我们将两个数组划分成了若干个组,我们每次贪心的去选择 p p p 最小的那个组(当然,组间顺序不能忽略),如果当前时间已经 < p < p <p 了,那就不合法了,否则删完这个组,获得 v v v 秒时间。

此时,我们注意到,一个数组并不一定会完全被划分为这若干个组,可能会有一段后缀剩余。这该怎么办呢?

不难发现,最终剩余的时间是固定的,为 ∑ i = 1 t o t a ( − a i + T ) + ∑ i = 1 t o t b ( − b i + T ) \sum \limits_{i=1}^{tota}(-a_i+T)+\sum \limits_{i=1}^{totb}(-b_i+T) i=1tota(ai+T)+i=1totb(bi+T),其中 t o t a tota tota t o t b totb totb 分别代表 a a a b b b 数组的长度。那么,我们只需将其翻转,进行时光倒流。此时,删掉一个数就相当于先减去 T T T 秒,在加上 a i a_i ai b i b_i bi。于是我们可以类似上面的方法,将剩下的这些数也分成若干个组,每个组也用一个二元组 ( p , v ) (p,v) (p,v) 表示删完这一个组需要先消耗 p p p 秒,可以加回 v v v 秒时间。(维护方法几乎相同,详见代码)。

依旧是保证任意时刻时间非负,每次贪心选 p p p 小的。

时间复杂度 O ( n    l o g    v ) O(n \;log\;v) O(nlogv)

注意,注意,注意,如果你在洛谷(即atcoder)上提交这一题,输出一定要加上一个换行,atcoder 评测不是忽略行末换行!!!(坑了我十几发提交)

代码

#include <bits/stdc++.h>
#define rep(i, a, b) for (register int i(a); i <= b; ++i)
#define per(i, a, b) for (register int i(a); i >= b; --i)
#define FILE(s) freopen(s".in", "r", stdin), freopen(s".out", "w", stdout)
#define mem(a, x) memset(a, x, sizeof a)
#define pb push_back
#define umap unordered_map
#define pqueue priority_queue
#define mp make_pair
#define PI acos(-1)
//#define int long long

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
int n, k, t, x[100005];
double a[100005], b[100005];
struct node {
    double p, v;
} A[100005], B[100005];
const double eps = 1e-12;

template <typename _T>
void rd(_T &x) {
    int f = 1; x = 0;
    char s = getchar();
    while (s > '9' || s < '0') {if (s == '-') f = -1; s = getchar();}
    while (s >= '0' && s <= '9') x = (x<<3)+(x<<1)+(s^48), s = getchar();
    x *= f;
}

template <typename _T>
void write(_T x) {
    if (x < 0) putchar('-'), x = -x;
    if (x > 9) write(x/10);
    putchar(x%10+'0');
    return ;
}

bool check(int v) {
    int tota = 0, totb = 0;
    per(i, k-1, 1) a[++tota] = ((double)(x[i+1]-x[i])/2./(double)(v));
    rep(i, k+1, n) b[++totb] = ((double)(x[i]-x[i-1])/2./(double)(v));
    double now = 0., minn = 9e18;
    int totA = 0, totB = 0;
    int p = 0, q = 0;
    rep(i, 1, tota) {
        now -= a[i];
        minn = min(minn, now);
        now += (double)(t);
        if (now > -eps) A[++totA] = (node){-minn, now}, now = 0., minn = 9e18, p = i;
    }
    now = 0., minn = 9e18;
    rep(i, 1, totb) {
        now -= b[i];
        minn = min(minn, now);
        now += (double)(t);
        if (now > -eps) B[++totB] = (node){-minn, now}, now = 0., minn = 9e18, q = i;
    }
    int p1 = 1, p2 = 1;
    now = (double)(t);
    while (p1 <= totA || p2 <= totB) {
        if (p1 <= totA && (now-A[p1].p) > -eps) now += A[p1++].v;
        else if (p2 <= totB && (now-B[p2].p) > -eps) now += B[p2++].v;
        else return false;
    }
    totA = totB = 0;
    now = 0., minn = 9e18;
    per(i, tota, p+1) {
        now -= (double)(t);
        minn = min(minn, now);
        now += a[i];
        if (now > -eps) A[++totA] = (node){-minn, now}, now = 0., minn = 9e18;
    }
    now = 0., minn = 9e18;
    per(i, totb, q+1) {
        now -= (double)(t);
        minn = min(minn, now);
        now += b[i];
        if (now > -eps) B[++totB] = (node){-minn, now}, now = 0., minn = 9e18;
    }
    now = (double)(t);
    rep(i, 1, tota) now -= a[i], now += (double)(t);
    rep(i, 1, totb) now -= b[i], now += (double)(t);
    if (now < 0) return false;
    p1 = p2 = 1;
    while (p1 <= totA || p2 <= totB) {
        if (p1 <= totA && (now-A[p1].p) > -eps) now += A[p1++].v;
        else if (p2 <= totB && (now-B[p2].p) > -eps) now += B[p2++].v;
        else return false;
    }
    return true;
}

int main() {
    rd(n), rd(k), rd(t);
    rep(i, 1, n) rd(x[i]);
    int l = 0, r = 1e9, ans = 0;
    while (l <= r) {
        int mid = (l+r) >> 1;
        if (check(mid)) ans = mid, r = mid-1;
        else l = mid+1;
    }
    printf("%d", ans);
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值