洛谷 P12806 [AMPPZ 2019] The Great Drone Show 题解

前言

传送门

比较考验思维的图论题目。

题意

在三维空间里有 \(n\) 架无人机,初始时都在地面上。地面是 \(z=0\) 的平面。第 \(i\) 架无人机的初始位置用坐标 \((x_i, y_i, 0)\) 描述。有 \(m\) 条线缆连接在无人机对之间。所有无人机和线缆组成一个连通的平面图。每条线缆拥有一个最大长度 \(l\)。下面执行 \(k\) 次操作,每次操作平滑改变一架无人机的 \(z\) 坐标。如果在操作过程中线缆长度超过最大长度,就会断裂。断裂后的线缆不会再修复。最后给出 \(q\) 次询问,每次询问一个无人机对 \((u, v)\),求第几次操作后 \(u,v\) 不连通。

数据范围:多测,组数 \(T\le 400\)\(n, m, k, q\le 5\times 10^5\),过程中所有量保证在 long long 范围内。

思路

转化题意

由于整个过程中无人机的 \((x, y)\) 坐标不会改变,我们考虑对每条边,预处理出最大能承受的 \(z\) 坐标差 \(r_e\)。显然有:

\[d^2_{(u, v)} = (x_u-x_v)^2+(y_u-y_v)^2\\ r_e = \left\lfloor\sqrt{l^2-d_{u,v}^2}\right\rfloor \]

接下来,我们考虑求出每条边断裂的时刻 \(t_e\)。换言之,边 \(e\) 在第 \(t_e\) 个操作之后断掉。由于每个操作中高度的变化是平滑的,我们只要检测变化前后是否会断裂即可。

利用平面图的 5 - 退化性质定向

发现一个现实问题:与每个点相连的边数都是 \(O(n)\) 的,如果每做一个操作都维护所有邻边,复杂度显然是爆炸的。这时我们注意到一个很重要的性质:给定的图是一个平面图,而平面图是 5 - 退化的。即其任意一个子图中至少有一个节点度数 \(\le 5\)。这意味着,我们可以利用此性质对所有无向边进行定向。

考虑我们用队列维护所有图中度数 \(\le 5\) 的节点,逐个删除它们,并删除所有邻边,更新邻居的度数。这样我们得到一个删除序列。当一个点被删除时,我们这样给它的邻边定向:由它指向它仍未被删除的邻居。这个定向的复杂度是线性的。

考察我们得到的有向图,我们发现,每个点至多有 \(5\) 条出边。现在我们钦定每条有向边 \(u\to v\) 只由它的起点 \(u\) 负责维护。这样对于每次操作,我们需要更新的边数就不超过 \(5\),是可控的。

利用有序集合维护操作求断边时刻

考虑具体的维护方式。

记点 \(i\) 所处高度为 \(h_i\),考虑一条边 \(u\to v\) 不断裂的条件:

\[h_u-r_{(u, v)}\le h_v\le h_u+r_{(u, v)} \]

