队列图完全连通性

终于有能拿出来的东西了。

upd:后来得知这个叫 baka's trick。


最初的问题
题意转化之后是双指针,每次加一条边,查询连通性,删除最早的边。
标准做法是 动态图完全连通性

但是继续观察,此题提供了两个特殊性质。

  1. 保证图为森林。
  2. 每次删边只删最早的。

基于性质 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 发现原题。
题目链接
做法完全一致。

最小差值生成树 这道题上拿到最优解。
提交记录

posted @ 2024-08-08 19:13  Houraisan_Kaguya  阅读(182)  评论(0)    收藏  举报