1 条题解
-
0
题目理解
我们有一个 的三维立方体,需要选择 个数,满足:
- 任意两个选择的数不能有相同的 坐标
- 任意两个选择的数不能有相同的 坐标
- 任意两个选择的数不能有相同的 坐标
这意味着选择的 个数恰好占据 个不同的 、 个不同的 、 个不同的 。换句话说,我们是在寻找一个 三维匹配:选择 个位置 ,使得每个 恰好出现一次、每个 恰好出现一次、每个 恰好出现一次。
这等价于在一个 的矩阵中,每个格子对应一个 维向量( 维),我们需要从每个 和 的组合中选一个 ,并且 不能重复。
问题转化
设 表示立方体中位置 的值。
我们需要选择 个位置 使得:
最小化 。
这是一个三维分配问题(3-dimensional assignment problem),是 NP-hard 问题。但由于 ,我们可以使用状态压缩动态规划来解决。
算法思路
由于 ,我们可以枚举 的排列。固定 的顺序后,问题转化为:对于每个 ,我们需要分配一个 对,使得所有 和所有 都恰好使用一次。
更具体地说:
- 按 从 到 依次考虑
- 维护两个布尔数组(或位掩码)表示哪些 和哪些 已经被使用
- 对于当前的 ,我们可以选择任意未被使用的 和 ,加上对应的值
- 使用 DP: 表示已经处理完前 个 (其中 等于 或 中 1 的个数),且已使用的 集合为 ,已使用的 集合为 时的最小和
由于 , 和 各有 种状态,总状态数为 ,对于每个状态我们可能需要遍历 种选择,总复杂度约为 $16.7 \times 10^6 \times 144 \approx 2.4 \times 10^9$,在 C++ 中优化后可能勉强通过。
优化
我们可以进一步优化:
注:$k = \text{popcount}(mask_y) = \text{popcount}(mask_z)$,因为我们已经处理了 个 ,所以 和 必须有相同数量的 1。这可以减少状态数。
更常见的做法是只用一个 ,然后对于每个 ,我们枚举所有未使用的 (通过 ),但 的分配需要另外处理。实际上,我们可以将问题看作:选择 个 对,每个 和 恰好出现一次,且每个 对应一个不同的 对。
我们可以预先计算 ,然后使用匈牙利算法的变种?不行,这是三维问题。
更好的 DP 状态定义:
- 令 表示已经考虑了前 $k = \text{popcount}(mask_y) = \text{popcount}(mask_z)$ 个 时的最小和
- 转移时,我们考虑第 个 (即 ),枚举所有 不在 中、 不在 中的组合,进行转移
复杂度:状态数 ?不,仍然是 ,但每个状态转移需要 ,总复杂度 ,当 时,,需要优化。
进一步优化
我们可以采用最小费用最大流的思路,或者使用DP + 预处理:
对于固定的 和 , 是确定的。我们可以预处理所有可能的 对,只考虑 的状态,状态数约为 ,当 时,这个和约为 (根据中心二项式系数近似)。
转移时,我们不需要枚举所有 种 对,可以枚举所有不在 中的 ,然后对于每个这样的 ,枚举所有不在 中的 。但这样仍然是 。
我们还可以使用位运算枚举子集的技巧,但这里枚举的是补集,无法直接优化。
实现方案
由于 时状态数约为 ,每个状态转移约 ,总运算量约 ,在 C++ 中经过优化(使用数组、位运算、预编译)可以 3 秒内通过。
C++ 代码
#include <bits/stdc++.h> using namespace std; const int INF = 2e9; int n; int a[13][13][13]; int dp[1 << 12][1 << 12]; int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n; // 读入数据 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { for (int k = 1; k <= n; k++) { cin >> a[i][j][k]; } } } // 初始化 DP 数组 int full_mask = (1 << n) - 1; for (int i = 0; i <= full_mask; i++) { for (int j = 0; j <= full_mask; j++) { dp[i][j] = INF; } } dp[0][0] = 0; // DP 转移 for (int mask_y = 0; mask_y <= full_mask; mask_y++) { int x = __builtin_popcount(mask_y); // 当前处理的 x 编号(0-based 或 1-based) // 注意:我们的 x 从 1 到 n,但这里 x 表示已经处理了 x 个层(即 x 从 0 到 n-1) if (x >= n) continue; for (int mask_z = 0; mask_z <= full_mask; mask_z++) { if (__builtin_popcount(mask_z) != x) continue; if (dp[mask_y][mask_z] == INF) continue; int cur = dp[mask_y][mask_z]; int remaining_y = full_mask ^ mask_y; int remaining_z = full_mask ^ mask_z; // 枚举所有未使用的 y 和 z // 为了优化,我们可以枚举 y,然后枚举 z for (int y = 0; y < n; y++) { if (!(remaining_y >> y & 1)) continue; for (int z = 0; z < n; z++) { if (!(remaining_z >> z & 1)) continue; int new_mask_y = mask_y | (1 << y); int new_mask_z = mask_z | (1 << z); int new_val = cur + a[x + 1][y + 1][z + 1]; if (new_val < dp[new_mask_y][new_mask_z]) { dp[new_mask_y][new_mask_z] = new_val; } } } } } cout << dp[full_mask][full_mask] << "\n"; return 0; }时间复杂度分析
- 状态数:约 $\sum_{k=0}^n \binom{n}{k}^2 \approx \binom{2n}{n} \approx 2.7 \times 10^6$(当 时)
- 每个状态转移:
- 总运算量:约 次操作,在 C++ 优化下可在 3 秒内完成
空间复杂度
$O(2^{2n}) = O(4096 \times 4096 \times 4 \text{ bytes}) \approx 64 \text{ MB}$,在限制内。
- 1
信息
- ID
- 7010
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 7
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者