队列图完全连通性
终于有能拿出来的东西了。
upd:后来得知这个叫 baka's trick。
最初的问题
题意转化之后是双指针,每次加一条边,查询连通性,删除最早的边。
标准做法是 动态图完全连通性。
但是继续观察,此题提供了两个特殊性质。
- 保证图为森林。
- 每次删边只删最早的。
基于性质 1 自然可以得到 LCT 的做法,也就是大多数题解的做法。
下面提出一种基于性质 2 的算法。
先描述一下问题:
维护 \(n\) 个点和一个队列,支持一下操作:
1 a b在队列末尾加入一条无向边 \((a, b)\)。
2删除队列首。
3 a b查询只考虑队列中的边的情况下,\(a, b\) 两点是否连通。
强制在线。
连通性问题首选并查集。
带删边就用可撤销并查集。
然而可撤销并查集的结构类似于栈(只能删除最后加入的元素)
分块算法
我们对边序列进行分块。
维护队列其实也就相当于是维护双指针(单调)
考虑若目前的两个指针分别为 \(l, r\)。
\(l\) 在第 \(B\) 个块里。
我们先从第 \(B+1\) 个块开始顺次插入边,直到 \(r\)。
然后再从第 \(B\) 个块末尾开始倒序插入边,直到 \(l\)。
这样 \(l\) 就是最后被插入的边了。
考虑左删除,直接删即可。
若 \(l\) 跑到了下一个块,就暴力重构所有边。
考虑右插入,我们先将第 \(B\) 个块的边全部删除,再插入新的边,然后再把第 \(B\) 个块的边插回来。
容易发现每一步 插入/删除 的次数均不超过 \(O(\sqrt n)\)。
复杂度达到 \(O(n^{1.5}\log n)\)。
线段树算法
容易发现上述算法的本质就是调整了插入并查集的边的顺序,使得插入和删除都比较容易。
那么能否使用分治结构实现?
Lemma:在线段树上用 \(O(\log n)\) 个节点表示一个区间时,位于每一层上面的节点至多只有 2 个。
显然,这个涉及到线段树区间操作复杂度的证明。
分类讨论递归情况即可。
我们提出以下结构维护:
若目前的左右指针为 \(l, r\),则在线段树上划分出来 \(O(\log n)\) 个小区间表示 \([l, r]\)。
随后按照深度从小到大的方式依次插入这些小区间。
首先,容易发现在指针扫过去的过程中,线段树上的每个节点至多被加入一次删除一次。
加入/删除 的时候,均需要先把深度更大的节点删除,而后再加回来。
我们知道 \(T(n) = T(\frac{n}{2}) + O(n)\),\(T(n) = O(n)\)。
也就是深度更大的那些节点的 \(size\) 的和与这个节点的大小同阶。
那么我们就可以放心操作了,因为复杂度上等价于只删除了这个节点。
总的 插入/删除 次数 \(O(n \log n)\)。
复杂度 \(O(n \log^2 n)\)。
倍增算法
线段树算法的实现较为困难,且难以拓展。
下面提出一种基于倍增实现的算法,理论上可以实现双端队列。
(即队列首尾同时插入删除)
考虑一种有 \(O(\log n)\) 层的结构,其中第 \(i\) 层能够存储 \(2^i\) 个元素。
插入就直接扔到第 \(0\) 层,若超出这层的容量限制就全部扔到上面一层。
容易发现这个结构类似于二进制分组。
现在我们维护两个这种结构 \(A, B\)。
插入并查集时仍然是先插大的层后插小的层。
新的边只插入 \(B\),删除时只删除 \(A\)。
若 \(A\) 目前为空,则从 \(B\) 中找到元素最多的层(容易发现一定是最早被插入的若干个元素)而后直接转移到 \(B\) 中。
删除无非就是插入的逆过程。
上述结构与线段树类似,但是更容易维护。
复杂度 \(O(n \log^2 n)\)。
再考虑如果改成首尾均插入/删除怎么办。
直接删的话复杂度会假。
我们采用经典倍缩策略,第 \(i\) 层的大小缩减到 \(2^{i - 1}\) 以下才向下合并。
也就是超过 \(2^i\) 向上合并,低于 \(2^{i - 1}\) 向下合并。
这样删除操作也可以均摊分析。
倍增版实现
提交记录。
namespace mset
{
const int sz1 = 400005, sz2 = 200005;
int fa[sz1], siz[sz1];
int sta[sz2];
void init(int n)
{
for (int i = 1; i <= n; ++i)
fa[i] = i;
for (int i = 1; i <= n; ++i)
siz[i] = 1;
}
int find(int a)
{
while (a != fa[a])
a = fa[fa[a]];
return a;
}
void net(int a, int b)
{
a = find(a); b = find(b);
if (siz[a] < siz[b])
swap(a, b);
sta[++sta[0]] = b;
fa[b] = a; siz[a] += siz[b];
}
void bak(int a)
{
while (a --> 0)
{
int x = sta[sta[0]--];
siz[fa[x]] -= siz[x];
fa[x] = x;
}
}
}
namespace quegraph
{
const int B = 19;
const int sz = 200005;
struct node
{
int a, b;
};
int now, ano;
node tmp[sz];
node *rt = tmp, *lf = rt - 1;
void init(int n)
{
mset::init(n);
}
void reinsertr(int i)
{
for (int j = now & ~((1 << i + 1) - 1), k = j ^ 1 << i; j < k; ++j)
mset::net(rt[j].a, rt[j].b);
}
void reinsertl(int i)
{
for (int j = ano & ~((1 << i + 1) - 1), k = j ^ 1 << i; j < k; ++j)
mset::net(lf[-j].a, lf[-j].b);
}
void insert(int a, int b)
{
int x = __builtin_ctz(now + 1);
int sum = (1 << x) - 1 + (ano & (1 << x) - 1);
mset::bak(sum);
rt[now].a = a; rt[now].b = b; ++now;
reinsertr(x);
for (int i = x - 1; i >= 0; --i)
if (ano & 1 << i)
reinsertl(i);
}
void del()
{
if (ano == 0)
{
int x = __lg(now), y = 1 << x;
now ^= y; ano = y;
rt += y; lf += y;
}
int sum = 0, x = __builtin_ctz(ano);
sum = (1 << x) + (now & (1 << x + 1) - 1);
mset::bak(sum); --ano;
if (now & 1 << x)
reinsertr(x);
for (int i = x - 1; i >= 0; --i)
{
reinsertl(i);
if (now & 1 << i)
reinsertr(i);
}
}
}
后记
后来在 UOJ 发现原题。
题目链接。
做法完全一致。

浙公网安备 33010602011771号