【THUWC2020】某科学的动态仙人掌

考场上感觉是一道可做题,然后最后做法差不多,但是一直卡在 \(dfn\),没去搞 \(bfn\)

然后今天捡起这道题想了一下就想出来了。

因为没有交题链接,下面的做法不一定正确。(结论经过对拍,应该没问题)

这是一个俩 \(\log\) 做法,不知道有没有更优的。


首先,这是一个区间点构成虚树的 \(K\) 连通块个数。其中 \(K\) 连通为当在原树上距离 \(\leq K\) 就加一条无向边。

众所周知,虚树森林的连通块个数可以由 \(\text{点数} - \text{边数}\) 得到。

所以可以得到一个 \(trival\)\(O(n^2Q)\) 的做法,即暴力连边,如果距离 \(\leq K\) 就并查集连边。

但是显然过不了。


我们考虑缩小边的数量。具体的,如果能给每个点钦点一个必连的点(比如说贪心的在左边选一个最大的,在右边选一个最小的),然后做一个高维数点,那就优美了。

对于询问,给定点集,我们钦定点集中任意一个点 \(u\),对原树 \(makeroot(u)\),那么剩下点集中的点 \(v\),如果它最近的祖先 \(a\) 满足 \(dis(v, a) \leq K\),那么连一条有向边 \(a \rightarrow v\)

显然,对于这个点集, \(u\)\(K\) 连通块集合 \(S\) 满足对于所有点 \(v \in S\)\(u\) 能到达 \(v\)

然而实际上 \(v\) 可以和任意一个满足距离限制条件的祖先 \(a\) 匹配,并不需要最近的。这样也是满足这个性质的。

那么,我们记一个 \(Ans = r - l + 1\) 作为初值,每次找集合 \(S\) 中的一个元素 \(u\) 作为根,并进行连边。记 \(u\) 能到达的除 \(u\) 外的点的集合为 \(A\),那么更新答案 \(Ans' = Ans - |A|\) ,更新集合 \(S' = S \backslash (A \cup \{u\})\)

即每次更新完删除根的连通块。考虑这个过程,我们可以找到一个不变的关系:

如果每次都选 \(bfs\) 序最小的点更新,那么:

  1. 对于每次更新的连通块,我们连的边 \((u, v)\)\(bfn_u < bfn_v\)
  2. 对于根为 \(u\) 的一个连通块更新完后,接下来对于未被更新的 \(v\)\(v\)\(u\) 子树中,\(bfn_v\) 比这个连通块所有点的 \(bfn\) 都大。

这样,如果每个点只向 \(bfn\) 比它小的点连边,连通性不变。那么我们的答案就是 \(\text{点数} - \text{这张图的边数}\)

实际上,对于任意一个 \(K\),对距离 \(\leq K\) 的点之间连边,得到的是一个弦图 \(G\)

一个经典的结论就是:对于一棵树,以及任意一个这样的 \(K\),用 \(bfs\) 得到的序列,就是 \(G\) 的完美消除序列。这也就印证了上面的那个性质。


现在问题转化为:

对于一个区间 \([l, r]\),计算 \(u\) 的个数使得 \(l \leq u \leq r\) 且满足(存在一个不同于 \(u\)\(l \leq v \leq r\),使得 (\(bfn_v < bfn_u\)))。

显然,对于 \(u\) 只要选择 \(bfn_v\) 小于自己的 \(v\) 就行了。

显然是选编号差 \(|u - v|\) 最小的更好,可以更方便的统计。

所以现在就有了一个 \(O(n^2 + nQ)\) 的做法:

对每个点 \(u\),预处理 \(v < u\) 且满足 \(dis(u, v) \leq K, bfn_v < bfn_u\) 的最大的 \(v\),记为 \(L_u\)。如果不存在记 \(L_u = 0\)

同理预处理 \(v > u\) 且满足 \(dis(u, v) \leq K, bfn_v < bfn_u\) 的最小的 \(v\),记为 \(R_u\)。如果不存在记 \(R_u = n + 1\)

于是只需要求

\[(r - l + 1) - \sum_{l \leq i \leq r} \left[(l \leq L_i) \lor (R_i \leq r)\right] \]

显然 \(\lor\) 是可以转变成 \(1 - \land\),所以就要计算:

\[\sum_{l \leq i \leq r} \left[(L_i < l) \land (r < R_i)\right] \]

放到树上,也就是没有父亲的点个数。


我们先计算出 \(L\)\(R\)

不失一般性地考虑算 \(L\),对于 \(u\),我们需要计算满足:

  1. \(dis(u, v) \leq K\)
  2. \(bfn_v \leq bfn_u\)
  3. \(v < u\)

的最大的 \(v\)

首先,可以通过树分治或 dsu 满足条件 \(1\)

然后对于条件 \(2\)\(3\),可以使用树状数组套 \(set\),即支持前缀加元素,询问前缀的元素集合里元素的前驱。

