1 条题解
-
0
G. 网格优化 题解
1. 题意回顾
有一个 的网格图,从 出发,只能向右或向下走,最终到达 。
- 向下走的边:从 到 ,边权为 (,)
- 向右走的边:从 到 ,边权为 (,)
路径经过的边权会被收集到一个集合 中。
目标:最大化 的 。是不属于 的最小非负整数。
约束:,所有测试用例的 之和 ,边权在 范围内。
2. 关键观察
观察 1:
如果 ,说明 都出现在 中,而 不在 中。
因此,我们想判断:能否找一条路径,使得它包含 中的所有数,但不包含 ?观察 2:
由于边权范围是 ,而路径长度固定为 条边(因为从 到 必须向下走 步,向右走 步),所以 的大小恰好是 。这意味着 中元素个数是固定的:。
观察 3:
如果 ,那么 必须包含 这 个不同的数。由于 ,必须有 。
另外, 中不能包含 。因此,问题等价于:对于每个 ,判断是否存在一条路径,使得:
- 所有 都至少出现一次;
- 不出现。
3. 数据范围分析
,路径长度 ,边权范围 。
这个范围非常小,适合用 状态压缩动态规划。我们可以用 位掩码 表示某个数值集合是否已经出现过。
设 表示:从 走到 时,已经收集到的数值集合为 是否可达。
但这样状态数太多:
, 的范围是 ,当 时 太大,不可行。
4. 优化:二分答案 + 可行性判断
因为 最大为 ,我们可以 二分答案:
判断是否存在一条路径,使得 ,即 全部出现在 中。
此时我们只需要关心这 个数是否都出现过,其他数不影响(只要不包含 即可,但二分时我们只要求 全出现,不限制 是否出现,因为二分的是下界)。
5. 可行性 DP
对于固定的 ,我们只关心 这 个数是否出现,其他数统一视为“无关”。
状态压缩:用一个 位二进制数 表示 中哪些数已经出现过。
状态: 表示从 走到 时,已经收集到的数值集合为 是否可达。
转移:从 向右或向下走,遇到边权 :
- 如果 ,则新 ;
- 如果 ,则 (不影响 的收集情况)。
初始状态:(初始集合为空)。
最终判断:是否存在 使得 为真且 包含了所有 位(即 )。
6. 复杂度分析
对于固定的 ,状态数:。
枚举 , 最大 , 太大。
但 实际上不会很大,因为 只有 个元素,而 需要 个不同的数,所以 。
但 仍然太大(),不可行。
7. 进一步优化:meet-in-the-middle
由于 ,路径长度 ,我们可以将路径分成两半:
- 前半段:从 到某个中间位置(如所有满足 的点,即反对角线)。
- 后半段:从该中间位置到 。
枚举所有前半段路径,收集它们得到的数值集合(用位掩码表示)。
然后枚举所有后半段路径,看能否和某个前半段拼接成完整路径,使得 全部出现。这样状态数大大减少:前半段路径条数大约为 ,当 时约为 ,仍然太大,不可直接枚举。
8. 实际可行解法(官方做法)
由于 且边权范围小,官方解法采用 折半枚举 结合 哈希 / 位压缩:
- 将路径分成两段:从起点到中间对角线,再从中间对角线到终点。
- 枚举前半段的所有路径(最多 条,当 时约为 ,可行)。
- 对每条前半段路径,记录它收集到的数值集合 和它的结束位置 (满足 )。
- 对每条后半段路径,记录它收集到的数值集合 和它的起始位置(同样在 上)。
- 对于每个中间位置,检查是否存在前半段和后半段使得 包含 的全部。
这就是 meet-in-the-middle + 位掩码。
9. 二分答案 + meet-in-the-middle 算法步骤
- 读入 和 矩阵。
- 预处理所有从 到对角线 的路径,记录每条路径的:
- 终点 (满足 )
- 收集到的数值集合 (用一个 位的整数表示,但实际只需 位)
- 预处理所有从对角线 到 的路径,记录每条路径的:
- 起点
- 收集到的数值集合
- 对每个 从 到 二分:
- 对于每个中间位置,检查是否存在 和 使得 包含 。
- 可用哈希表或按位置分组优化。
- 找到最大的 使得存在这样的路径。
10. 最终答案
最大可能的 不会超过 ,因为 大小只有 ,最多能包含 ,所以 或 ?
实际上,如果 恰好包含 ,则 ,不可能达到 (因为那样需要包含 共 个数,但只有 条边)。
所以答案范围是 。
11. 复杂度
- 枚举前半段路径:$O(\binom{n-1}{\lfloor (n-1)/2 \rfloor} \cdot (n-1))$
- 枚举后半段路径:$O(\binom{n-1}{\lfloor (n-1)/2 \rfloor} \cdot (n-1))$
- 二分答案
- 总复杂度:$O(\binom{n-1}{\lfloor (n-1)/2 \rfloor} \cdot n \cdot \log n)$,当 时约 ,可接受。
12. 核心代码框架
#include <bits/stdc++.h> using namespace std; int n; vector<vector<int>> d, r; // 对角线上的点:x + y = n + 1 vector<vector<int>> diag; // diag[pos] 存储该点信息 // 枚举所有从 (1,1) 到对角线 x+y=mid+1 的路径 void dfs1(int x, int y, int mask, int len, vector<unordered_map<int,bool>>& dp1) { if (x + y == n + 1) { dp1[(x-1)*n + (y-1)][mask] = true; return; } // 向下走 int w = d[x][y]; int nmask = mask | (1 << w); dfs1(x+1, y, nmask, len+1, dp1); // 向右走 w = r[x][y]; nmask = mask | (1 << w); dfs1(x, y+1, nmask, len+1, dp1); } // 类似地枚举后半段路径... bool check(int k) { // 判断能否收集到 0..k-1 int target = (1 << k) - 1; for (int mid = 0; mid < diag.size(); mid++) { for (auto& [mask1, _] : dp1[mid]) { for (auto& [mask2, _] : dp2[mid]) { if ((mask1 | mask2) == target) return true; } } } return false; } int main() { int t; cin >> t; while (t--) { cin >> n; d.assign(n+1, vector<int>(n+1)); r.assign(n+1, vector<int>(n+1)); // 读入 d 矩阵 (n-1 行) for (int i = 1; i <= n-1; i++) { for (int j = 1; j <= n; j++) { cin >> d[i][j]; } } // 读入 r 矩阵 (n 行) for (int i = 1; i <= n; i++) { for (int j = 1; j <= n-1; j++) { cin >> r[i][j]; } } // meet-in-the-middle 预处理 vector<unordered_map<int,bool>> dp1(n*n), dp2(n*n); dfs1(1, 1, 0, 0, dp1); // 从 (n,n) 反向 dfs 到对角线... // 然后二分答案 int lo = 0, hi = 2*n-2; while (lo < hi) { int mid = (lo + hi + 1) / 2; if (check(mid)) lo = mid; else hi = mid - 1; } cout << lo << "\n"; } return 0; }
- 1
信息
- ID
- 6660
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 4
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者