天梯赛L3-026传送门:用Splay树模拟‘交换后缀’,保姆级代码逐行解析
天梯赛L3-026传送门用Splay树模拟‘交换后缀’保姆级代码逐行解析在算法竞赛中数据结构的选择往往决定了解决问题的效率与优雅程度。天梯赛L3-026传送门这道题目表面上看是一个关于路径操作的模拟题实则暗藏了对高级数据结构应用的深度考察。本文将带您深入剖析如何用Splay树这一灵活的自平衡二叉搜索树来高效模拟题目中的交换后缀操作。1. 问题重述与核心思路题目描述了一个由n条平行路径构成的世界每条路径上有若干个传送门。关键操作是架设传送门——这实际上等价于交换两条路径上从某个坐标开始的后缀部分。这种操作会动态改变路径结构因此需要一种能够高效支持区间操作的数据结构。核心观察点每次操作只影响两条路径的特定后缀操作可能嵌套即被交换的后缀本身包含其他传送门需要动态维护路径结构并快速计算影响经过分析Splay树的以下特性使其成为理想选择动态性支持在O(log n)时间内完成子树分裂与合并灵活性无需严格的平衡条件实现相对简单功能性天然支持区间操作2. 解题框架与关键组件2.1 离散化处理由于坐标范围可能很大到1e9直接建树不现实。我们需要先对每条路径上的坐标进行离散化vectorvectorint bt(n1); for(int i 1; i n; i){ bt[i].push_back(0); // 左哨兵 bt[i].push_back(inf); // 右哨兵 } // 收集所有出现的y值 for(auto [op,x1,x2,y]:q){ bt[x1].push_back(y); bt[x2].push_back(y); } // 去重排序完成离散化 for(int i 1 ; i n; i){ sort(bt[i].begin(),bt[i].end()); bt[i].erase(unique(bt[i].begin(),bt[i].end()), bt[i].end()); }2.2 Splay树节点设计每个节点需要维护以下信息左右子节点指针父节点指针节点值离散化后的坐标子树大小struct node{ int s[2], v, p; // 左右孩子、值、父节点 int size; // 子树大小 void init(int _v, int _p){ v _v, p _p, size 1; } }tr[N2]; // 需要开四倍空间2.3 建树与预处理采用递归方式建树同时记录每个离散化值对应的节点指针int build(int l, int r, int p, int id) { int mid l r 1; int u idx; tr[u].init(bt[id][mid], p); ver[id][mid] u; // 记录该离散化值对应的节点 if(l mid) ls(u) build(l, mid-1, u, id); if(r mid) rs(u) build(mid1, r, u, id); pushup(u); return u; }3. 核心操作实现3.1 Splay操作Splay操作是保持树平衡的关键包含旋转和双旋转两种情况void rotate(int x) { int y tr[x].p, z tr[y].p; int k tr[y].s[1] x; // 判断x是y的哪个孩子 tr[z].s[tr[z].s[1] y] x, tr[x].p z; tr[y].s[k] tr[x].s[k^1], tr[tr[x].s[k^1]].p y; tr[x].s[k^1] y, tr[y].p x; pushup(y), pushup(x); } void splay(int x, int k, int id) { while(tr[x].p ! k) { int y tr[x].p, z tr[y].p; if(z ! k) { if((tr[z].s[0] y) (tr[y].s[0] x)) rotate(y); else rotate(x); } rotate(x); } if(!k) root[id] x; }3.2 交换后缀操作这是本题最核心的部分需要将两条路径从指定位置开始的后缀交换for(auto [op,x1,x2,y]:q){ // 找到离散化后的位置 int l1 lower_bound(bt[x1].begin(),bt[x1].end(),y)-bt[x1].begin(); int l2 lower_bound(bt[x2].begin(),bt[x2].end(),y)-bt[x2].begin(); // 获取对应节点 int u1 ver[x1][l1], u2 ver[x2][l2]; // 将两个节点splay到根 splay(u1, 0, x1), splay(u2, 0, x2); // 交换右子树即后缀 swap(rs(u1), rs(u2)); // 更新父指针和size tr[rs(u1)].p u1, tr[rs(u2)].p u2; pushup(u1), pushup(u2); // 计算答案变化 int st1 get_k(1,u1), ed1 get_k(tr[u1].size,u1); int st2 get_k(1,u2), ed2 get_k(tr[u2].size,u2); ans 1ll*st1*ed1 1ll*st2*ed2; ans - 1ll*st1*ed2 1ll*st2*ed1; cout ans \n; }4. 实现细节与优化技巧4.1 哨兵节点的作用在离散化时添加0和inf作为哨兵确保总能找到小于/大于任何实际坐标的边界值简化边界条件的处理保证树结构的完整性4.2 空间复杂度分析为什么需要开四倍空间原始n个路径每个路径添加2个哨兵节点每个查询涉及2个路径可能新增2个节点因此最坏情况下总节点数约为4n量级。4.3 时间复杂度保证每个操作主要包含两次二分查找O(log m)两次splay操作均摊O(log m)子树交换O(1)因此单次操作时间复杂度为O(log m)m为路径上的节点数。5. 常见错误与调试技巧在实现这类复杂数据结构题目时容易遇到以下问题内存越界检查空间是否足够特别是四倍空间规则确认节点编号从1开始还是0开始旋转错误确保在rotate后正确更新所有相关指针特别注意父指针的更新顺序size维护错误在任何可能改变树结构的操作后调用pushup验证旋转、合并、分裂后size的正确性调试建议先实现并验证splay基本操作的正确性用小数据测试建树过程逐步添加操作验证每个步骤使用assert检查不变量// 示例调试代码 void check(int u) { if(!u) return; assert(tr[u].size 1 tr[ls(u)].size tr[rs(u)].size); if(ls(u)) assert(tr[ls(u)].p u); if(rs(u)) assert(tr[rs(u)].p u); check(ls(u)); check(rs(u)); }6. 扩展思考与变式掌握了这个解法后可以思考以下变式问题如果传送门有方向性单向传送如何修改模型如果每次操作影响的是一个区间而非后缀如何调整能否用其他平衡树如Treap实现相同功能如果要求支持撤销操作该如何设计这些思考可以帮助深化对Splay树和区间操作的理解。在实际比赛中选择合适的数据结构并正确实现其核心操作往往是解决复杂问题的关键。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2596309.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!