因此可以轻松地做到 \(O(n\log^3 n)\)

然而这太慢了。

使用树分治,考虑合并时,因为 \(LCA\) 固定,把 \(dis\) 拆成 \(dep\) 的限制。

使用双指针,把 \(dep\) 从小到大扫过去。此时要往集合动态插入元素。

维护一个序列数据结构,维护 \(v = v'\) 时最小的满足条件的 \(dfn\),支持后缀元素取 \(min\),查询区间元素最小值,前缀单 \(log\) 二分。

因为前缀最小值是单调减的,所以二分正确性显然。当然这实际上就是一个单调栈。

这样就可以做到 \(O(n\log^2 n)\) 了。


计算出了 \(L\)\(R\),还有计算答案时。显然是一个三维数点。我们将询问拆成两部分。

那么要询问满足如下条件的 \(v\) 的个数:

  1. \(v < A\)
  2. \(L_v < B\)
  3. \(R_v > C\)

离线加树套树或使用分治解决。

复杂度 \(O(n \log^2 n)\)


综上,时间复杂度 \(O(n \log^2 n)\),空间复杂度可以做到 \(O(n)\)

#include <bits/stdc++.h>

const int MAXN = 300010;
int n, Q, K;
int head[MAXN], to[MAXN << 1], nxt[MAXN << 1], tot;
void addedge(int b, int e) {
	nxt[++tot] = head[b]; to[head[b] = tot] = e;
	nxt[++tot] = head[e]; to[head[e] = tot] = b;
}
int bfn[MAXN], dep[MAXN], buc[MAXN];
void dfs0(int u, int fa = 0) {
	++buc[dep[u]];
	for (int i = head[u]; i; i = nxt[i])
		if (to[i] != fa)
			dep[to[i]] = dep[u] + 1, dfs0(to[i], u);
}
int sz[MAXN], vis[MAXN], rt, rtv, all;
void getroot(int u, int fa = 0) {
	sz[u] = 1;
	int maxd = 0;
	for (int i = head[u]; i; i = nxt[i])
		if (!vis[to[i]] && to[i] != fa) {
			getroot(to[i], u);
			sz[u] += sz[to[i]];
			maxd = std::max(maxd, sz[to[i]]);
		}
	maxd = std::max(maxd, all - sz[u]);
	if (maxd < rtv) rtv = maxd, rt = u;
}
void _getroot(int u, int d) {
	rt = u, all = d, rtv = 0x3f3f3f3f, getroot(u);
}
typedef std::vector<int> vi;
typedef std::pair<int, int> pi;
typedef std::vector<pi> vpi;
vpi tree[MAXN];
struct cmp {
	bool operator () (int a, int b) {
		return tree[a].size() > tree[b].size();
	}
} ;
std::priority_queue<int, vi, cmp> q;
void calc(int u, vpi & tar, int dep = 1, int fa = 0) {
	tar.emplace_back(dep, u);
	for (int i = head[u]; i; i = nxt[i])
		if (to[i] != fa && !vis[to[i]])
			calc(to[i], tar, dep + 1, u);
}
int L[MAXN], R[MAXN];
const int INF = 0x3f3f3f3f;
int sgt[MAXN << 2];
void mdf(int tar, int v) {
	int u = 1, l = 1, r = n;
	while (l < r) {
		sgt[u] = std::min(sgt[u], v);
		int mid = l + r >> 1;
		if (tar <= mid) u = u << 1, r = mid;
		else u = u << 1 | 1, l = mid + 1;
	}
	sgt[u] = std::min(sgt[u], v);
}
void clr(int tar) {
	int u = 1, l = 1, r = n;
	while (l < r) {
		sgt[u] = INF;
		int mid = l + r >> 1;
		if (tar <= mid) u = u << 1, r = mid;
		else u = u << 1 | 1, l = mid + 1;
	}
	sgt[u] = INF;
}
int qryl(int R, int v, int u = 1, int l = 1, int r = n) {
	if (sgt[u] > v) return 0;
	if (r <= R) {
		while (l < r) {
			int mid = l + r >> 1;
			if (sgt[u << 1 | 1] <= v) u = u << 1 | 1, l = mid + 1;
			else u = u << 1, r = mid;
		}
		return l;
	}
	int mid = l + r >> 1;
	if (R > mid) if (int t = qryl(R, v, u << 1 | 1, mid + 1, r)) return t;
	return qryl(R, v, u << 1, l, mid);
}
int qryr(int L, int v, int u = 1, int l = 1, int r = n) {
	if (sgt[u] > v) return 0;
	if (L <= l) {
		while (l < r) {
			int mid = l + r >> 1;
			if (sgt[u << 1] <= v) u = u << 1, r = mid;
			else u = u << 1 | 1, l = mid + 1;
		}
		return l;
	}
	int mid = l + r >> 1;
	if (L <= mid) if (int t = qryr(L, v, u << 1, l, mid)) return t;
	return qryr(L, v, u << 1 | 1, mid + 1, r);
}
void solve(int u) {
	vis[u] = true; int bak;
	tree[bak = 1].emplace_back(0, u);
	for (int i = head[u]; i; i = nxt[i]) if (!vis[to[i]])
		calc(to[i], tree[++bak]);
	for (int i = 1; i <= bak; ++i) q.push(i);
	auto make = [] (vpi & x, vpi & y) {
		int cur = 0, SZ = x.size();
		std::reverse(y.begin(), y.end());
		for (auto t : y) {
			while (cur < SZ && x[cur].first + t.first <= K) {
				int u = x[cur].second;
				mdf(u, bfn[u]);
				++cur;
			}
			int u = t.second, atl = qryl(u, bfn[u]), atr = qryr(u, bfn[u]);
			if (atl < u) L[u] = std::max(L[u], atl);
			if (atr > u) R[u] = std::min(R[u], atr);
		}
		for (int i = 0; i < cur; ++i) clr(x[i].second);
		std::reverse(y.begin(), y.end());
	};
	while (q.size() > 1) {
		int x = q.top(); q.pop();
		int y = q.top(); q.pop();
		std::sort(tree[x].begin(), tree[x].end());
		std::sort(tree[y].begin(), tree[y].end());
		make(tree[x], tree[y]); make(tree[y], tree[x]);
		if (tree[x].size() < tree[y].size()) std::swap(x, y);
		for (auto t : tree[y]) tree[x].push_back(t);
		tree[y].clear();
		q.push(x);
	}
	while (!q.empty()) q.pop();
	for (int i = 1; i <= bak; ++i) tree[i].clear();
	for (int i = head[u]; i; i = nxt[i])
		if (!vis[to[i]]) {
			getroot(to[i]);
			_getroot(to[i], sz[to[i]]);
			solve(rt);
		}
}
inline int absx(int x) { return x < 0 ? -x : x ; }
namespace divide {
	struct qry {
		int typ, a, b, c;
	} qs[MAXN * 5];
	int bak, ansl[MAXN * 2], tree[MAXN];
	void solve(qry * l, qry * r) {
		if (l + 1 >= r) return ;
		qry * mid = l + (r - l >> 1);
		solve(l, mid); solve(mid, r);
		qry * lhs = l, * rhs = mid;
		while ((lhs < mid) || (rhs < r)) {
			if (rhs == r || (lhs < mid && lhs -> b < rhs -> b)) {
				if (lhs -> typ == 0) {
					for (int x = lhs -> c; x; x &= x - 1) ++tree[x];
				}
				++lhs;
			} else {
				if (rhs -> typ != 0) {
					int at = rhs -> typ;
					int coef = absx(at) / at, res = 0;
					at *= coef;
					for (int x = rhs -> c + 1; x <= n + 1; x += x & -x) res += tree[x];
					ansl[at] += coef * res;
				}
				++rhs;
			}
		}
		for (qry * it = l; it != mid; ++it) if (it -> typ == 0)
			for (int x = it -> c; x; x &= x - 1) --tree[x];
		std::sort(l, r, [] (qry a, qry b) {
			return a.b == b.b ? absx(a.typ) < absx(b.typ) : a.b < b.b;
		});
	}
	void solve() {
		std::sort(qs + 1, qs + 1 + bak, [] (qry a, qry b) {
			return a.a == b.a ? absx(a.typ) < absx(b.typ) : a.a < b.a;
		});
		solve(qs + 1, qs + 1 + bak);
	}
}
int main() {
	std::ios_base::sync_with_stdio(false), std::cin.tie(0);
	memset(sgt, 0x3f, sizeof sgt);
	std::cin >> n >> Q >> K;
	for (int i = 1; i <= n; ++i) R[i] = n + 1;
	for (int i = 2, t; i <= n; ++i)
		std::cin >> t, addedge(i, t);
	dep[1] = 1; dfs0(1);
	for (int i = 1; i <= n; ++i) buc[i] += buc[i - 1];
	for (int i = n; i; --i) bfn[i] = buc[dep[i]]--;
	_getroot(1, n); solve(rt);
	for (int i = 1; i <= n; ++i)
		divide::qs[++divide::bak] = (divide::qry) {0, i, L[i], R[i]};
	for (int i = 1, l, r; i <= Q; ++i) {
		std::cin >> l >> r;
		divide::qs[++divide::bak] = (divide::qry) {i, r, l, r};
		divide::qs[++divide::bak] = (divide::qry) {-i, l - 1, l, r};
	}
	divide::solve();
	for (int i = 1; i <= Q; ++i)
		std::cout << divide::ansl[i] << '\n';
	return 0;
}
posted @ 2019-12-31 15:11  daklqw  阅读(998)  评论(0编辑  收藏  举报