题解:永无乡
一、题目分析
本题中永无乡有(n)座岛,每座岛有独特重要度并可排名。岛之间通过桥相连,存在连通关系。有两种操作,一是在两座岛间建桥(B x y
),二是询问与某岛连通的所有岛中第(k)重要的岛的编号(Q x k
) 。
二、解题思路
本题结合并查集和Splay树来解决。并查集用于维护岛之间的连通性,Splay树用于在每个连通分量中快速查找第(k)小的元素(即第(k)重要的岛)。
(一)数据结构定义
- Splay树节点定义
struct Node
{
int s[2], p, v, id;
int size;
void init(int _v, int _id, int _p)
{
v = _v, id = _id, p = _p;
size = 1;
}
}tr[N];
定义节点结构体Node
,包含左右子节点指针s[2]
、父节点指针p
、节点值v
(表示岛的重要度)、岛的编号id
以及子树大小size
。init
函数用于初始化节点。
2. 其他变量
int root[N], idx;
int p[N];
root[N]
存储每个连通分量对应的Splay树的根节点;idx
用于给Splay树节点编号;p[N]
是并查集数组。
(二)并查集操作函数
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
查找节点(x)所在连通分量的根节点,同时进行路径压缩。
(三)Splay树基本操作函数
- 更新节点大小函数
pushup
void pushup(int x)
{
tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + 1;
}
根据左右子树的大小更新当前节点的子树大小。
2. 旋转函数rotate
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);
}
对节点(x)进行旋转操作,根据(x)是其父节点(y)的左儿子还是右儿子,进行相应的旋转,并更新节点大小。
3. Splay操作函数splay
void splay(int x, int k, int b)
{
while (tr[x].p != k)
{
int y = tr[x].p, z = tr[y].p;
if (z != k)
if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
else rotate(y);
rotate(x);
}
if (!k) root[b] = x;
}
将节点(x)通过旋转操作调整到节点(k)的子节点位置(若(k = 0),则调整到根节点),并更新对应连通分量的根节点。
4. 插入函数insert
void insert(int v, int id, int b)
{
int u = root[b], p = 0;
while (u) p = u, u = tr[u].s[v > tr[u].v];
u = ++ idx;
if (p) tr[p].s[v > tr[p].v] = u;
tr[u].init(v, id, p);
splay(u, 0, b);
}
向连通分量(b)对应的Splay树中插入值为v
、编号为id
的节点,先找到合适的插入位置,然后插入节点并将其Splay到根节点。
5. 获取第k
小元素编号的函数get_k
int get_k(int k, int b)
{
int u = root[b];
while (u)
{
if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
else if (tr[tr[u].s[0]].size + 1 == k) return tr[u].id;
else k -= tr[tr[u].s[0]].size + 1, u = tr[u].s[1];
}
return -1;
}
在连通分量(b)对应的Splay树中找到第k
小的元素的编号,通过比较左子树大小和k
的值,在树中进行搜索并返回编号。
(四)辅助函数dfs
void dfs(int u, int b)
{
if (tr[u].s[0]) dfs(tr[u].s[0], b);
if (tr[u].s[1]) dfs(tr[u].s[1], b);
insert(tr[u].v, tr[u].id, b);
}
通过深度优先搜索遍历Splay树,将节点插入到另一个连通分量的Splay树中,用于合并两个连通分量的Splay树。
(五)主函数main
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ )
{
p[i] = root[i] = i;
int v;
scanf("%d", &v);
tr[i].init(v, i, 0);
}
idx = n;
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
a = find(a), b = find(b);
if (a != b)
{
if (tr[root[a]].size > tr[root[b]].size) swap(a, b);
dfs(root[a], b);
p[a] = b;
}
}
scanf("%d", &m);
while (m -- )
{
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);
if (*op == 'B')
{
a = find(a), b = find(b);
if (a != b)
{
if (tr[root[a]].size > tr[root[b]].size) swap(a, b);
dfs(root[a], b);
p[a] = b;
}
}
else
{
a = find(a);
if (tr[root[a]].size < b) puts("-1");
else printf("%d\n", get_k(b, a));
}
}
return 0;
}
- 读取岛的数量
n
和一开始存在的桥的数量m
,初始化并查集数组p
和每个岛对应的Splay树的根节点root
,同时读入每座岛的重要度并初始化Splay树节点。 - 处理一开始存在的桥,通过并查集判断桥两端的岛是否属于不同连通分量,若是,则将较小的Splay树合并到较大的Splay树中。
- 读取新的操作数量
m
,循环处理每个操作:- 若操作是
B x y
(建桥),通过并查集判断两座岛是否属于不同连通分量,若是,则将较小的Splay树合并到较大的Splay树中。 - 若操作是
Q x k
(查询),先找到岛x
所在连通分量的根节点,若连通分量中岛的数量小于k
,输出-1
;否则通过get_k
函数找到第k
小的岛的编号并输出。
- 若操作是
四、时间复杂度和空间复杂度
(一)时间复杂度
- 并查集操作:每次查找时间复杂度接近(O(1))(经过路径压缩),合并操作时间复杂度也接近(O(1))(启发式合并)。
- Splay树操作:插入、旋转、Splay操作和查询第(k)小元素的时间复杂度均为(O(\log n))。
- 总的时间复杂度为(O((n + m)\log n)),其中
n
是岛的数量,m
是操作的总数。
(二)空间复杂度
Splay树最多有(n + n\log n)个节点(考虑启发式合并的情况),并查集数组和其他辅助变量空间复杂度为(O(n)),所以总的空间复杂度为(O(n + n\log n)),近似为(O(n\log n))。