#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 16;
int f[1 << N][N][4], a[N][N];
int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
scanf("%d", &a[i][j]);
}
}
memset(f, 0x3f, sizeof(f));
// 初始化:从每个点开始
for (int i = 0; i < n; i++) {
f[1 << i][i][0] = 0;
}
// 状态转移
for (int i = 0; i < (1 << n); i++) {
for (int j = 0; j < n; j++) {
if ((i >> j) & 1) {
for (int k = 0; k < n; k++) {
int prev_mask = i - (1 << j);
// k必须在之前的mask中
if ((prev_mask >> k) & 1) {
// type 0: 未使用任何加速
f[i][j][0] = min(f[i][j][0], f[prev_mask][k][0] + a[k][j]);
// type 1: 已使用重力加速
f[i][j][1] = min(f[i][j][1], f[prev_mask][k][0]); // 使用重力加速
f[i][j][1] = min(f[i][j][1], f[prev_mask][k][1] + a[k][j]); // 不使用
// type 2: 已使用反重力加速
f[i][j][2] = min(f[i][j][2], f[prev_mask][k][0] + 2 * a[k][j]); // 使用反重力加速
f[i][j][2] = min(f[i][j][2], f[prev_mask][k][2] + a[k][j]); // 不使用
// type 3: 两次都已使用
f[i][j][3] = min(f[i][j][3], f[prev_mask][k][1] + 2 * a[k][j]); // 先用重力,再用反重力
f[i][j][3] = min(f[i][j][3], f[prev_mask][k][2]); // 先用反重力,再用重力
f[i][j][3] = min(f[i][j][3], f[prev_mask][k][3] + a[k][j]); // 两次都用完了
}
}
}
}
}
int res = 1e9;
for (int i = 0; i < n; i++) {
res = min(res, f[(1 << n) - 1][i][3]);
}
printf("%d\n", res);
return 0;
}
星际空间站跳跃问题题解
一、问题分析
1.1 问题本质解读
这道题表面上是"选择n-1条边连通所有空间站",但实际上需要恰好进行n-1次跳跃把所有空间站跳完,这是一个非常关键的区别。
- 如果是"连通所有节点",那是最小生成树问题
- 如果是"恰好n-1次跳跃访问所有节点",这是哈密顿路径问题(Hamiltonian Path)
哈密顿路径要求每个节点恰好访问一次,而n个节点恰好需要n-1条边。这正符合题目要求。
1.2 特殊约束分析
题目要求必须使用两次特殊操作:
- 重力加速:某次跳跃代价变为0
- 反重力加速:某次跳跃代价翻倍(乘2)
目标:最小化总代价
二、算法设计
2.1 状态压缩动态规划
由于n ≤ 16,我们可以用状态压缩DP来解决。定义三维状态:
dp[mask][u][state] = 最小代价
其中:
mask:二进制数,表示已访问的空间站集合(例如 101 表示访问了第0和第2个空间站)u:当前所在的空间站编号(0到n-1)state:特殊操作的使用状态0:未使用任何加速1:已使用重力加速(某次代价归零)2:已使用反重力加速(某次代价翻倍)3:两次都已使用
2.2 状态转移方程
考虑从状态 (mask, u, state) 转移到 (mask | (1<<v), v, new_state),即从空间站u跳到空间站v。
情况1:当前state = 0(未使用任何加速)
// 不使用加速,正常跳跃 dp[new_mask][v][0] = min(dp[new_mask][v][0], dp[mask][u][0] + cost[u][v]) // 使用重力加速(这次跳跃代价为0) dp[new_mask][v][1] = min(dp[new_mask][v][1], dp[mask][u][0]) // 使用反重力加速(这次跳跃代价翻倍) dp[new_mask][v][2] = min(dp[new_mask][v][2], dp[mask][u][0] + 2 * cost[u][v])
情况2:当前state = 1(已使用重力加速)
// 使用反重力加速(这次跳跃代价翻倍) dp[new_mask][v][3] = min(dp[new_mask][v][3], dp[mask][u][1] + 2 * cost[u][v]) // 不使用加速(两次都还没用完,但这里必须用完) // 这个情况需要继续使用反重力加速才能到达state 3
情况3:当前state = 2(已使用反重力加速)
// 使用重力加速(这次跳跃代价为0) dp[new_mask][v][3] = min(dp[new_mask][v][3], dp[mask][u][2])
情况4:当前state = 3(两次都已使用)
// 正常跳跃 dp[new_mask][v][3] = min(dp[new_mask][v][3], dp[mask][u][3] + cost[u][v])
2.3 初始化
从每个空间站开始都可以作为起点:
for (int i = 0; i < n; i++) {
dp[1 << i][i][0] = 0; // 只访问了第i个空间站,且未使用任何加速
}
2.4 答案
ans = min(dp[(1<<n)-1][i][3]) for all i
即在所有空间站都被访问(mask = (1<<n)-1),且两次特殊操作都已使用(state = 3)的情况下,取最小值。
三、代码实现
完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 16;
int f[1 << N][N][4], a[N][N];
int main() {
int n;
scanf("%d", &n);
// 读取代价矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
scanf("%d", &a[i][j]);
}
}
// 初始化所有状态为无穷大
memset(f, 0x3f, sizeof(f));
// 从每个点开始初始化
for (int i = 0; i < n; i++) {
f[1 << i][i][0] = 0;
}
// 状态转移
for (int i = 0; i < (1 << n); i++) {
for (int j = 0; j < n; j++) {
if ((i >> j) & 1) { // 如果第j个空间站在当前mask中
for (int k = 0; k < n; k++) {
int prev_mask = i - (1 << j);
if ((prev_mask >> k) & 1) { // k在之前的mask中
// state 0: 未使用任何加速
f[i][j][0] = min(f[i][j][0], f[prev_mask][k][0] + a[k][j]);
// state 1: 已使用重力加速
f[i][j][1] = min(f[i][j][1], f[prev_mask][k][0]); // 使用重力加速(代价0)
f[i][j][1] = min(f[i][j][1], f[prev_mask][k][1] + a[k][j]); // 不使用
// state 2: 已使用反重力加速
f[i][j][2] = min(f[i][j][2], f[prev_mask][k][0] + 2 * a[k][j]); // 使用反重力加速(代价翻倍)
f[i][j][2] = min(f[i][j][2], f[prev_mask][k][2] + a[k][j]); // 不使用
// state 3: 两次都已使用
f[i][j][3] = min(f[i][j][3], f[prev_mask][k][1] + 2 * a[k][j]); // 先用重力,再用反重力
f[i][j][3] = min(f[i][j][3], f[prev_mask][k][2]); // 先用反重力,再用重力
f[i][j][3] = min(f[i][j][3], f[prev_mask][k][3] + a[k][j]); // 两次都用完了
}
}
}
}
}
// 答案是所有节点都访问且两次操作都使用的最小代价
int res = 1e9;
for (int i = 0; i < n; i++) {
res = min(res, f[(1 << n) - 1][i][3]);
}
printf("%d\n", res);
return 0;
}
四、复杂度分析
时间复杂度
- 状态数量:O(2ⁿ × n × 4) = O(n × 2ⁿ)
- 每个状态的转移:O(n)
- 总时间复杂度:O(n² × 2ⁿ)
当 n = 16 时,约为 16² × 2¹⁶ = 256 × 65536 ≈ 1677万次操作,完全可以接受。
空间复杂度
- DP数组大小:(1 << 16) × 16 × 4 = 65536 × 16 × 4 ≈ 420万个int
- 空间复杂度:O(n × 2ⁿ × 4) = O(n × 2ⁿ)
五、示例分析
示例1
输入:
3 0 2 1 2 0 1 3 1 0
最优解:
- 从空间站0出发
- 跳到空间站2:使用重力加速,代价 = 0(原代价1归零)
- 跳到空间站1:使用反重力加速,代价 = 1 × 2 = 2(原代价1翻倍)
- 总代价:0 + 2 = 2
示例2
输入:
4 0 3 1 4 3 0 2 5 1 2 0 6 4 5 6 0
最优解:
- 从空间站0出发
- 跳到空间站2:使用重力加速,代价 = 0(原代价1归零)
- 跳到空间站1:正常跳,代价 = 2
- 跳到空间站3:使用反重力加速,代价 = 5 × 2 = 10(原代价5翻倍)
- 总代价:0 + 2 + 10 = 12
(这个示例展示了重力加速和反重力加速可以不在相邻的跳跃中使用)
六、关键要点
- 问题本质:这是哈密顿路径问题,不是最小生成树问题
- 状态设计:用第三维表示特殊操作的使用状态
- 转移顺序:按照mask从小到大转移,确保子问题先被计算
- 位运算:熟练使用位运算操作(
<<,>>,&,|)来操作mask - 边界处理:确保只转移合法的边(即k在prev_mask中)
七、优化方向
- 滚动数组:可以使用滚动数组优化空间复杂度到 O(n × 2ⁿ)
- 剪枝:某些状态可以提前剪掉
- 并行计算:对于同一层的mask可以并行计算
不过对于n ≤ 16的约束,当前实现已经足够高效了。

京公网安备 11010502036488号