去年参加ACM北美赛区资格赛的时候遇到的题目,今天偶然翻来做一做。
题意
正常情况下每分钟你的生命就会流失一分钟,但是在一些时间点上你可以获得一些( N ≤ 1 0 5 N \leq 10^5 N≤105)改变生命流失速度的机会,但是如果你改变了生命流失速率,那么你就会瞬间流失 C C C 分钟的生命,问你最晚的死亡时间是什么时候。
解题思路
如果用动态规划的方式去求解,那么可以很容易得到一个DP方程,如果我们设 f i = f_i = fi= 以第 i i i个药片作为最后使用的药片的最晚死亡时间。那么转移方程就应该是: f i = max { g ( i , k ) } ( 0 ≤ k < i ) f_i = \max \{g(i, k)\} \ (0 \leq k < i) fi=max{g(i,k)} (0≤k<i)。
g
(
i
,
k
)
g(i, k)
g(i,k)就表示之前使用了第
k
k
k个药片,换成第
i
i
i个药片以后所能存活的最长时间。如果我们把整个过程看做一个线性函数斜率不断改变的过程,那么
y
y
y坐标就是不做改变应该存活的时间,而
x
x
x轴就代表实际存活的时间。假设一开始死亡时间为
T
T
T,药片出现时间为
t
i
t_i
ti,改变的斜率为
k
i
k_i
ki,那么
g
(
i
,
k
)
=
t
i
+
T
−
使
用
i
时
的
高
度
k
i
g(i,k) = t_i + \frac{T-使用i时的高度}{k_i}
g(i,k)=ti+kiT−使用i时的高度。合并到DP方程展开后就是:
f
i
=
max
(
t
i
+
T
−
(
k
j
t
j
+
T
−
k
j
f
j
+
k
j
(
t
i
−
t
j
)
+
C
)
k
i
)
f_i= \max{} \bigg( t_i+\frac{T-(k_jt_j+T-k_jf_j+k_j(t_i-t_j) + C)}{k_i} \bigg)
fi=max(ti+kiT−(kjtj+T−kjfj+kj(ti−tj)+C))
化简一下
f
i
=
max
(
t
i
−
C
+
k
j
(
t
i
−
f
j
)
k
i
)
f_i=\max{} \bigg( t_i-\frac{C+k_j(t_i-f_j)}{k_i} \bigg)
fi=max(ti−kiC+kj(ti−fj))
其中
1
≤
k
<
i
1 \leq k<i
1≤k<i。
但是观察一下数据范围, 1 0 5 10^5 105 显然对于这个 O ( N 2 ) O(N^2) O(N2) 的DP来说太大了,但是我们观察到 t i t_i ti是单调上升的,而且对于进来的不同 k i k_i ki,有一部分斜率比之前最小值要大的显然不能构成最优解,也就是说,这里面 j j j的选择是可以优化的。那么如果想让程序不超时,我们需要一个 O ( 1 ) O(1) O(1)的最优解选择方法,那么就要想到斜率DP。
我们继续优化这个式子,假设现在有两个药片
s
,
j
s, j
s,j且
s
<
j
<
i
s < j < i
s<j<i。那么假设
j
j
j是比
s
s
s更优的选择,那么一定满足:
t
i
−
C
+
k
s
(
t
i
−
f
s
)
k
i
<
t
i
−
C
+
k
j
(
t
i
−
f
j
)
k
i
t_i-\frac{C+k_s(t_i-f_s)}{k_i} < t_i-\frac{C+k_j(t_i-f_j)}{k_i}
ti−kiC+ks(ti−fs)<ti−kiC+kj(ti−fj)
两边相同部分删去,由于本题保证斜率不为负数,所以符号不变
C
+
k
s
(
t
i
−
f
s
)
k
i
<
−
C
+
k
j
(
t
i
−
f
j
)
k
i
k
s
t
i
−
k
s
f
s
>
k
j
t
i
−
k
j
f
j
\frac{C+k_s(t_i-f_s)}{k_i}<-\frac{C+k_j(t_i-f_j)}{k_i} \\ k_st_i-k_sf_s>k_jt_i-k_jf_j
kiC+ks(ti−fs)<−kiC+kj(ti−fj)ksti−ksfs>kjti−kjfj
因为前面斜率一定比后面的大,所以
(
k
s
−
k
j
)
t
i
>
k
s
f
s
−
k
j
f
j
(k_s-k_j)t_i>k_sf_s-k_jf_j
(ks−kj)ti>ksfs−kjfj
最终我们得到了
t
i
>
k
s
f
s
−
k
j
f
j
k
s
−
k
j
t_i> \frac{k_sf_s-k_jf_j}{k_s-k_j}
ti>ks−kjksfs−kjfj,当这个条件满足的时候,我们知道选择第
j
j
j个药片比第
s
s
s个药片要优。
如果我们定义
s
l
o
p
e
(
s
,
j
)
=
k
s
f
s
−
k
j
f
j
k
s
−
k
j
slope(s, j) = \frac{k_sf_s-k_jf_j}{k_s-k_j}
slope(s,j)=ks−kjksfs−kjfj也就是相当于点
s
s
s到点
j
j
j的斜率,那我们需要判断满足这个条件的点具有的特征。假设对于三个点
s
<
j
<
i
s < j < i
s<j<i并且
s
l
o
p
e
(
s
,
j
)
>
s
l
o
p
e
(
j
,
i
)
slope(s, j) > slope(j, i)
slope(s,j)>slope(j,i),那么我们可以判断点
j
j
j一定不会形成最优解。
证明也很简单,假设
t
i
>
s
l
o
p
e
(
s
,
j
)
>
s
l
o
p
e
(
j
,
i
)
t_i > slope(s, j) > slope(j, i)
ti>slope(s,j)>slope(j,i),那么最优解一定会是
i
i
i而不是
j
j
j,根据我们之前的定义。反过来如果
t
i
≤
s
l
o
p
e
(
s
,
j
)
t_i \leq slope(s, j)
ti≤slope(s,j),那么
s
s
s才是最优解,所以无论哪种情况上凸起的
j
j
j都不会形成最优解,因此利用单调队列维护一个下凸包就可以了。
剩下的就是用单调队列维护下凸包的操作了,只要队首的元素
s
l
o
p
e
(
l
,
l
+
1
)
slope(l, l + 1)
slope(l,l+1)得出最优解不是
l
l
l,那么
l
l
l位置就可以出列了,否则队首就是最优解。计算完
f
i
f_i
fi以后插入队尾也是一样,
s
l
o
p
e
(
r
−
1
,
r
)
slope(r-1,r)
slope(r−1,r)得出
r
r
r不是最优解就要出列。注意一定要排除
i
i
i号药片斜率比
i
i
i之前的最小值要大的情况,因为这种情况会干扰单调性导致算法不正确。
时间复杂度
O ( N ) O(N) O(N)
代码
我用了一个优先队列来储存最小值,其实没必要
#include <algorithm>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
typedef long long ll;
using namespace std;
const int INF = 2147483647;
const int INF2 = 0x3f3f3f3f;
const ll INF64 = 1e18;
const double INFD = 1e30;
const double EPS = 1e-6;
const double PI = 3.14159265;
const ll MOD = 1e9 + 7;
const int MAXN = 100005;
ll n, m, k;
int CASE = 1;
// 这里我用了long double来防止精度不够
typedef long double LD;
struct Pill {
ll t;
LD ks;
Pill() {}
Pill(ll t, ll x, ll y) : t(t), ks(y / (LD)x) {}
bool operator<(const Pill& b) const { return t < b.t; }
};
vector<Pill> pills;
LD dp[MAXN];
// 模拟了一个队列
int QQ[MAXN * 2];
// 斜率计算
LD slope(int s, int j) {
return (pills[j].ks * dp[j] - pills[s].ks * dp[s]) /
(pills[j].ks - pills[s].ks);
}
int main() {
#ifdef LOCALLL
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
scanf("%lld%lld%lld", &n, &m, &k);
for (int i = 0; i < m; i++) {
ll t, x, y;
scanf("%lld%lld%lld", &t, &x, &y);
pills.emplace_back(t, x, y);
}
pills.emplace_back(0, 1, 1);
sort(pills.begin(), pills.end());
dp[0] = n;
QQ[1] = 0;
int l = 1, r = 1;
priority_queue<LD, vector<LD>, greater<LD>> PQ;
PQ.push(1);
for (int i = 1; i <= m; i++) {
if (pills[i].ks >= PQ.top()) continue;
while (l < r && slope(QQ[l], QQ[l + 1]) <= pills[i].t) ++l;
int opt = QQ[l];
LD ki = pills[i].ks;
LD kj = pills[opt].ks;
// 根据公式从dp[j]计算dp[i]
LD newx = (LD)pills[i].t - (k + kj * (pills[i].t - dp[opt])) / ki;
dp[i] = max(dp[i], newx);
while (l < r && slope(QQ[r - 1], QQ[r]) >= slope(QQ[r], i)) r--;
QQ[++r] = i;
PQ.push(pills[i].ks);
}
LD ans = 0;
for (int i = 0; i <= m; i++) {
ans = max(ans, dp[i]);
}
printf("%.7Lf", ans);
return 0;
}