队列图完全连通性

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


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

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

  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 sz = 400005;
	int fa[sz], dep[sz];
	vector<int> sta;
	void init(int n)
	{
		for (int i = 1; i <= n; ++i)
			fa[i] = i;
		for (int i = 1; i <= n; ++i)
			dep[i] = 1;
	}
	int find(int a)
	{
		while (fa[a] != a)
			a = fa[a];
		return a;
	}
	void merge(int a, int b)
	{
		a = find(a); b = find(b);
		if (dep[a] > dep[b])
			std::swap(a, b);
		sta.push_back(a);
		fa[a] = b; dep[b] += dep[a];
	}
	void roll(int a)
	{
		while (sta.size() > a)
		{
			dep[fa[sta.back()]] -= dep[sta.back()];
			fa[sta.back()] = sta.back();
			sta.pop_back();
		}
	}
};

namespace quegraph
{
	unsigned l = 0, r = -1;
	const int LG = 20;
	vector<pair<int, int> > add[LG], del[LG];
	int top1[LG], top2[LG];
	void back(int a)
	{
		while (a >= 0)
		{
			if (del[a].size())
			{
				top2[a] = mset::sta.size();
				for (auto i : del[a])
					mset::merge(i.first, i.second);
			}
			if (add[a].size())
			{
				top1[a] = mset::sta.size();
				for (auto i : add[a])
					mset::merge(i.first, i.second);
			}
			--a;
		}
	}
	void insert(int a, int b)
	{
		for (int i = 0; ; ++i)
			if (add[i].size() == 0)
			{
				if (i != 0)
				{
					if (del[i - 1].size())
						mset::roll(top2[i - 1]);
					else
						mset::roll(top1[i - 1]);
				}
				top1[i] = mset::sta.size();
				for (int j = i - 1; j >= 0; --j)
				{
					for (auto k : add[j])
					{
						add[i].push_back(k);
						mset::merge(k.first, k.second);
					}
					add[j].clear();
				}
				add[i].emplace_back(a, b);
				mset::merge(a, b);
				back(i - 1);
				break;
			}
	}
	void delt()
	{
		for (int i = 0; ; ++i)
			if (i == LG)
			{
				for (--i; ; --i)
					if (add[i].size())
					{
						del[i].swap(add[i]);
						top2[i] = top1[i--];
						break;
					}
				continue;
			}else if (del[i].size())
			{
				mset::roll(top2[i]);
				int j = 1, k = 0;
				while (j < del[i].size())
				{
					del[k].push_back(del[i][j++]);
					if (del[k].size() == (1U << k))
						++k;
				}
				del[i].clear();
				back(i);
				break;
			}
	}
	bool check(int a, int b)
	{
		return mset::find(a) == mset::find(b);
	}
};

或许把 vector 换成数组能大大减小常数。
但是为了封装方便还是用 vector 实现的。

后记

\(Lyin\) 交流的过程中,\(Lyin\) 提出只依靠性质 2 也可以使用 \(LCT\) 维护。
具体的,当出现环的时候就删除这条链上时间最早的边。
复杂度 \(O(n \log n)\) 好像,比这个优。
不过考虑常数影响的话就不一定了。

后来在 UOJ 发现原题。
题目链接
做法完全一致。

posted @ 2024-08-08 19:12  Houraisan_Kaguya  阅读(6)  评论(0编辑  收藏  举报