2018 ICPC North American Qualifier Contest Longest Life(斜率DP)

去年参加ACM北美赛区资格赛的时候遇到的题目,今天偶然翻来做一做。

题意

正常情况下每分钟你的生命就会流失一分钟,但是在一些时间点上你可以获得一些( N ≤ 1 0 5 N \leq 10^5 N105)改变生命流失速度的机会,但是如果你改变了生命流失速率,那么你就会瞬间流失 C C C 分钟的生命,问你最晚的死亡时间是什么时候。

解题思路

如果用动态规划的方式去求解,那么可以很容易得到一个DP方程,如果我们设 f i = f_i = fi= 以第 i i i个药片作为最后使用的药片的最晚死亡时间。那么转移方程就应该是: f i = max ⁡ { g ( i , k ) }   ( 0 ≤ k &lt; i ) f_i = \max \{g(i, k)\} \ (0 \leq k &lt; i) fi=max{g(i,k)} (0k<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+Tkjfj+kj(titj)+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(tikiC+kj(tifj))
其中 1 ≤ k &lt; i 1 \leq k&lt;i 1k<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 &lt; j &lt; i s &lt; j &lt; i s<j<i。那么假设 j j j是比 s s s更优的选择,那么一定满足:
t i − C + k s ( t i − f s ) k i &lt; t i − C + k j ( t i − f j ) k i t_i-\frac{C+k_s(t_i-f_s)}{k_i} &lt; t_i-\frac{C+k_j(t_i-f_j)}{k_i} tikiC+ks(tifs)<tikiC+kj(tifj)
两边相同部分删去,由于本题保证斜率不为负数,所以符号不变
C + k s ( t i − f s ) k i &lt; − C + k j ( t i − f j ) k i k s t i − k s f s &gt; k j t i − k j f j \frac{C+k_s(t_i-f_s)}{k_i}&lt;-\frac{C+k_j(t_i-f_j)}{k_i} \\ k_st_i-k_sf_s&gt;k_jt_i-k_jf_j kiC+ks(tifs)<kiC+kj(tifj)kstiksfs>kjtikjfj
因为前面斜率一定比后面的大,所以
( k s − k j ) t i &gt; k s f s − k j f j (k_s-k_j)t_i&gt;k_sf_s-k_jf_j (kskj)ti>ksfskjfj
最终我们得到了 t i &gt; k s f s − k j f j k s − k j t_i&gt; \frac{k_sf_s-k_jf_j}{k_s-k_j} ti>kskjksfskjfj,当这个条件满足的时候,我们知道选择第 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)=kskjksfskjfj也就是相当于点 s s s到点 j j j的斜率,那我们需要判断满足这个条件的点具有的特征。假设对于三个点 s &lt; j &lt; i s &lt; j &lt; i s<j<i并且 s l o p e ( s , j ) &gt; s l o p e ( j , i ) slope(s, j) &gt; slope(j, i) slope(s,j)>slope(j,i),那么我们可以判断点 j j j一定不会形成最优解。
文中的三个点示意
证明也很简单,假设 t i &gt; s l o p e ( s , j ) &gt; s l o p e ( j , i ) t_i &gt; slope(s, j) &gt; 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) tislope(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(r1,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;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值