题解-P4169 [Violet] 天使玩偶/SJY摆棋子

题意

二维平面上有 \(n\) 个点,给你 \(m\) 次操作,每次操作可以插入一个点或者询问所有点中距离给定点最近的曼哈顿距离。\(n,m\le 3\times 10^5.\)

分析

这是一道 K-D Tree 的裸题。

而对于这道题,我们还需要考虑插入操作。我们给出两种方式:

  1. 按很多题解的思路,在这棵树上直接按普通二叉树的插入方式一维一维地递归下去,找到一个空节点就插入。不过有可能导致整棵树不平衡,而这种二叉树又不能像平衡树一样旋来旋去(否则就不符合左右儿子与父节点的坐标关系),所以只能用最朴素的替罪羊树的思想:要么定期重构,要么插入时检查这个子树是否不平衡,不平衡就重构整棵子树。重构方式是先还原成原来这棵子树中的点的序列(拍扁),再按建树方式重构;不平衡率 \(\alpha\) 一般设为 \(0.75\) 左右。
  2. 其实上面的做法有点 naive,因为它需要重构,而重构的复杂度很高,代码又很长。所以我们想一种不用重构的方法:首先离线,把原来 \(n\) 个点和所有需要插入的点全部记录下来,然后对于每个点记录一个删除标记 del,原来需要插入的点删除标记为 \(1\)。然后对所有记录下来的点来建成一棵 K-D Tree。这样原来的插入操作变成了单点修改删除标记!而且还不用写重构,复杂度也很低。至于查询,只用更新没有删除的点即可。

对于方式 2,假设一个点被删除,那记录最左端、最下端为 inf,最右端、最上端为 -inf,然后按正常方式从左右儿子更新即可。

直接一发最优解!(2023.2.13 12:00)

代码

这里给出方法 2 代码:

#include <bits/stdc++.h>
using namespace std;
#define gc getchar
#define pc putchar
typedef double db;
inline int read() {
	int x = 0; char ch = gc();
	while (!isdigit(ch)) ch = gc();
	while (isdigit(ch))
		x = x * 10 + ch - 48, ch = gc();
	return x;
}
inline void write(int x) {
	if (!x) return (void)(pc('0'), pc('\n'));
	char ch[50]; int tp = 0;
	while (x) ch[++tp] = x % 10 + 48, x /= 10;
	while (tp) { pc(ch[tp--]); } pc('\n');
} const int N = 600010, inf = 0x3f3f3f3f;
inline db sqr(db x) { return x * x; }
inline void chkmn(int &x, int y) { x = min(x, y); }
inline void chkmx(int &x, int y) { x = max(x, y); }
struct Query { int x, y; } q[N];
struct Point { int x, y, del, rev; } a[N];
int n, m, ans, cnt, root, lc[N], rc[N], d[N];
int L[N], R[N], D[N], U[N];
int id[N];
inline void push_upi(int x, int o) {
	chkmn(L[x], L[o]), chkmn(D[x], D[o]);
	chkmx(R[x], R[o]), chkmx(U[x], U[o]);
}
inline void push_up(int x) {
	if (!a[x].del) 
		L[x] = R[x] = a[x].x, D[x] = U[x] = a[x].y;
	else L[x] = D[x] = inf, R[x] = U[x] = -1;
	if (lc[x]) push_upi(x, lc[x]);
	if (rc[x]) push_upi(x, rc[x]);
}
int build(int l, int r) {
	if (l > r) return 0;
	int mid = (l + r) >> 1;
	db avx = 0, avy = 0, sx = 0, sy = 0;
	for (int i = l; i <= r; i++)
		avx += a[i].x, avy += a[i].y;
	avx /= (r - l + 1), avy /= (r - l + 1);
	for (int i = l; i <= r; i++)
		sx += sqr(a[i].x - avx),
		sy += sqr(a[i].y - avy);
//	按方差划分维度
	if (sx > sy)
		nth_element(a + l, a + mid, a + r + 1,
			[&](const Point& u, const Point& v) {
				return u.x < v.x; }), d[mid] = 1;
	else nth_element(a + l, a + mid, a + r + 1,
			[&](const Point& u, const Point& v) {
				return u.y < v.y; }), d[mid] = 2;
	id[a[mid].rev] = mid; // 记录每个点在树上的编号,方便修改
	lc[mid] = build(l, mid - 1);
	rc[mid] = build(mid + 1, r);
	push_up(mid);
	return mid;
}
void update(int l, int r, int x) {
	int mid = (l + r) >> 1;
	if (x == mid)
		return (void)(a[x].del = 0, push_up(x));
	if (x < mid) update(l, mid - 1, x);
	else update(mid + 1, r, x);
	push_up(mid);
}
Point k;
inline int dis(Point u, Point v) {
	return abs(u.x - v.x) + abs(u.y - v.y);
}
inline int mndis(int u, Point v) {
//	这个点到矩形的最小距离
	if (!u) return inf;
	int res = 0;
	if (v.x < L[u]) res += L[u] - v.x;
	if (v.x > R[u]) res += v.x - R[u];
	if (v.y < D[u]) res += D[u] - v.y;
	if (v.y > U[u]) res += v.y - U[u];
	return res;
}
void query(int x) {
//	更新每个可能会被更新的点
	if (!a[x].del) chkmn(ans, dis(a[x], k));
	int dl = mndis(lc[x], k), dr = mndis(rc[x], k);
	if (dl < ans && dr < ans) {
		if (dl < dr) {
			query(lc[x]);
			if (dr < ans) query(rc[x]);
		} else {
			query(rc[x]);
			if (dl < ans) query(lc[x]);
		}
	} else {
		if (dl < ans) query(lc[x]);
		if (dr < ans) query(rc[x]);
	}
}
int T[N];
int main() {
	n = read(), m = read(), cnt = n;
	for (int i = 1; i <= n; i++) 
		a[i].x = read(), a[i].y = read(), a[i].rev = i;
	for (int i = 1; i <= m; i++) {
		int t = read(), x = read(), y = read();
		if (t == 1) 
        a[++cnt] = (Point){x, y, 1, cnt};
		else q[i] = (Query){x, y};
		T[i] = t;
	}
	root = build(1, cnt);
	for (int i = 1, now = n; i <= m; i++) 
		if (T[i] == 1) update(1, cnt, id[++now]);
		else {
			ans = inf, k = (Point){q[i].x, q[i].y};
			query(root);
			write(ans);
		}
	return 0;
}
posted @ 2023-08-04 16:28  Laijinyi  阅读(16)  评论(1编辑  收藏  举报