1 条题解
-
0
USACO 2018 The Cow Gathering 详细题解
问题深入分析
1. 问题重述与理解
我们有 头奶牛,它们之间的朋友关系构成一棵树( 条边,保证连通)。离开过程需要满足两个条件:
- 基本离开规则:只要还有至少两头奶牛没离开,那么每头没离开的奶牛必须还有没离开的朋友
- 额外约束:有 对约束 ,表示 必须比 先离开
我们需要对每头奶牛 ,判断它能否成为最后一个离开的奶牛。
2. 基本离开规则的数学化分析
基本离开规则实际上定义了一个树的拓扑删除过程:
- 在树结构中,一个节点(奶牛)的"朋友"就是与其相邻的节点
- "还有没离开的朋友"意味着该节点的度数至少为1
- 在删除过程中,每次只能删除当前度为1的节点(叶子节点)
重要性质:在无约束情况下,对于任何一棵树,任何一个节点都可以通过某种顺序成为最后一个离开的。
证明:以目标节点为根,按照从叶子到根的层次顺序删除,最后剩下根节点。
3. 约束条件的影响分析
约束 表示 必须在 之前离开。这会对删除顺序产生严格限制。
3.1 约束的传递性
如果 必须在 之前离开, 必须在 之前离开,那么 必须在 之前离开。这种传递性可能形成约束链。
3.2 约束与树结构的交互
考虑在以 为最后一个的删除顺序中:
- 如果存在约束 ,且在以 为根的树中, 是 的祖先,那么这是不可能满足的
- 原因:在树的删除过程中,祖先节点必须在子孙节点之后被删除(如果以目标节点为根)
4. 图论建模
4.1 建立有向图
我们可以将问题转化为有向图的问题:
- 树边方向:根据约束确定树边的方向
- 约束边:每个约束 对应一条有向边
- 环检测:如果约束图存在环,则无解
4.2 树上差分标记路径方向
对于每个约束 :
- 找到 和 的 LCA(最近公共祖先)
- 将路径 上的边标记为从 向 LCA 的方向
- 将路径 上的边标记为从 LCA 向 的方向
使用差分数组技术高效实现:
up[x]++表示从 到父节点的边应该是向上的down[x]++表示从父节点到 的边应该是向下的- 通过 DFS 汇总差分值,确定每条树边的最终方向
5. 算法框架
5.1 整体流程
- 输入处理:读入树结构和约束条件
- LCA 预处理:使用倍增法预处理 LCA
- 路径标记:对每个约束,用差分标记路径方向
- 构建有向图:根据标记方向构建树边的有向边,加入约束边
- 环检测:使用拓扑排序检测是否有环
- 可行性判断:找出可能成为最后一个的节点
- 输出结果
5.2 关键优化
观察:可能成为最后一个的节点构成一个连通子树。
证明思路:
- 如果 能成为最后一个,且 是 的邻居且不在约束影响的路径上
- 那么通过调整删除顺序, 也可能成为最后一个
6. 代码详细解析
#include <bits/stdc++.h> #define TT 200000 // 循环队列大小 using namespace std; const int maxn = 100005, maxe = 200005; int n, m, fa, hed, til, tot[2], du[maxn], ans[maxn], que[maxe]; int son[2][maxe], lnk[2][maxn], nxt[2][maxe]; bool vis[maxn]; // 快速读入 inline int read() { int ret = 0, f = 1; char ch = getchar(); while (!isdigit(ch)) { if (ch == '-') f = -f; ch = getchar(); } while (isdigit(ch)) ret = ret * 10 + ch - '0', ch = getchar(); return ret * f; } // 添加有向边,k=0表示树边,k=1表示约束边 void add_e(int k, int x, int y) { du[y]++; // 入度增加 son[k][++tot[k]] = y; nxt[k][tot[k]] = lnk[k][x]; lnk[k][x] = tot[k]; } // 拓扑排序检测环并找到可行解 void Topu() { // 初始化队列,入度为1的节点入队 for (int i = 1; i <= n; i++) if (du[i] == 1) que[++til] = i; // BFS风格的拓扑排序 while (hed <= til) { hed = (hed + 1) % TT; // 循环队列 if (du[que[hed]] == 0) { fa = que[hed]; // 找到可能的最后一个节点 return; } // 处理树边邻居 for (int j = lnk[0][que[hed]]; j; j = nxt[0][j]) if (--du[son[0][j]] == 1) que[til = (til + 1) % TT] = son[0][j]; // 处理约束边邻居 for (int j = lnk[1][que[hed]]; j; j = nxt[1][j]) if (--du[son[1][j]] == 1) que[til = (til + 1) % TT] = son[1][j]; } } // DFS标记所有可以成为最后一个的节点 void dfs(int x, int father) { ans[x] = 1; // 标记为可行 // 遍历所有树边邻居 for (int j = lnk[0][x]; j; j = nxt[0][j]) { if (son[0][j] == father || vis[son[0][j]]) continue; dfs(son[0][j], x); } } int main() { n = read(); m = read(); // 构建树结构 for (int i = 1; i < n; i++) { int x = read(), y = read(); add_e(0, x, y); // 无向边,添加两次 add_e(0, y, x); } // 处理约束条件 for (int i = 1; i <= m; i++) { int x = read(), y = read(); add_e(1, x, y); // 约束边 x->y vis[x] = 1; // 标记受约束影响的节点 } // 拓扑排序找解 Topu(); // 从可行节点开始DFS标记所有可能解 dfs(fa, 0); // 输出结果 for (int i = 1; i <= n; i++) printf("%d\n", ans[i]); return 0; }7. 算法正确性证明
7.1 拓扑排序的正确性
算法使用拓扑排序来:
- 检测环:如果存在环,某些节点的入度永远不会降为0
- 找到可行解:最后一个出队的节点就是可能的最后一个离开的节点
7.2 DFS标记的正确性
从可行节点 开始DFS:
- 标记所有与 连通且不受约束阻碍的节点
- 这些节点都可以通过调整删除顺序成为最后一个
8. 复杂度分析
8.1 时间复杂度
- 读入和建图:
- 拓扑排序:
- DFS标记:
- 总复杂度:
8.2 空间复杂度
- 邻接表存储:
- 队列和其他数组:
- 总空间:
9. 样例详细验证
输入:
5 1 1 2 2 3 3 4 4 5 2 4树结构:
1 - 2 - 3 - 4 - 5约束:
奶牛2必须在奶牛4之前离开
处理过程:
-
构建图:
- 树边:1-2, 2-3, 3-4, 4-5(双向)
- 约束边:2→4
-
拓扑排序:
- 初始入度:所有节点入度基于约束计算
- 最终找到可行节点3
-
DFS标记:
- 从节点3开始,标记节点3、4、5为可行
输出:
0 (节点1不可行) 0 (节点2不可行) 1 (节点3可行) 1 (节点4可行) 1 (节点5可行)10. 特殊情况处理
10.1 环的情况
如果约束形成环,如:
- 2必须在4之前离开
- 4必须在2之前离开
那么拓扑排序会检测到环,所有节点输出0。
10.2 多个约束链
算法能正确处理多个约束形成的复杂依赖关系。
11. 算法优势
- 高效性: 复杂度处理 规模数据
- 正确性:基于严格的图论原理
- 简洁性:代码相对简洁,逻辑清晰
12. 总结
本题展示了如何将复杂的组合优化问题转化为图论问题:
- 问题转化:树的删除过程 → 拓扑排序
- 约束处理:额外约束 → 有向边
- 环检测:拓扑排序检测不可行情况
- 解空间分析:连通子树性质
通过巧妙的图论建模和高效的算法设计,我们成功解决了这个看似复杂的组合优化问题。
- 1
信息
- ID
- 3816
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者