洛谷 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\)。显然有:
接下来,我们考虑求出每条边断裂的时刻 \(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\) 不断裂的条件:
设当前操作的是点 \(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)\),我们实际上要求的就是:
如果我们把 \(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\) 同阶,总时间复杂度应为:
足以通过本题数据。
Hint
本题码量大,细节较多,在此列出几个需要注意的细节。
- 计算 \(r\) 时,对精度要求很高。为了避免浮点数丢精度,可以采用整数二分来处理。
- 断裂边时,必须同时在 \(lo\) 和 \(hi\) 集合中删去对应键值。
- 在集合中二分时,注意正确选择阈值。
- 多测清空,但直接
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;
}

浙公网安备 33010602011771号