1 条题解
-
0
题目 D. Aquatic Dragon 详细题解
问题描述
有 个岛屿排成一线,编号 到 。相邻岛屿 与 之间有一对单向水下隧道(一条 ,一条 ),每条隧道最多使用一次。
初始你和龙都在岛 ,目标是一起到达岛 。
每个岛 上有一个神社,第一次访问时会立即将龙的体力增加 (不耗时)。龙初始体力 。
允许三种移动:- 游泳:与龙一起移动到相邻岛,要求体力 ,消耗 体力,耗时 。
- 飞行:与龙一起移动到相邻岛,要求体力 ,消耗全部体力(变为 ),耗时 。
- 步行:独自通过隧道走到相邻岛,不消耗体力,耗时 ,每条隧道只能使用一次。
求最小总时间。
关键观察与简化
-
飞行与游泳的比较
若 ,则飞行总是优于或等于游泳。此时最优策略是一直飞行:从 飞到 , 飞到 ,…,直到 。总时间 。
以下假设 。 -
步行路径的结构
由于每条隧道只能使用一次,步行路径不会出现重叠的区间(但可以端点相接)。
可以将整个行程分解为若干个步骤,每个步骤对应一段步行区间 (),具体执行方式为:- 先从 步行到某个端点( 或 ),再步行返回 ,从而第一次访问区间内所有岛屿的神社。
- 然后将龙从 移动到 (通过游泳或飞行)。
根据是否与下一个步骤相接,可分为三种类型:
- 类型 1:步行到 并返回,然后游泳到 。
- 类型 2:步行到 并返回,然后飞行到 。
- 类型 3:步行到 并返回,然后飞行到 (此时 的神社已被激活,下一步从 开始)。
若步骤以游泳结束且与下一步相接,则合并两个步骤更优,因此最优解中不会出现这种情况。所以只需考虑以上三种类型。
-
体力变化建模
定义前缀和再定义辅助数组
考虑上一次飞行发生在从 到 的情况:
- 若为类型 2(飞行后神社 被激活),则到达 时龙的体力为
- 若为类型 3(飞行后神社 已被使用过,即此前已经激活),则体力为
因此定义基准值 :
- 类型 2 步后,。
- 类型 3 步后,。
-
执行一个步骤的条件
设当前在岛 ,基准值为 ,要执行一个以 为终点的步骤:- 若以游泳结束(类型 1),需要保证沿途体力非负,等价于 。
- 若以飞行结束(类型 2 或 3),需要飞行前体力 ,即:
- 类型 2:。
- 类型 3:。 由于 ,可简化判断,但原题解采用 和 。
-
时间代价
设步骤从 到 :- 若类型 1:步行往返距离 ,耗时 ;游泳距离 ,耗时 。
- 若类型 2:步行往返 ,飞行一次 。
- 若类型 3:步行往返 ,飞行一次 。
在从 到 的区间中,可以穿插多个类型 1 的游泳步骤(从某个 到 游泳)。设 为满足 且 的 的个数,则这些 可以游泳通过,从而节省对应的步行时间(原本需要步行往返该段,现改为游泳一次)。因此总时间可写为:
$$\text{time} = T_s\cdot (y-1-x) + T_f + 2T_w\cdot (y-1-x - c) + \begin{cases}0 & e'=0 \\ 2T_w & e'=1\end{cases} + dp[y][e'] $$其中 表示飞行步骤的类型(0 表示类型 2,1 表示类型 3), 是从 开始到 的最优时间。
动态规划定义
设
- :当前在岛 且与龙在一起,上一次飞行是类型 2 从 到 ,即 。
- :上一次飞行是类型 3 从 到 ,即 。
边界:(已到达终点)。
答案:(初始状态可视为未飞行,但 ,,实际上需要特殊处理,但通过转移可覆盖)。
转移方程
对于状态 ,考虑下一步飞到 (),且飞行步骤类型为 。
当前 。
条件:- 若 ,需 ,且新 。
- 若 ,需 ,且新 。
设 为满足 且 的 的个数。则转移代价为:
$$ \begin{aligned} \text{cost} &= T_s\cdot (y-1-x) + T_f + 2T_w\cdot (y-1-x - c) \\ &\quad + (e'==1 ? 2T_w : 0) + dp[y][e'] \end{aligned} $$于是
优化:减少状态枚举
直接转移是 ,需要优化。
关键观察:若 ,则飞行步骤可以用游泳代替(因为 ),且不会使后续条件更严格。因此我们只考虑即新基准值严格小于当前基准值,且大于当前基准值减 。
所有可能的基准值只有 个( 和 )。将状态 按 从小到大排序,依次计算 。
维护两个线段树(对应 和 ),每个线段树的索引 存储:
$$\text{val}_{e'}[y] = dp[y][e'] + T_s\cdot (y-1) + 2T_w\cdot (\text{已失效的步行段数}) + \text{可能额外项} $$具体来说,随着 增大,需要:
- 将那些 的 对应的步行代价增加 (因为它们不能再作为游泳步骤)。
- 将那些 的 从线段树中删除(设为 )。
通过全局偏移量加线段树区间加实现。查询时,对 求最小值,然后加上 等项得到 。计算完后将 插入对应的线段树。
边界处理:岛屿
到达 时,可以有两种方式:
- 最后一步是游泳到 ,则需满足 (因为游到 后不需要再飞行)。
- 最后一步是飞行到 ,则按正常转移处理。
另外,也可以完全不用飞行,全程通过步行+游泳到达。这种情况可以通过初始化时考虑 并反向转移得到,或在最终答案中取 与直接计算。
算法步骤总结
- 读入 和数组 。
- 若 ,输出 ,结束。
- 计算前缀和 ,再计算 ,。
- 建立 个状态 和 ,分别赋予键值 和 。按键值升序排序。
- 初始化两个线段树(大小 ),所有值设为 。将 和 插入对应线段树(注意插入时需考虑 和 的偏移)。
- 维护一个指针,标记当前已失效的 (即 ),将它们在线段树中置为 。
- 按排序顺序处理每个状态 :
- 更新失效指针,进行区间加(增加步行代价)。
- 在线段树中查询 的最小值,加上 等得到候选值。
- 更新 为该候选值(若多个转移取最小)。
- 将 插入对应的线段树(索引 ),插入值需减去相应的偏移以便后续查询。
- 最终答案取 (并考虑全程无飞行的可能)。
复杂度分析
- 排序 。
- 每个状态处理时,线段树查询、更新均为 ,总状态数 ,故总复杂度 。
- 空间 。
参考代码框架(C++)
#include <bits/stdc++.h> using namespace std; using ll = long long; const ll INF = 1e18; int N, D, Ts, Tf, Tw; vector<int> P; vector<ll> Q, R, S; struct State { int idx, type; // type 0 -> R, 1 -> S ll val; bool operator<(const State& o) const { return val < o.val; } }; // 线段树支持区间加、单点赋值、区间最小值 struct SegTree { int n; vector<ll> mn, lazy; SegTree(int sz) { n = 1; while (n < sz) n <<= 1; mn.assign(2*n, INF); lazy.assign(2*n, 0); } void apply(int node, ll add) { mn[node] += add; lazy[node] += add; } void push(int node) { if (lazy[node]) { apply(node*2, lazy[node]); apply(node*2+1, lazy[node]); lazy[node] = 0; } } void range_add(int l, int r, ll add, int node, int nl, int nr) { if (r < nl || nr < l) return; if (l <= nl && nr <= r) { apply(node, add); return; } push(node); int mid = (nl+nr)/2; range_add(l, r, add, node*2, nl, mid); range_add(l, r, add, node*2+1, mid+1, nr); mn[node] = min(mn[node*2], mn[node*2+1]); } void point_set(int pos, ll val, int node, int nl, int nr) { if (nl == nr) { mn[node] = val; return; } push(node); int mid = (nl+nr)/2; if (pos <= mid) point_set(pos, val, node*2, nl, mid); else point_set(pos, val, node*2+1, mid+1, nr); mn[node] = min(mn[node*2], mn[node*2+1]); } ll range_min(int l, int r, int node, int nl, int nr) { if (r < nl || nr < l) return INF; if (l <= nl && nr <= r) return mn[node]; push(node); int mid = (nl+nr)/2; return min(range_min(l, r, node*2, nl, mid), range_min(l, r, node*2+1, mid+1, nr)); } void add_range(int l, int r, ll add) { if (l <= r) range_add(l, r, add, 1, 0, n-1); } void set_point(int pos, ll val) { point_set(pos, val, 1, 0, n-1); } ll query_min(int l, int r) { return range_min(l, r, 1, 0, n-1); } }; int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> N >> D >> Ts >> Tf >> Tw; P.resize(N+1); for (int i=1; i<=N; ++i) cin >> P[i]; if (Tf <= Ts) { cout << 1LL*(N-1)*Tf << '\n'; return 0; } Q.assign(N+1, 0); for (int i=1; i<=N; ++i) Q[i] = Q[i-1] + P[i]; R.resize(N+1); S.resize(N+1); for (int i=1; i<=N; ++i) { R[i] = Q[i-1] - 1LL*D*(i-1); S[i] = Q[i] - 1LL*D*(i-1); } vector<State> states; for (int i=1; i<=N; ++i) { states.push_back({i, 0, R[i]}); states.push_back({i, 1, S[i]}); } sort(states.begin(), states.end()); // 线段树存储每个索引的最小候选值(已减去 T_s*(y-1) 和步行偏移) SegTree seg0(N+2), seg1(N+2); // 初始化 dp[N][0]=dp[N][1]=0 // 插入时,需要对 y 调整:候选值 = dp[y][e'] - T_s*(y-1) - 2Tw*(something) // 具体实现需根据题解公式推导,此处省略细节 // ... // 最终答案 dp[1][0] cout << ans << '\n'; return 0; }注意:上述代码仅为框架,完整实现需要仔细处理偏移量和全局步行计数。原题解已给出完整逻辑,编程时需严格按照公式实现线段树动态维护。
本题是 ICPC 区域赛难题,考察对复杂状态转移的建模、贪心优化及数据结构应用。理解体力变化与步行、游泳、飞行的关系是解题关键。
- 1
信息
- ID
- 7157
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 4
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者