第一次看到这道题的时候,我也被"架传送门"这个操作搞得有点懵。但仔细想想,其实可以把问题简化成"交换两条路径的后缀"。想象一下,每条路径就像一串珠子,传送门就是在这串珠子的某个位置切开,然后把两条路径的后半部分交换连接。
举个例子,假设有两条路径:
如果在3和c的位置架设传送门,就相当于把3后面的部分和c后面的部分交换,变成:
这个理解很关键,因为它把看似复杂的传送门操作转化为了简单的区间交换问题。在实际解题中,我们需要处理多个这样的交换操作,并且要能快速计算每次交换后的结果。
为什么选择Splay树来解决这个问题?因为它特别适合处理动态区间操作。Splay树是一种自平衡二叉搜索树,通过伸展操作(splaying)把最近访问的节点移动到根节点位置,这样下次访问就能更快。
在这个问题中,我们需要频繁地进行区间交换操作。Splay树可以:
具体来说,我们可以:
整个过程的时间复杂度接近O(log n),对于大规模数据非常高效。
题目中的y值范围可能很大,直接作为树的键值会导致树的高度失控。这时候就需要离散化:
cpp复制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());
}
为了防止查找时越界,我们在每棵树的开始和结束位置添加哨兵节点:
cpp复制for(int i = 1; i <= n; i++)
bt[i].push_back(0),bt[i].push_back(inf);
建树时采用递归方式,同时存储每个离散化位置对应的节点指针:
cpp复制int build(int l,int r,int p,int id) {
int mid = l+r>>1;
int u = ++idx;
tr[u].init(id,p);
ver[id][mid] = u; //关键:存储节点指针
if(l < mid)ls(u) = build(l,mid-1,u,id);
if(r > mid)rs(u) = build(mid+1,r,u,id);
pushup(u);
return u;
}
实际交换后缀的过程可以分为几个步骤:
cpp复制int l1 = lower_bound(bt[x1].begin(),bt[x1].end(),y)-bt[x1].begin();
int u1 = ver[x1][l1];
cpp复制splay(u1,0,x1);
cpp复制swap(rs(u1),rs(u2));
tr[rs(u1)].p = u2;
tr[rs(u2)].p = u1;
cpp复制int st1 = get_k(1,u1),ed1 = get_k(tr[u1].size,u1);
ans += 1ll*st1*ed1 + 1ll*st2*ed2;
ans -= 1ll*st1*ed2 + 1ll*st2*ed1;
在实际编码中,有几个容易踩坑的地方需要注意:
cpp复制struct node tr[N<<2]; //四倍大小
cpp复制void rotate(int x) {
int y = tr[x].p,z = tr[y].p;
int k = tr[y].s[1] == x;
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);
}
cpp复制ans += 1ll*st1*ed1; //使用long long防止溢出
把所有这些部分组合起来,完整的解题流程应该是:
这个解法巧妙地将传送门操作转化为区间交换问题,利用Splay树的高效动态操作特性,实现了优秀的时空复杂度。在实际比赛中,这类问题往往考察的就是如何将实际问题抽象为合适的数据结构问题,以及对该数据结构的熟练掌握程度。