1 条题解
-
0
题目概述
有 个嫌疑人,通过 条线索连接成一棵树。每条线索连接两个嫌疑人,有权重 。每个嫌疑人节点上有若干贴纸(数字),数量等于该节点的度数。
线索按时间顺序出现,每次出现线索 时:
- 在 和 之间连边,权重为
- 在 上贴数字 ,在 上贴数字
- 必须满足
已知最终的树结构和每个嫌疑人所有的贴纸数字(按从上到下的顺序),要求判断是否存在合法的线索出现顺序,如果存在则输出一种可能的顺序。
问题分析
关键约束
- 线索条件:
- 贴纸顺序:贴纸从上到下对应线索出现的时间顺序
- 树结构:最终形成一棵树
问题难点
- 需要为每条边分配两个端点的贴纸
- 贴纸的分配必须满足时间顺序约束
- 需要重建整个时间线
算法思路
核心思想:自底向上的贪心匹配
步骤1:建立树结构
- 用邻接表存储树
- 记录每条边的权重和编号
步骤2:DFS遍历树
从叶子节点向根节点处理,对于每个节点 :
- 收集所有子节点的"剩余需求"
- 将当前节点的贴纸与连接的边进行匹配
步骤3:匹配规则
对于节点 ,设其度数为 ,有 个贴纸 (按时间顺序)。
对于每条边 ,设其权重为 :
- 如果 是 的子节点,已知 给 的"上传值"
- 那么 需要提供的值为
将所有的需求排序,将贴纸排序,进行贪心匹配。
步骤4:构建时间依赖图
根据匹配结果,构建边之间的时间先后关系,然后拓扑排序输出顺序。
算法详解
数据结构
vector<tuple<int, int, int>> e(n - 1); // 边列表 (u, v, w) vector<vector<tuple<int, int, int>>> g(n); // 邻接表 (邻居, 权重, 边编号) vector<vector<int>> c(n); // 每个节点的贴纸 vector<vector<int>> l(n); // l[u][i] = 贴纸i对应的边编号DFS匹配过程
function<void(int, int)> dfs = [&](int u, int f) { // 先递归处理所有子节点 for (auto [i, w, n] : g[u]) if (i != f) dfs(i, u); // 准备匹配 vector<pii> s(c[u].size()), o(c[u].size()); // o: 贴纸排序 (值, 原始索引) for (int i = 0; i < c[u].size(); i++) o[i] = {c[u][i], i}; sort(o.begin(), o.end()); if (u != 0) { // 非根节点 int p = 0; // s: 需求列表 (需求值, 边编号) for (auto [i, w, n] : g[u]) { if (i != f) s[p++] = {w - up[i], n}; else s[c[u].size() - 1] = {0, n}; // 父边的需求特殊处理 } sort(s.begin(), prev(s.end())); // 排序前d-1个需求 // 贪心匹配 int d = 0; for (int i = 0; i < c[u].size() - 1; i++) { if (o[i + d].first < s[i].first) { if (d) return false; // 无法匹配 d = 1; up[u] = o[i].first; // 记录上传值 ps[u] = o[i].second; // 记录使用的贴纸 if (o[i + d].first < s[i].first) return false; // 仍然不满足 } l[u][o[i + d].second] = s[i].second; // 记录匹配关系 } if (!d) { // 如果还没使用上传值 up[u] = o.back().first; ps[u] = o.back().second; } l[u][ps[u]] = s.back().second; // 父边匹配最后一个贴纸 } else { // 根节点特殊处理 // 根节点的所有边都是子边 for (int i = 0; i < c[u].size(); i++) { auto [v, w, n] = g[u][i]; s[i] = {w - up[v], n}; } sort(s.begin(), s.end()); for (int i = 0; i < c[u].size(); i++) { if (o[i].first < s[i].first) return false; // 无法匹配 l[u][o[i].second] = s[i].second; } } };时间顺序构建
// 构建依赖图:贴纸顺序意味着边的时间顺序 vector<vector<int>> g2(n - 1); // 边之间的时间依赖 vector<int> d(n - 1, 0); // 入度 for (int i = 0; i < n; i++) { for (int j = 1; j < c[i].size(); j++) { // 贴纸j必须在贴纸j-1之后出现 int prev_edge = l[i][j - 1]; int curr_edge = l[i][j]; g2[prev_edge].push_back(curr_edge); d[curr_edge]++; } } // 拓扑排序输出顺序 queue<int> q; for (int i = 0; i < n - 1; i++) if (d[i] == 0) q.push(i); while (!q.empty()) { int u = q.front(); q.pop(); cout << u + 1 << " "; for (int v : g2[u]) { if (--d[v] == 0) q.push(v); } }
样例解析
样例1
输入: 5 0 1 2 3 1 3 1 3 4 12 3 5 6 0 4 2 6 1 3 8 3 输出: Yes 1 4 2 3树结构:
1 / \ 2 3 / \ 4 5匹配过程:
- 节点2(叶子):贴纸[2],边(1,2)需求3,满足3≤2+?
- 节点4(叶子):贴纸[8],边(3,4)需求12,满足12≤8+?
- 节点5(叶子):贴纸[3],边(3,5)需求6,满足6≤3+?
- 节点3:贴纸[6,1,3],需要匹配边(1,3),(3,4),(3,5)
- 节点1:贴纸[0,4],需要匹配边(1,2),(1,3)
最终得到合法顺序:1→4→2→3
正确性证明
匹配条件的必要性
对于边 权重 ,如果 是 的子节点,那么:
- 上传的值 是 为这条边贡献的部分
- 需要贡献
- 必须存在一个贴纸 使得
时间顺序的保持
贴纸从上到下的顺序确定了边出现的时间顺序,通过构建依赖图和拓扑排序保证时间约束。
复杂度分析
- DFS遍历:
- 排序操作:每个节点排序度数次,总
- 拓扑排序:
- 总复杂度:
对于 可以接受。
算法亮点
- 自底向上处理:从叶子到根,逐步确定匹配关系
- 贪心匹配:排序后贪心匹配需求和贴纸
- 上传值机制:子节点向父节点传递需求信息
- 依赖图构建:将贴纸顺序转化为边的时间顺序
- 拓扑排序:输出合法的时间序列
总结
这道题的核心在于将时间顺序约束转化为图上的匹配问题:
- 问题转化:将线索出现顺序转化为贴纸与边的匹配问题
- 树形DP:利用树结构自底向上传递信息
- 贪心策略:在约束条件下进行最优匹配
- 拓扑排序:处理时间依赖关系
这种"树形结构 + 贪心匹配 + 拓扑排序"的组合在解决带有顺序约束的树问题时非常有效。
- 1
信息
- ID
- 4871
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者