1 条题解
-
0
「奶吧多饮」之旅 题解
题目分析
我们需要在一棵树中找到从节点 1 到节点 n 的哈密尔顿路径(访问所有节点恰好一次),且相邻访问的节点在树上的距离不超过 2。
核心思路
1. 问题转化
将城市结构看作一棵树:
- 节点:路口(编号 1 到 n)
- 边:连接路口的街道
- 目标:找到从 1 到 n 的哈密尔顿路径,满足相邻节点距离 ≤ 2
2. 关键观察
- 距离约束:相邻访问节点在树上的距离 ≤ 2,意味着在遍历时不能"跳过"太多节点
- 关键路径:从 1 到 n 的路径是固定的,必须按特定顺序访问
- 子树分类:根据子树大小和是否在关键路径上,采用不同的遍历策略
算法详解
数据结构设计
struct Edge { int to, nxt; } Graph[MAXN << 1]; std::vector<int> son[MAXN]; // 存储每个节点的子节点 int siz[MAXN]; // 子树大小 bool imp[MAXN]; // 标记是否在关键路径上 int ans[MAXN]; // 存储最终路径主要步骤
步骤1:预处理 (
Init函数)void Init(const int &u, const int &fa) { siz[u] = 1, imp[u] = (u == N); // 标记终点N for (遍历u的所有子节点v) { Init(v, u); siz[u] += siz[v]; imp[u] |= imp[v]; // 传播关键路径标记 son[u].push_back(v); } // 对子节点排序:关键路径节点排在最后,其他按子树大小升序 std::sort(son[u].begin(), son[u].end(), [](int a, int b) { if (imp[b]) return true; if (imp[a]) return false; return siz[a] < siz[b]; }); }预处理阶段:
- 计算每个节点的子树大小
- 标记从 1 到 n 的关键路径
- 对子节点进行智能排序,为后续遍历做准备
步骤2:主要DFS遍历 (
DFS1函数)处理三种不同的到达情况:
情况1:当前节点是终点 N
if (u == N) { if (d == 0 && siz[u] > 1) // 从N出发但还有未访问子节点 Dead; // 无解 DFS2(u, fa, cur + dir * (siz[u] + 1), -dir); }情况2:从父节点到达当前节点 (d=1)
- 最多允许 3 个"大子树"(大小 > 1)
- 按特定顺序:小子树 → 大子树 → 关键路径子树
情况3:从当前节点出发 (d=0)
- 最多允许 2 个"大子树"
- 按特定顺序:当前节点 → 小子树 → 大子树 → 关键路径子树
步骤3:辅助遍历 (
DFS2函数)void DFS2(const int &u, const int &fa, int cur, const int &dir) { ans[cur += dir] = u; // 记录当前节点 int n = son[u].size(), sml = 0, lrg; // 统计小子树和大子树数量 rep(i, 0, n-1) sml += (siz[son[u][i]] == 1); lrg = n - sml; if (lrg >= 2) Dead; // 大子树过多,无解 if (lrg) { // 先处理唯一的大子树 DFS2(son[u][n-1], u, cur + dir * (siz[son[u][n-1]] + 1), -dir); cur += dir * siz[son[u][n-1]]; } // 最后处理所有小子树 rep(i, 0, sml-1) ans[cur += dir] = son[u][i]; }这个函数负责完整遍历一个子树,使用方向参数
dir灵活控制遍历顺序。关键技巧
-
子树分类策略:
- 小子树:大小为 1 的子树,可以灵活安排
- 大子树:大小 > 1 的子树,需要特殊处理
- 关键路径子树:包含终点 n 的子树,必须最后处理
-
方向控制:
- 使用
dir参数(+1 或 -1)控制遍历方向 - 实现正序和逆序遍历的灵活切换
- 使用
-
可行性剪枝:
- 当发现大子树数量超过限制时立即终止
- 确保路径的连续性约束
复杂度分析
- 时间复杂度:O(n) - 每个节点被处理一次
- 空间复杂度:O(n) - 存储树结构和结果数组
学习要点
- 树形结构的智能遍历:根据子树特性采用不同的遍历策略
- 约束条件的转化:将"距离不超过2"转化为子树数量的限制
- 构造性算法设计:在遍历过程中动态构建解
- 分类讨论思想:针对不同情况(到达方向、节点类型)采用不同处理逻辑
代码实现建议
- 模块化设计:将不同功能的DFS分离,提高代码可读性
- 早期剪枝:在发现无解情况时立即终止,避免不必要的计算
- 状态封装:通过参数明确传递状态信息(如方向、深度等)
- 1
信息
- ID
- 3770
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者