1 条题解
-
0
贸易路线问题详细题解
问题深入分析
本题要求在一棵有个节点的树中找到一条最长的简单路径,且路径的两个端点生产不同的商品。这是一个经典的带约束条件的树直径问题。
问题特点
- 树结构:个城市通过条道路连接形成树结构
- 商品类型约束:每个城市有商品类型,路径两端点必须满足
- 路径长度:路径长度为经过的道路数量,即节点数减1
- 数据规模:,需要高效算法
与经典树直径问题的区别
- 经典问题:找到任意两个节点间的最长路径
- 本题:找到两个商品类型不同的节点间的最长路径
算法核心思想
基本思路
我们可以采用树形动态规划的方法,在计算每个节点的信息时,不仅记录路径长度,还要记录路径端点的商品类型。
状态设计
对于每个节点,我们维护两个关键信息:
first[u] = (len1, type1):在的子树中,从出发到某个叶节点的最长路径长度及其商品类型second[u] = (len2, type2):在的子树中,从出发到某个叶节点的次长路径长度及其商品类型,且type2 ≠ type1
这样设计的原因是:最长路径可能由两个不同分支的路径拼接而成,我们需要知道这些路径的商品类型信息。
详细算法步骤
初始化
val[cur] = {{0, a[cur]}, {-IMX, 0}};- 最长路径:长度为0,商品类型为当前节点类型
- 次长路径:长度为负无穷,表示无效状态
DFS遍历过程
对于节点和其子节点:
1. 答案更新阶段
在合并子树信息前,检查所有可能的路径组合:
// 情况1:u的最长路径 + v的最长路径 if (val[u].first.second != val[v].first.second) ans = max(ans, val[u].first.first + val[v].first.first + 1); // 情况2:u的次长路径 + v的最长路径 else if (val[u].second.second != val[v].first.second) ans = max(ans, val[u].second.first + val[v].first.first + 1); // 情况3:u的最长路径 + v的次长路径 if (val[u].first.second != val[v].second.second) ans = max(ans, val[u].first.first + val[v].second.first + 1); // 情况4:u的次长路径 + v的次长路径 else if (val[u].second.second != val[v].second.second) ans = max(ans, val[u].second.first + val[v].second.first + 1);这里的
+1是因为两条路径通过边连接。2. 状态更新阶段
用子节点信息更新当前节点状态:
// 用v的最长路径更新u if (val[v].first.first + 1 > val[u].first.first) { if (val[u].first.second == val[v].first.second) { // 商品类型相同,替换最长路径 val[u].first = {val[v].first.first + 1, val[v].first.second}; } else { // 商品类型不同,更新最长和次长路径 val[u].second = val[u].first; val[u].first = {val[v].first.first + 1, val[v].first.second}; } } else if (val[v].first.first + 1 > val[u].second.first) { if (val[u].first.second != val[v].first.second) { // 更新次长路径 val[u].second = {val[v].first.first + 1, val[v].first.second}; } } // 用v的次长路径更新u(类似逻辑) if (val[v].second.first + 1 > val[u].first.first) { // ... 类似处理 }算法正确性证明
为什么这样设计状态?
我们需要找到经过某个节点的最长路径,该路径由的两个不同分支的路径拼接而成。维护最长和次长路径可以覆盖所有可能的情况。
为什么记录商品类型?
因为最终路径的两个端点必须商品类型不同。在拼接路径时,我们需要检查两条路径端点的商品类型是否相同。
为什么算法能覆盖所有情况?
对于任意一条满足条件的路径,它必然经过树中的某个节点(可能是端点),且路径可以分解为的两个不同分支的路径。我们的算法会在DFS过程中检查所有这样的节点。
复杂度分析
时间复杂度
- 每个节点被访问一次:
- 每个节点的每个子节点被处理一次:
- 总时间复杂度:
空间复杂度
- 邻接表存储树:
- 状态数组:
- DFS栈空间:(最坏情况)
- 总空间复杂度:
样例详细演示
输入数据
6 1 2 4 4 2 4 5 1 1 2 2 6 1 4 4 3树结构:
1(t=1) / | \ 5 2 4(t=4) (t=2)| | 6 3(t=4) (t=4)DFS执行过程
以节点1为例:
-
初始化:
val[1] = {{0,1}, {-∞,0}} -
处理子节点5:
- 节点5状态:
{{0,2}, {-∞,0}} - 更新答案:类型1≠2,
ans = max(0, 0+0+1) = 1 - 更新状态:
val[1] = {{1,2}, {0,1}}
- 节点5状态:
-
处理子节点2:
- 节点2状态:
{{1,4}, {0,2}}(已处理节点6) - 更新答案:
- 1的最长(1,2) vs 2的最长(1,4):类型2≠4,
ans = max(1, 1+1+1) = 3 - 1的次长(0,1) vs 2的最长(1,4):类型1≠4,
ans = max(3, 0+1+1) = 3
- 1的最长(1,2) vs 2的最长(1,4):类型2≠4,
- 更新状态:
val[1] = {{2,4}, {1,2}}
- 节点2状态:
-
处理子节点4:
- 节点4状态:
{{1,4}, {0,4}}(已处理节点3) - 更新答案:
- 1的最长(2,4) vs 4的最长(1,4):类型相同,跳过
- 1的次长(1,2) vs 4的最长(1,4):类型2≠4,
ans = max(3, 1+1+1) = 3
- 更新状态保持不变
- 节点4状态:
最终答案:3
边界情况考虑
特殊情况1:所有节点商品类型相同
- 题目保证至少有两个不同商品类型的城市
- 如果输入违反保证,算法会返回0(但题目保证不会出现)
特殊情况2:链状结构
- 算法在链状树上同样有效
- 最长路径就是整个链(如果端点商品类型不同)
特殊情况3:星形结构
- 中心节点连接多个叶节点
- 算法会检查中心节点与所有叶节点的组合
算法优化与变种
可能的优化
- 使用引用避免拷贝:在DFS参数中传递引用
- 使用数组代替pair:可能获得轻微性能提升
- 迭代DFS:避免递归栈溢出(但在大多数系统可接受)
相关问题变种
- 多商品类型约束:要求路径端点商品类型属于特定集合
- 权重版本:边有权重,求权重和最大的路径
- 多个约束条件:同时要求商品类型、城市规模等多个条件
代码实现细节
重要常量设置
const int IMX = 1 << 30; // 足够大的负数,表示无效状态使用作为负无穷,避免整数溢出问题。
状态更新顺序
严格按照"先更新答案,再更新状态"的顺序,确保不会重复计算同一路径。
商品类型比较
直接比较整数,因为商品类型用整数表示。
总结
本题通过巧妙的树形DP设计,在经典树直径算法的基础上增加了商品类型约束。核心创新点在于:
- 状态设计:同时维护最长和次长路径及其商品类型
- 答案更新:在合并子树时检查所有可能的路径组合
- 效率保证:时间复杂度和空间复杂度
该算法不仅正确解决了问题,而且在大数据规模下仍能高效运行,体现了树形动态规划在解决带约束路径问题中的强大能力。
- 1
信息
- ID
- 4043
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者