设当前操作的是点 \(u\),操作后高度为 \(h_u'\),则我们分类讨论:

  • 对于 \(u\) 的所有入边 \(v\to u\),编号为 \(e\),其数量级是 \(O(n)\) 的,我们需要批量更新。我们维护两个有序集合 \(lo_u\)\(hi_u\),键值分别是二元组 \((h_v-r_e, e)\)\((h_v+r_e, e)\)\(e\) 是边的编号。若 \(u\) 的新高度记为 \(h_u'\),则我们在 \(lo_u\) 中二分找到第一关键字大于 \(h_u'\) 的,后缀全部断裂;在 \(hi_u\) 中二分找到第一关键字小于 \(h_u'\) 的,前缀全部断裂。

  • 对于 \(u\) 的所有出边 \(u\to v\),编号为 \(e\),其数量不超过 \(5\) 条,我们直接遍历,比较 \(\lvert h_u'-h_v\rvert\)\(r_e\) 的大小关系,检查操作后是否会断裂。若不断裂,还要在 \(v\) 的集合中更新键值。

当一条边断裂时,我们记录此时的时刻 \(t_e\),标记它为断裂,并在边终点对应的集合中删去键值。显然每条边只会被删除一次,复杂度均摊线性;每个操作至多扫描 \(5\) 条边,算上集合操作,总复杂度是 \(O((k+m)\log n)\)

利用 Kruskal 重构树得出答案

最后我们考虑通过 \(t_e\) 求出答案。

考虑对于询问 \((u, v)\),我们实际上要求的就是:

\[ans = \max_{path_{u\to v}}\left(\min_{e\in path}(t_e)\right) \]

如果我们把 \(t_e\) 看作边权,这可以理解为,\(u\to v\) 的所有路径中,最小边权最大的那条路径上的最小边权。如果一条边始终不断裂,令 \(t_e=\infty\)。这个东西适合用 Kruskal 重构树求出。

我们考虑用 Kruskal 对原无向图做最大生成树,并构建重构树,具体方法不再赘述。根据重构树的性质,\(u,v\) 都为树上的叶子,而我们所求的答案,就为 \(\text{LCA}(u, v)\) 的点权。

复杂度分析

分析复杂度。5 - 退化定向是 \(O(n+m)\) 的;用 std::set\(t_e\) 的过程是 \(O((k+m)\log n)\) 的;做 Kruskal 是 \(O(m\log m)\) 的,查询 LCA,如果使用 dfs 序算法,可以 \(O(n\log n)\) 预处理,\(O(1)\) 查询。合并一下,我们认为 \(n,m,k,q\) 同阶,总时间复杂度应为:

\[O((n+m+k+q)\log n) \]

足以通过本题数据。

Hint

本题码量大,细节较多,在此列出几个需要注意的细节。

  1. 计算 \(r\) 时,对精度要求很高。为了避免浮点数丢精度,可以采用整数二分来处理。
  2. 断裂边时,必须同时在 \(lo\)\(hi\) 集合中删去对应键值。
  3. 在集合中二分时,注意正确选择阈值。
  4. 多测清空,但直接 memset 大数组可能导致效率下降。应当只清空用到的部分。

其余未尽之处详见代码。

代码

#include<algorithm>
#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
#include<set>
#define fastio ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define int long long
#define MIKU 0
using namespace std;

const int N = 500005, inf = 0x3f3f3f3f;

//链式前向星存图。注意预留足够空间。
struct Graph {
	struct Edge { int v, id, nxt; } G[N<<3];
	int h[N<<1], tot;
	Graph() { memset(h, -1, sizeof(h)); }
	void addE(int u, int v, int id) { G[tot] = {v, id, h[u]}; h[u] = tot ++; }
	void clear() { memset(h, -1, sizeof(h)); tot = 0; }
} G0, G1, Tk;

int Z, n, n2, m, k, q;
int x[N], y[N], deg[N], del[N], h[N], w[N<<1];;
int re[N<<2], brk[N<<2], tim[N<<2];
vector<pair<int, int> > out[N];
set<pair<int, int> > lo[N], hi[N];

//dfs 序求 LCA。
#define MIN(x, y) (in[(x)] < in[(y)] ? (x) : (y))
int st[25][N<<1], in[N<<1], idx;
void st_init() {
	for(int i=1; i<=__lg(n2); i++) for(int j=1; j+(1<<i)-1<=n2; j++) {
		st[i][j] = MIN(st[i-1][j], st[i-1][j+(1<<(i-1))]);
	}
}
void dfs(int u, int f) {
	st[0][in[u] = ++idx] = f;
	for(int e=Tk.h[u]; ~e; e=Tk.G[e].nxt) if(Tk.G[e].v != f) dfs(Tk.G[e].v, u); 
}
int LCA(int x, int y) {
	if(x == y) return x;
	if((x = in[x]) > (y = in[y])) swap(x, y);
	int d = __lg(y - x++);
	return MIN(st[d][x], st[d][y-(1<<d)+1]);
}

//Kruskal 的数据结构。
struct Edge { int u, v, t; } Ed[N<<2];
bool cmp(Edge a, Edge b) { return a.t > b.t; }
struct DSU {
	int s[N<<1];
	void init() { for(int i=1; i<=n2; i++) s[i] = i; }
	int find(int x) { return x == s[x] ? x : s[x] = find(s[x]); }
	void merge(int a, int b) { if((a=find(a)) != (b=find(b))) s[a] = b; }
} dsu;

//整数二分求平方根。
int sqr(int x) {
	int l = 0, r = 1e9+5;
	while(l < r) {
		int mid = (l + r + 1) >> 1;
		if(mid * mid <= x) l = mid;
		else r = mid - 1;
	}
	return l;
}

//多测清空。
void init() {
	memset(deg, 0, sizeof(deg));
	G0.clear(); G1.clear(); Tk.clear();
	idx = 0;
	for(int i=1; i<=n; i++) {
		lo[i].clear(); hi[i].clear(); out[i].clear();
		h[i] = del[i] = 0;
	}
	for(int i=1; i<=m; i++) brk[i] = tim[i] = 0;
}

//读入并建图。
void build() {
	for(int i=1; i<=m; i++) {
		int u, v, l; cin>>u>>v>>l;
		deg[u] ++; deg[v] ++;
		int d = (x[u]-x[v]) * (x[u]-x[v]) + (y[u]-y[v]) * (y[u]-y[v]);
		re[i] = sqr(l*l - d);
		G0.addE(u, v, i); G0.addE(v, u, i);
	}
}

//5-退化定向。实现类似拓扑排序。
void dir() {
	queue<int> q;
	for(int i=1; i<=n; i++) if(deg[i]<=5) q.push(i);
	while(!q.empty()) {
		int u = q.front(); q.pop();
		if(del[u]) continue;
		del[u] = 1;
		for(int e=G0.h[u]; ~e; e=G0.G[e].nxt) {
			int v = G0.G[e].v, id = G0.G[e].id;
			if(del[v]) continue;
			G1.addE(u, v, id);
			out[u].push_back({v, id});
			if(--deg[v] == 5) q.push(v);
		}
	}
}

//初始化集合。
void init_set() {
	for(int u=1; u<=n; u++) {
		for(int e=G1.h[u]; ~e; e=G1.G[e].nxt) {
			int v = G1.G[e].v, id = G1.G[e].id, r = re[id];
			lo[v].insert({-r, id});		//起始时所有 h 为 0。
			hi[v].insert({r, id});
		}
	}
}

//set 维护操作求断边时刻。
void solve() {
	for(int i=1; i<=k; i++) {
		int u, dh; cin>>u>>dh;
		int nh = h[u] + dh;
        //出边。
		for(auto e : out[u]) {
			int v = e.first, id = e.second;
			if(brk[id]) continue;
			if(abs(nh-h[v]) > re[id]) {	//断裂。
				tim[id] = i; brk[id] = 1;
				lo[v].erase({h[u]-re[id], id}); hi[v].erase({h[u]+re[id], id});
			} else {					//不断裂,更新键值。
				lo[v].erase({h[u]-re[id], id}); lo[v].insert({nh-re[id], id});
				hi[v].erase({h[u]+re[id], id}); hi[v].insert({nh+re[id], id});
			}
		}
        //入边。
		auto it = lo[u].upper_bound({nh, inf});	//此处编号设为 inf,避免高度恰为 nh 的被删。
		while (it != lo[u].end()) {
			int key = it->first, id = it->second;
			tim[id] = i; brk[id] = 1;
			it = lo[u].erase(it);
			hi[u].erase({key+2*re[id], id});	//另一个集合也要删。
		}
		it = hi[u].lower_bound({nh, 0});		//此处必须用 0,原因请自己思考。
		while(it != hi[u].begin()) {
			it --;
			int key = it->first, id = it->second;
			tim[id] = i; brk[id] = 1;
			it = hi[u].erase(it);
			lo[u].erase({key-2*re[id], id});	//同理。
		}
		h[u] = nh;
	}
}

//Kruskal 重构树。
void Kruskal() {
	for(int u=1; u<=n; u++) {
		for(int e=G1.h[u]; ~e; e=G1.G[e].nxt) {
			int v = G1.G[e].v, id = G1.G[e].id;
			Ed[id] = {u, v, tim[id] ? tim[id] : inf};	//断裂时刻作为边权。不断裂的设为 inf。
		}
	}
	sort(Ed+1, Ed+m+1, cmp);
	dsu.init();
	int cnt = 0;
	for(int i=1; i<=m; i++) {			//模板 Kruskal 重构树。
		if(cnt >= n-1) break;
		int u = Ed[i].u, v = Ed[i].v, t = Ed[i].t;
		if((u=dsu.find(u)) == (v=dsu.find(v))) continue;
		cnt ++; 
		int p = n + cnt; w[p] = t;
		Tk.addE(p, u, 0); Tk.addE(p, v, 0);
		Tk.addE(u, p, 0); Tk.addE(v, p, 0);
		dsu.merge(u, p); dsu.merge(v, p);
	}
}

//处理询问。
void query() {
	for(int i=1; i<=q; i++) {
		int u, v; cin>>u>>v;
		int ans = w[LCA(u, v)];
		cout<<(ans == inf ? -1 : ans)<<'\n';
	}
}

void work() {
	cin>>n; 
	n2 = n << 1;
	for(int i=1; i<=n; i++) cin>>x[i]>>y[i];
	cin>>m; 
	init(); build(); dir(); init_set();
	cin>>k; 
	solve();
	cin>>q; 
	Kruskal(); dfs(n2-1, 0); st_init();
	query();
}

signed main() {
	fastio;
	cin>>Z;
	while(Z--) work();
	return MIKU;
}
posted @ 2026-08-27 09:01  EtherealYz  阅读(6)  评论(0)    收藏  举报