1 条题解
-
0
F. 旅行售卖猫 详细题解
问题重述
有 个城市,第 个城市有两个参数 。
两个不同城市 之间的道路长度为 。
一条简单路径的代价为路径上相邻城市道路长度之和。
对于每个 ,求恰好经过 个不同城市的所有简单路径中的最小代价。核心观察与排序
定义 。
将城市按照 升序排序,即 。
排序后,对于 ,有 ,因此这个性质使得在排序序列中从左到右的边权容易计算。
问题转化为线性 DP
考虑一条简单路径,将其顶点按在排序后的位置重新排列。可以证明(通过交换论证)存在一条最优路径,其顶点在排序后的序列中构成一个连续区间。
因此,我们只需考虑所有区间 (),长度为 ,并计算以某种顺序经过这些点的最小代价。对于区间 ,有两种可能的顺序:从左到右或从右到左。
- 从左到右:代价为 $a_l + (a_{l+1}+b_l) + (a_{l+2}+b_{l+1}) + \dots + (a_r+b_{r-1})$,简化后为 ?需要仔细推导。
实际上,设排序后点的坐标为 ,并记其 值。对于路径 :
- 边 长度 = (因为 )。
- 总代价 = $\sum_{i=l}^{r-1} (a_{p_i} + b_{p_{i+1}}) = \left(\sum_{i=l}^{r-1} a_{p_i}\right) + \left(\sum_{i=l+1}^{r} b_{p_i}\right)$。
- 记 ,则上式 = 。
类似地,逆序路径 的代价为 。
因此区间 的最优路径代价为
于是问题变成:对每个长度 ,求所有区间 满足 的最小 。
动态规划优化(标程采用的方法)
直接枚举区间 对于 且 已经可行,但标程使用了一种更巧妙的 DP,将每个点依次加入,维护路径的当前状态,从而在 内同时计算所有 的答案。
状态定义
设 表示当前已经考虑了排序后的前若干个点,并已选出了 个点构成一条未完成的路径,其中 表示路径的形态:
- :路径只有一个点(即只确定了起点,没有边)。
- :路径有两个端点,但只记录了 左端点的贡献(实际上状态设计很精巧,需结合转移理解)。
- :路径已经完整(两个端点都已计入),可以用于更新答案。
初始时 ,其余为 。
转移
按排序顺序依次加入新点 (即城市 )。
设当前点为 ,其参数为 。
我们考虑将 加入已有的路径:- 作为新路径的第一个点:
如果路径长度为 ,则起点贡献为 (因为首尾都要算?标程中 ,)。 - 作为路径的中间点:
从任意状态 扩展,新路径长度 ,状态 不变,代价增加 (因为 作为内部点,会贡献 和 各一次)。 - 作为路径的右端点(即终结当前路径):
- 若当前状态为 (只有一个点),则添加 后形成两个点,状态变为 ,代价增加 (右端点的 贡献)。
- 若当前状态为 (已有一个端点),则添加 后形成完整路径,状态变为 ,代价增加 (右端点的 贡献)。
- 同时,当形成完整路径()时,还可以更新答案:(对应路径右端点贡献两次 )。
具体转移方程(用辅助数组 暂存):
- 对于 :
- $tdp[j+1][t] = \min(tdp[j+1][t],\; dp[j][t] + a + b)$(插入中间)
- (从单点扩展)
- (从两个端点之一扩展成完整路径)
- 更新答案:
最后将 合并到 ,继续下一个点。
算法步骤
- 读入 。
- 对于每个测试用例:
- 读入 及所有 。
- 按 升序排序。
- 初始化 、 为无穷大, 为无穷大。
- 对每个点 执行上述转移。
- 输出 。
复杂度
- 排序 。
- DP 共 个点,每个点内循环 ,总 。
- 空间 。
- 满足 。
代码实现(标程原样,添加注释)
#include<bits/stdc++.h> using namespace std; struct apos { long long a, b; // 按 b-a 升序排序 friend bool operator<(apos x, apos y) { return x.b - x.a < y.b - y.a; } } ap[3000]; long long dp[3001][3], tdp[3001][3], ans[3001]; const long long INF = 1e18; int main() { ios::sync_with_stdio(false), cin.tie(0); int T; cin >> T; while (T--) { int n; cin >> n; for (int i = 0; i < n; ++i) cin >> ap[i].a >> ap[i].b; sort(ap, ap + n); // 初始化 for (int i = 0; i <= n; ++i) { for (int j = 0; j < 3; ++j) dp[i][j] = tdp[i][j] = INF; ans[i] = INF; } for (int i = 0; i < n; ++i) { long long a = ap[i].a, b = ap[i].b; // 当前点作为第一个点 tdp[1][0] = min(tdp[1][0], 2 * a); tdp[1][1] = min(tdp[1][1], a); for (int j = 1; j < n; ++j) { // 插入中间 for (int t = 0; t < 3; ++t) tdp[j+1][t] = min(tdp[j+1][t], dp[j][t] + a + b); // 作为右端点扩展 tdp[j+1][1] = min(tdp[j+1][1], dp[j][0] + b); tdp[j+1][2] = min(tdp[j+1][2], dp[j][1] + a); // 更新答案(形成完整路径) ans[j+1] = min(ans[j+1], dp[j][1] + b); ans[j+1] = min(ans[j+1], dp[j][2] + 2 * b); } // 将 tdp 合并到 dp for (int j = 1; j <= n; ++j) { for (int t = 0; t < 3; ++t) { dp[j][t] = min(dp[j][t], tdp[j][t]); tdp[j][t] = INF; } } } // 输出答案 for (int i = 2; i <= n; ++i) cout << ans[i] << " \n"[i == n]; } return 0; }正确性说明
该 DP 实质上是模拟了逐步构建最优路径的过程。排序保证了当新点加入时,与已有路径中点的边权可以简单表示为 或 的形式。通过三种状态 分别记录路径的端点情况,可以无遗漏地枚举所有可能的路径构造方式。最终答案数组 记录的是所有长度为 的路径的最小代价。
示例验证(第一个用例)
输入:
3 0 2 2 1 3 3排序后为 。运行 DP 得到 ,与题目输出一致。
- 1
信息
- ID
- 7257
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者