环形游乐场中的最优游览路线(带时间窗口和收益)
题目描述
游乐场是一个环形道路,共有 \(n\) 个景点,编号从 \(0\) 到 \(n-1\)(景点 \(n-1\) 的下一个景点是 \(0\))。环形道路的长度为 \(L\),相邻景点 \(i\) 和 \((i+1)\bmod n\) 之间的距离为 \(d_i\)(已知,且 \(\sum_{i=0}^{n-1} d_i = L\))。
每个景点 \(i\) 有一个参观收益 \(v_i\) 和一个参观所需时间 \(t_i\)。你从任意一个景点出发,以单位速度沿环形道路行走(可顺时针或逆时针),到达某个景点的瞬间即可选择是否参观,若参观则花费时间 \(t_i\) 并获得收益 \(v_i\)(参观期间不能移动)。
你总共有 \(T\) 单位的时间(包括行走和参观时间),要求在时间 \(T\) 结束时回到起点景点。问在时间限制内,能获得的最大总收益是多少?
输入说明
- 整数 \(n\)(\(2 \leq n \leq 200\))
- 整数 \(T\)(\(1 \leq T \leq 1000\))
- 数组 \(d[0..n-1]\)(\(1 \leq d_i \leq 100\))
- 数组 \(v[0..n-1]\)(\(1 \leq v_i \leq 1000\))
- 数组 \(t[0..n-1]\)(\(1 \leq t_i \leq 100\))
输出说明
一个整数,表示最大总收益。
示例(简化)
\(n=4, T=10\)
\(d = [2,3,1,4]\)
\(v = [5,8,3,6]\)
\(t = [1,2,1,2]\)
一种可行方案:从景点0出发,顺时针走到景点1(距离2),参观景点1(时间2,收益8),走到景点2(距离3),参观景点2(时间1,收益3),返回景点0(距离1+4=5)。总时间 = 行走(2+3+5)=10,参观(2+1)=3,总时间13>10?注意:必须保证行走+参观总时间 ≤ T,且结束时回到起点。
可见该示例中时间紧张,需精心选择景点。最优解需计算得出。
解题步骤(循序渐进)
步骤1:问题拆解与难点
- 环形处理:起点任意,但必须返回起点,构成一个环。
- 方向选择:可顺时针或逆时针走,但一旦选定方向,不能“调头”(因为时间是连续的,调头等价于提前折返,可通过状态设计包含)。
- 时间窗口:每个景点只能在到达时刻选择是否参观,且参观花费时间。
- 目标:在总时间 ≤ T 的条件下,最大化参观景点的收益之和。
步骤2:化环为链
常见技巧:由于起点任意,可枚举起点,然后破环成链。
但环形+双向移动,使得破环后仍需考虑两个方向出发的情况。
更优方法:将环形复制成两倍长度的链,然后考虑从任意起点出发,在长度为 \(n\) 的窗口内完成一个“回路”。
但本题要求必须返回起点,等价于从起点出发,走若干景点后沿原路返回或绕一圈返回,实际上可视为:
- 从起点 \(s\) 出发,顺时针走一段(可能参观若干景点),然后返回起点;
- 或逆时针走一段,然后返回起点;
- 或顺时针走一段后再逆时针走另一段(但最终要返回起点,这等价于从起点分两个方向走,最终汇合在起点)。
然而,更简单的理解是:最终回到起点意味着总行走距离一定是环长 \(L\) 的整数倍?不一定,因为可以中途参观,走的路程可小于一圈或多圈。实际上,如果只在一个方向上走并返回,则总行走距离是两倍的“单程距离”。
为了简化,我们考虑从起点出发,只在一个方向上移动(顺时针或逆时针),并允许中途参观,最后原路返回起点。这样,总行走距离是“到达的最远景点与起点的距离”的两倍。
类似地,可以逆时针走另一侧。
更一般的情况是:从起点出发,顺时针走一段参观一些景点,再逆时针走另一段参观一些景点,最后回到起点,这等价于在起点分两个方向走,总行走距离是“顺时针最远距离 + 逆时针最远距离”的两倍。
为了统一,我们可以将环形展开成链,并枚举起点,然后考虑从起点向左(逆时针)和向右(顺时针)两个方向延伸的行程,但最后要回到起点,所以两个方向走的路径是对称的(去和回)。
但这样设计状态会复杂。一个更简洁的通用DP方法:区间DP,状态表示在某个环形区间上完成游览并返回起点的最大收益**。
步骤3:区间DP状态设计
我们定义:
\(dp[l][r][time]\) 表示在环上从景点 \(l\) 出发,顺时针走到景点 \(r\)(闭区间 [l, r] 这段弧),并且最终回到景点 \(l\),总时间消耗为 time 的条件下,能获得的最大收益。
这里“最终回到起点 l”意味着:我们从 l 出发,顺时针参观 [l, r] 中的一些景点(每个最多一次),最后沿原路返回到 l。
行走距离 = 从 l 到 r 的顺时针距离 \(\text{dist}(l, r)\) 的两倍(因为去和回)。
参观时间是参观的景点的时间之和。
总时间 = 行走距离 + 参观总时间。
同理,可以定义逆时针的类似状态,但通过对称性,我们只需处理顺时针方向,然后在合并两个半环时计算总收益。
但题目中,我们可以从任意起点出发,走任意方向,最终回到起点,相当于选择环上一段连续的弧(可能走多段,但最终要回到起点,所以游览的景点集合是环上一段或多段,但多段的情况可视为两段:起点左右各一段)。
事实上,最优解一定是在起点两侧各选一段(可能为空),顺时针走右侧一段(并返回),逆时针走左侧一段(并返回),总时间相加 ≤ T。
那么我们可以分别计算:
- 对每个起点 s,计算从 s 出发,顺时针走一段区间 \([s, s+k]\) 并返回 s 的“时间-收益”表(类似背包)。
- 对每个起点 s,计算从 s 出发,逆时针走一段区间 \([s-k, s]\) 并返回 s 的“时间-收益”表。
然后将顺时针和逆时针的两个表组合,得到从 s 出发的总收益。
步骤4:子问题求解(单方向区间DP)
我们先解决顺时针方向的问题:
定义 \(f[l][r][time]\) 表示从 l 出发,顺时针走到 r(闭区间),参观此区间内一些景点(可不全参观),最后停在 r(不返回 l),总时间恰好为 time 的最大收益。
为什么最后停在 r?因为这样方便递推:从小区间 [l, r] 扩展,可以是由 [l, r-1] 走到 r 并决定是否参观 r。
但我们要的是“返回起点”的情况,所以可以用 \(f\) 来计算:从 l 出发走到 r 并返回 l 的时间 = 走到 r 的时间 + 从 r 返回 l 的行走时间(距离是 \(\text{dist}(l, r)\)),注意返回途中不能参观新景点。
因此,从 l 到 r 并返回 l 的总时间 = time + \(\text{dist}(l, r)\),收益 = \(f[l][r][time]\)。
这里 time 是“去程+参观”的时间,不包括返程行走时间。
步骤5:\(f[l][r][time]\) 的递推
考虑区间 [l, r],最后访问的景点是 r(因为我们是顺时针走,最后到的景点一定是 r)。
那么,到达 r 之前,我们可能从 l 直接走到 r,也可能在中间某些景点停留参观。但为了 DP 方便,我们考虑最后一步是从某个中间点 m 走到 r。
但这样会涉及“在区间内跳着参观”的问题,DP 状态需要知道上一次参观的位置,状态会变多。
更常用的方法:将状态设计为 \(f[l][r][k]\),表示在区间 [l, r] 内,只参观区间内的某些景点,且最后参观的景点是 l 或 r**,总参观时间为 k 的最大收益。
这是区间 DP 常见思路:当我们从小区间扩展到大区间时,新增的点是 l-1 或 r+1,并且我们可以选择是否参观它。
但为了简化,我们采用另一种思路:将行走距离和参观时间一起考虑,直接算总时间。
设 \(dp[l][r]\) 表示从 l 出发顺时针走到 r 并返回 l 的最小时间(或给定时间内的最大收益),但这种形式不易同时处理收益最大化。
为了同时优化收益和时间,我们使用 DP 计算“在给定时间上限内,从 l 出发走到 r 并返回 l 的最大收益”。
具体地,用 \(F[l][r][t]\) 表示从 l 出发,在区间 [l, r] 内参观(可任意选择),最后返回 l,总行走+参观时间恰好为 t 的最大收益。
转移时,考虑最后一个参观的景点是 j(l ≤ j ≤ r):
我们从 l 走到 j(距离 \(\text{dist}(l, j)\)),参观 j(时间 \(t_j\)),然后从 j 走到 l(距离 \(\text{dist}(l, j)\)),但这之间在 j 之前可能还参观了其他景点。
可以把路径拆成:从 l 出发,在 [l, j-1] 内参观并返回 l,然后从 l 到 j,参观 j,返回 l。
但这样会重复计算 l 到 j 的路径两次(实际上从 l 到 j-1 并返回,再从 l 到 j 并返回,路径重叠,行走距离会多算)。
所以这样不行。
步骤6:正确区间 DP 定义
定义 \(dp[l][r][side][time]\):
- \(side=0\) 表示当前在区间左端点 l
- \(side=1\) 表示当前在区间右端点 r
- time 是已用时间
- 值表示最大收益
意义:我们考虑区间 [l, r] 内的景点,当前在左端点(或右端点),已经参观了这个区间内的某些景点(不一定是全部),并且当前位置是 l 或 r,已用时间 time,能得到的最大收益。
初始时,区间只有一个点 i,我们可以选择参观或不参观 i。
递推:从小区间 [l, r] 扩展到 [l-1, r] 或 [l, r+1]。
例如,从 [l, r] 且当前在 l,要扩展到 l-1,则有两种情况:
- 从 l 走到 l-1(行走时间 \(d_{l-1}\)),不参观 l-1,则状态变为 [l-1, r] 且当前位置在 l-1(因为 l-1 是新区间的左端点),时间增加 \(d_{l-1}\)。
- 走到 l-1 并参观,则时间增加 \(d_{l-1} + t_{l-1}\),收益增加 \(v_{l-1}\)。
类似地,从当前位置在 l,走到 r+1 也是类似,但要注意行走方向是顺时针还是逆时针,距离计算要用环形距离。
但这样状态是 \(O(n^2 \cdot T)\),n≤200, T≤1000 可以接受。
步骤7:环形处理
将环复制成两倍长度链:景点 0..n-1, 再接上 0..n-1。
然后枚举起点 s,在链上取区间 [s, s+n-1],在这个区间上做 DP,但我们只关心从 s 出发并回到 s 的情况,且总时间 ≤ T。
在这个区间上,我们设 \(dp[l][r][side][time]\) 表示在链区间 [l, r] 上,当前位置在端点(l 或 r),已用时间 time 的最大收益。
初始化:对于每个点 i 作为起点,不参观任何点时,dp[i][i][0][0] = 0(在左端点即右端点)。
转移:
- 从 dp[l][r][0] 扩展到 l-1:
走到 l-1 不参观:新时间 = time + dist(l-1, l),新状态 dp[l-1][r][0][新时间] 收益不变。
走到 l-1 参观:新时间 = time + dist(l-1, l) + t[l-1],新状态 dp[l-1][r][0][新时间] 收益 + v[l-1]。 - 从 dp[l][r][0] 扩展到 r+1:
走到 r+1 不参观:新时间 = time + dist(l, r+1)? 注意,从 l 到 r+1 的行走距离是 dist(l, r+1),但 l 到 r 已经走过一些路径,实际上,当前位置在 l,要走到 r+1,必须经过 r,所以我们应走 l→r→r+1 吗?不对,因为区间 [l, r] 内的景点我们已经访问过一部分,但路径可能不是连续走的,我们只是记录了当前位置在 l,可能之前是从 r 走到 l 的,所以 l 到 r+1 必须沿环走,距离是 min(顺时针距离, 逆时针距离)。但这里我们只允许在区间端点向外扩展,所以从 l 到 r+1 应走 l→l-1→...→r+1 逆时针,或者 l→r→r+1 顺时针,取决于环形。
为了避免方向混乱,我们固定顺时针方向为正方向,距离 \(\text{dist}(i, j)\) 表示从 i 顺时针走到 j 的距离(j 可能在 i 后面或前面,我们用模运算处理)。
在链 [0, 2n-1] 上,我们只考虑区间长度 ≤ n 的情况,这样顺时针方向是 l 到 r 是递增的,不会绕圈。
那么,在链区间 [l, r] 上,从 l 到 r+1 的行走距离就是 d[r](相邻距离)。从 r 到 l-1 的行走距离是 d[l-1](注意方向,这里是从 r 逆时针走到 l-1,距离是 d[r] 吗?不,逆时针从 r 到 l-1 的距离是链上 r 到 l-1 的顺时针距离的补集,但因为我们是在链上区间扩展,l≤r,从 r 到 l-1 是向左走,距离是 d[r-1]? 实际上,从 r 到 r-1 的距离是 d[r-1],但这里 r 到 l-1 是多个距离之和。
为了简化,我们只允许从当前端点走到紧邻区间外的第一个点,即从 l 走到 l-1,或从 r 走到 r+1,这样行走距离就是相邻距离 d[l-1] 或 d[r]。
这样,DP 转移就统一了。
步骤8:转移方程
设相邻距离数组为 \(d[i]\) 表示 i 到 i+1 的距离。
状态 dp[l][r][0][time] 表示在区间 [l, r],当前在 l 点,已用时间 time 的最大收益。
状态 dp[l][r][1][time] 表示在区间 [l, r],当前在 r 点。
初始化:对每个 i,dp[i][i][0][0] = 0, dp[i][i][1][0] = 0。
转移:
- 从 dp[l][r][0] 扩展:
a) 走到 l-1 不参观:
time2 = time + d[l-1]
if time2 ≤ T: dp[l-1][r][0][time2] = max(自身, dp[l][r][0][time])
b) 走到 l-1 参观:
time2 = time + d[l-1] + t[l-1]
if time2 ≤ T: dp[l-1][r][0][time2] = max(自身, dp[l][r][0][time] + v[l-1]) - 从 dp[l][r][0] 走到 r+1:
但当前在 l,要走到 r+1 必须先经过 r,所以不能直接走到 r+1。因此,从端点 0 只能走到 l-1,从端点 1 只能走到 r+1。
类似地,从 dp[l][r][1] 扩展:
a) 走到 r+1 不参观:
time2 = time + d[r]
if time2 ≤ T: dp[l][r+1][1][time2] = max(自身, dp[l][r][1][time])
b) 走到 r+1 参观:
time2 = time + d[r] + t[r+1]
if time2 ≤ T: dp[l][r+1][1][time2] = max(自身, dp[l][r][1][time] + v[r+1])
注意:参观景点时,收益加一次,时间加参观时间。
步骤9:计算答案
我们想要从起点 s 出发,最后回到起点 s。
在 DP 状态中,我们可能最后停在 l 或 r。
要回到 s,必须保证当前区间包含 s,且从当前位置走回 s 的时间加上已用时间 ≤ T。
但这样要额外算回程时间,比较麻烦。
更直接的方法:在 DP 中增加一维,表示是否回到过起点,但那样状态太多。
一个技巧:将起点 s 复制到链的中间位置,然后我们只取区间长度为 n 的窗口,计算从 s 出发并回到 s 的最大收益。
实际上,我们可以枚举起点 s,在链上取区间 [s, s+n-1],初始化 dp[s][s][0][0]=0,然后做区间扩展 DP,最后在所有状态中,如果当前位置是 p,要回到 s,需要额外时间 walk(p, s),总时间 ≤ T 则更新答案。
但这样要算很多次,复杂度 O(n^3·T) 可能太大。
优化:我们可以用 DP 计算从 s 出发,顺时针走一段并返回 s 的收益表,以及逆时针的收益表,然后合并。
步骤10:最终算法框架
- 复制环成链,长度 2n。
- 预处理顺时针距离前缀和,方便计算 dist(i, j)。
- 对每个起点 s (0 ≤ s < n):
a) 计算顺时针收益表 g1[t]:从 s 出发,顺时针走一段区间并返回 s,在时间 t 内的最大收益。
这可以通过区间 DP 在链区间 [s, s+n-1] 上计算,但只允许向右(顺时针)走,并必须返回 s。
可以定义 f[i][time] 表示从 s 顺时针走到 i 并返回 s 的最大收益,用背包思想做。
转移:f[i][time] = max(f[i-1][time - 2dist(s,i) - t[i]] + v[i], f[i-1][time - 2dist(s,i)]) 等。
b) 同理计算逆时针收益表 g2[t]。
c) 合并 g1 和 g2:枚举时间 t1 给顺时针,t2 给逆时针,t1+t2 ≤ T,收益 = g1[t1] + g2[t2],取最大值。 - 所有 s 的最大值即为答案。
由于篇幅限制,这里给出关键递推:
设 \(f[i][t]\) 表示从 s 顺时针走到 i(i ≥ s),并返回 s,总时间 ≤ t 的最大收益。
则:
\(f[i][t] = \max( f[i-1][t], \; f[i-1][t - 2*d(s,i) - t[i]] + v[i] )\)
其中 d(s,i) 是 s 到 i 的顺时针距离。
注意:这里假设在 i 参观,则去程距离 d(s,i),参观时间 t[i],返程距离 d(s,i),总行走参观时间 = 2*d(s,i) + t[i]。
另外,可能在 i 不参观,则 f[i][t] 至少为 f[i-1][t]。
但这样只考虑了最后一站是 i 的情况,实际上中间可以参观多个点,所以这实为分组背包:每个点 i 是一个物品,重量 = 2*d(s,i) + t[i],价值 = v[i],但必须按顺序访问,所以用 DP 更新。
具体实现时,用一维背包 DP,对 i 从 s 到 s+n-1,时间从 T 到 0 倒序更新。
同理逆时针方向。
合并时,注意起点 s 的参观收益只能算一次,所以初始化 g1[0]=0, g2[0]=0,但合并时如果两边都包含 s 的收益,要减去重复的。可以在初始化时,先单独处理起点 s 的参观。
步骤11:总结
这道题是环形区间 DP 与背包问题的结合。
核心是将环形问题转化为:从起点出发,顺时针一段、逆时针一段,最后回到起点。
分别用背包 DP 计算两个方向的“时间-收益”表,然后合并。
时间复杂度 O(n^2·T),在给定数据范围可行。