1 条题解
-
0
「SDOI2017」天才黑客 详细题解
问题深入分析
1. 核心难点
- 动态口令:程序的口令在运行过程中会改变
- LCP代价:通过边的代价与当前口令和边口令的相似度相关
- 状态爆炸:直接维护(节点, 口令)的状态数达到 ,不可行
2. 关键转化
观察1:在字典树中,从节点 到节点 的路径代价只与它们的LCA有关
- 这是因为LCP长度等于LCA的深度
观察2:我们可以把问题看作在分层图上跑最短路
- 第一层:原图的节点
- 第二层:字典树节点(代表不同口令)
- 但直接构建这样的图会太大
算法核心思想
1. 虚树优化建图
对于每个原图节点 ,我们只关心:
- 从 出发的边对应的字典树节点(出边口令)
- 到达 的边对应的字典树节点(入边口令)
步骤:
- 收集所有与 相关的字典树节点
- 构建这些节点的虚树(包含所有LCA)
- 在虚树上建立辅助结构来快速计算最小代价
2. 代价计算优化
对于从口令 切换到口令 的代价 ,我们利用虚树性质:
在虚树中,对于任意节点 ,考虑所有可能的 :
min_{x} (dist[u][x] + dep[LCA(x,y)]) = min_{z在y到根的路径上} (f(z) + dep[z])
其中
3. 前后缀链技术
为了实现快速查询,我们在虚树上构建:
- 前缀链 :从虚树第一个节点到第 个节点的最优值
- 后缀链 :从第 个节点到虚树最后一个节点的最优值
这样对于任意 ,我们可以在 时间内求出最小代价。
详细实现步骤
阶段1:预处理字典树
// 树链剖分预处理LCA void dfs1(int u) { sz[u] = 1; for (每个子节点 v of u) { dep[v] = dep[u] + 1; dfs1(v); sz[u] += sz[v]; if (sz[v] > sz[bigson[u]]) bigson[u] = v; } } void dfs2(int u) { dfn[u] = ++dfc; if (bigson[u]) { top[bigson[u]] = top[u]; dfs2(bigson[u]); } for (每个子节点 v of u, v ≠ bigson[u]) { top[v] = v; dfs2(v); } }
阶段2:为每个节点构建优化图
步骤2.1:收集相关字典树节点
- 添加根节点 1
- 添加所有出边的口令节点
- 添加所有入边的口令节点
步骤2.2:构建虚树
- 按DFS序排序
- 添加相邻节点的LCA
- 再次排序去重
步骤2.3:建立辅助结构
// 为虚树节点分配编号 for j = 1 to cnt: id[j] = new_node() mxn[j] = mnn[j] = j // 构建前后缀链 pre[1] = new_node(); link(pre[1], id[1], 0) suf[cnt] = new_node(); link(suf[cnt], id[cnt], 0) for j = 2 to cnt: pre[j] = new_node() link(pre[j], id[j], 0) link(pre[j], pre[j-1], 0) // 前缀传递 for j = cnt-1 downto 1: suf[j] = new_node() link(suf[j], id[j], 0) link(suf[j], suf[j+1], 0) // 后缀传递
步骤2.4:建立LCP优化边
for j = cnt downto 2: fa = Find(LCA(x[j], x[j-1])) mxn[fa] = max(mxn[fa], mxn[j]) mnn[fa] = min(mnn[fa], mnn[j]) if (fa ≠ 1) link(id[j], id[fa], 0) if (mnn[j] ≠ 1) link(id[j], pre[mnn[j]-1], dep[x[fa]]) if (mxn[j] ≠ cnt) link(id[j], suf[mxn[j]+1], dep[x[fa]])
阶段3:连接原图边
出边连接:
for 每条从u出发的边e = (u,v,c,y): link(id[Find(y)], e, c)
入边连接:
for 每条到达u的边e = (u,v,c,y): link(e, suf[1], dep[y]) // 直接连接到后缀链 if (Find(y) ≠ 1) link(e, id[Find(y)], 0)
阶段4:运行最短路算法
// 初始化 for 每条从节点1出发的边e: dis[e] = c[e] push e to priority_queue // Dijkstra while queue not empty: pop min node u if visited[u]: continue mark u visited for each edge (u, v, w): if dis[v] > dis[u] + w: dis[v] = dis[u] + w push v to queue
阶段5:统计答案
for 每条边e = (u,v,c,y): ans[v] = min(ans[v], dis[e])
复杂度分析
时间复杂度
- 虚树构建: 每个节点,总共
- 最短路:
- 总体:
空间复杂度
- 图存储:
- 虚树相关: 每个节点
正确性证明
1. 状态完整性
通过虚树,我们包含了所有可能的口令转移:
- 直接转移:口令不变或微小变化
- 间接转移:通过前后缀链实现口令大跳转
2. 代价正确性
前后缀链保证了:
- 对于任意目标口令 ,我们都能找到最优的源口令
- LCP代价通过LCA深度正确计算
3. 最优性
Dijkstra算法在扩展后的图上保证找到全局最优解。
优化技巧总结
- 虚树压缩:将指数级状态压缩到线性
- 前后缀链: 时间查询最小代价
- 统一编号:所有节点(原图边、虚树节点、辅助节点)统一编号,便于Dijkstra
- 链式前向星:高效存储稀疏图
边界情况处理
- 空口令:对应字典树根节点1
- 自环重边:题目明确说明可能存在
- 内存管理:多组数据要及时清空
- 整数溢出:使用long long存储距离
这种方法将复杂的动态口令问题转化为经典的最短路问题,通过巧妙的图构建避免了状态爆炸,是图论建模的典范。
- 1
信息
- ID
- 3124
- 时间
- 1500ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 3
- 已通过
- 1
- 上传者