题解:P17240 [IOI 2026] 弹球机 / ballmachine

挺有趣味的题。

考虑这样的做法:从小到大枚举 \(0\leq i<m\),不断在 \(i\) 处插入一个球直到无法插入。这样我们可以得到 \(i\) 所在的链长 \(l_i\),而这些链构成了树的一个剖分,因此还能得到 \(n=\sum l_i\)

对于一个点 \(u\),我们称与其同链的儿子为重儿子,称其他儿子为轻儿子。考虑如何利用链剖分和先序遍历得到树结构。容易想到两种遍历方式,一种是优先遍历轻儿子,另一种是优先遍历重儿子。不难发现前者可以更好地还原树结构,直接递归构造,记录链顶的父亲对应的链和位置即可。

于是容易得到一个简单的做法:从小到大枚举 \(0\leq i<m\),不断在 \(i\) 处插入值为 \(m-1-i\) 的球。不难证明此时 \(fa_u\) 上的球的值 \(\geq\) \(u\) 上的球的值,因此会先遍历轻儿子,再遍历重儿子。这个做法 \(K=1\)\(B=M-1\),可以获得 \(47\) 分。

\(0,\cdots,M-1\) 来区分链太浪费了。考虑将叶子 \(i\) 表示成 \(10a_i+b_i\),其中 \(0\leq a_i\leq 19\)\(0\leq b_i\leq 9\)。注意到如果 \(l_i\geq 2\),我们在链上以某种方式标上 \(a_i,b_i\) 即可区分这些链。

我们可以先从小到大枚举 \(0\leq i<m\),不断在 \(i\) 处插入值为 \(0\) 的球。这一轮只是为了得到 \(l_i\)\(n\)

接下来还是枚举 \(0\leq i<m\):若 \(l_i=1\),我们将其标成 \(0\);否则我们在链顶标 \(a_i+1\),中间节点标 \(31\),链底标 \(b_i+21\)。显然这样标会优先遍历轻儿子,并且我们依然可以递归还原出所有 \(l_i\geq 2\) 的链之间的结构。

现在还剩下 \(l_i=1\) 的点,我们需要知道它们挂在哪里。考虑直接分块,每 \(20\) 个叶子节点分成一块。对于每个块分别做,枚举 \(0\leq i<m\)

  • \(l_i=1\)\(i\) 为块内第 \(j\) 个叶子节点,将其标为 \(j+9\)
  • \(l_i=1\)\(i\) 不在块内,将其标为 \(30\)
  • \(l_i\geq 2\),将链顶标为 \(a_i+10\),其他点标为 \(b_i\)

显然这样标会优先遍历重儿子,因此我们容易再递归过程中快速得知当前链对应的叶子。遍历到链顶时,若其不存在重儿子,说明 \(l_i=1\),此时我们利用递归信息记录当前叶子节点的父亲即可。同时我们可以在前一轮中提前记录轻儿子个数,这样可以知道当前子树什么时候遍历完。

这个做法 \(K\leq 12\)\(B\leq 31\),因此 \(C\leq 43\),可以通过。时间复杂度为 \(\mathcal{O}(nm)\)

代码实现有一些比较 tricky 的地方。

主要代码
int len[MAXM], id[MAXM], bel[MAXM];
pii fa[MAXM];
vector<int> deg[MAXM], nd[MAXM];

bool insert(int U, int X);
vector<int> collect();

vector<int> find_structure(int M) {
	for (int i = 0; i < M; ++i) {
		while (insert(i, 0)) ++len[i];
	}
	collect();

	int N = 0;
	for (int i = 0; i < M; ++i) {
		N += len[i];
		if (len[i] == 1) {
			insert(i, 0);
		} else {
			int x = i / 10, y = i % 10;
			insert(i, 1 + x);
			for (int j = 1; j <= len[i] - 2; ++j) insert(i, 31);
			insert(i, 21 + y);	
		}
	}

	auto vec = collect();
	int cur = 0, cnt = 1;
	auto dfs1 = [&](auto &&self, int c, int p) -> void {
		int v = vec[cur++];
		if (v <= 20) {
			id[c] += (v - 1) * 10;
		} else if (v >= 21 && v <= 30) {
			id[c] += v - 21;
			return;
		}
		int d = 0;
		while (vec[cur] <= 20) {
			++d;
			if (!vec[cur]) {
				++cur;
				continue;
			}
			fa[++cnt] = {c, p};
			self(self, cnt, 0);
		}
		deg[c].emplace_back(d);
		self(self, c, p + 1);
	};
	dfs1(dfs1, 1, 0);

	for (int c = 1; c <= cnt; ++c) bel[id[c]] = c;

	for (int t = 0; t < M; t += 20) {
		for (int i = 0; i < M; ++i) {
			if (len[i] == 1) {
				int c = i >= t && i < t + 20 ? i - t + 10 : 30;
				insert(i, c);
			} else {
				int x = i / 10, y = i % 10;
				insert(i, 10 + x);
				for (int j = 1; j < len[i]; ++j) insert(i, y);
			}
		}

		auto vec = collect();
		int cur = 0;
		auto dfs2 = [&](auto &&self, int x, int p, pii f) {
			int v = vec[cur++];
			if (v == 30) return;
			if (v >= 10) {
				if (cur == vec.size() || vec[cur] >= 10) {
					fa[++cnt] = {bel[f.first], f.second};
					id[cnt] = v - 10 + t;
					return;
				} else {
					x += vec[cur];
				}
			}
			if (cur == vec.size() || p + 1 == len[x]) return;
			self(self, x, p + 1, {x, p});
			int d = deg[bel[x]][p];
			while (d--) self(self, (vec[cur] - 10) * 10, 0, {x, p});
		};
		dfs2(dfs2, (vec[0] - 10) * 10, 0, {0, 0});
	}

	vector<int> res(N - 1);
	int tot = M;
	for (int c = cnt; c; --c) {
		int L = len[id[c]];
		nd[c].resize(L);
		for (int i = L - 2; i >= 0; --i) nd[c][i] = tot++;
		nd[c][L - 1] = id[c];
	}
	for (int c = 1; c <= cnt; ++c) {
		if (c > 1) res[nd[c][0]] = nd[fa[c].first][fa[c].second];
		for (int i = 1; i < len[id[c]]; ++i) res[nd[c][i]] = nd[c][i - 1];
	}
	return res;
}

Bonus

感觉上优化空间很大,我们可以再凹一下。

不妨形式化一点,设有 \(x\)\(i\) 满足 \(l_i\geq 2\)\(y\)\(i\) 满足 \(l_i=1\)

第一阶段保持不变,我们可以得到 \(l_i\)\(n\)

对于第二阶段,取底数 \(S\),对于 \(0\leq i<x\),将 \(i\) 表示成 \(Sa_i+b_i\),设 \(0\leq a_i<T\),且 \(0\leq b_i<S\)。对于 \(l_i=1\),将其标为 \(0\);否则将链顶标为 \(a_i+1\),其他点标为 \(T+1+b_i\)。显然这样标可以保证先遍历轻儿子,且由于 \(l_i\) 已知,我们实际上是容易知道当前点是否为叶子节点的。这部分对 \(B\) 的贡献为 \(T+S\)

对于第三阶段,重新取底数 \(P\),对于 \(0\leq i<x\),将 \(i\) 表示成 \(Pa_i+b_i\),设 \(0\leq a_i<Q\),且 \(0\leq b_i<P\)。对于 \(l_i\geq 2\),把链顶标为 \(a_i+P\),其他点标为 \(b_i\)。显然这样标可以保证先遍历重儿子,而链顶值域为 \([P,P+Q-1]\),共有 \(Q\) 个值,因此我们可以把 \(Q-1\) 个值分配给待还原的单点,剩下的值分配给其他单点。这部分对 \(B\) 的贡献为 \(P+Q-1\),对 \(K\) 的贡献为 \(\left\lceil\dfrac{y}{Q-1}\right\rceil\)

因此形式化后,最终 \(C=2+\left\lceil\dfrac{y}{Q-1}\right\rceil+\max(T+S,P+Q-1)\)

打表得出最坏情况下 \(C\leq 32\)

打表代码
int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	int mx = 0;
	for (int m = 1; m <= 200; ++m) {
		for (int x = 1; x <= m; ++x) {
			int y = m - x;

			int mn1 = inf;
			for (int S = 1; S <= 200; ++S) {
				int T = (x + S - 1) / S;
				chkMin(mn1, S + T);
			}

			int mn2 = inf;
			if (!y) mn2 = 2 + mn1;
			else {
				for (int Q = 2; Q <= 200; ++Q) {
					int P = (x + Q - 1) / Q, v = (y + Q - 2) / (Q - 1);
					chkMin(mn2, 2 + v + max(mn1, P + Q - 1));
				}
			}
			chkMax(mx, mn2);
		}
	}

	cout << mx;
	return 0;
}

于是我们成功得到了比原来优秀得多的做法。

主要代码
int len[MAXM], posX[MAXM], posY[MAXM], id[MAXM], bel[MAXM];
pii fa[MAXM];
vector<int> deg[MAXM], nd[MAXM];

bool insert(int U, int X);
vector<int> collect();

vector<int> find_structure(int M) {
	int N = 0;
	for (int i = 0; i < M; ++i) {
		while (insert(i, 0)) ++len[i];
		N += len[i];
	}
	collect();

	vector<int> X, Y;
	for (int i = 0; i < M; ++i) {
		if (len[i] >= 2) {
			posX[i] = X.size();
			X.emplace_back(i);
		} else {
			posY[i] = Y.size();
			Y.emplace_back(i);
		}
	}

	tuple<int, int, int> mn1 = {inf, inf, inf};
	for (int S = 1; S <= 200; ++S) {
		int T = (X.size() + S - 1) / S;
		chkMin(mn1, {S + T, S, T});
	}
	auto [mnv1, S, T] = mn1;

	for (int i = 0; i < M; ++i) {
		if (len[i] == 1) {
			insert(i, 0);
		} else {
			int x = posX[i] / S, y = posX[i] % S;
			insert(i, 1 + x);
			for (int j = 1; j < len[i]; ++j) insert(i, T + 1 + y);
		}
	}

	auto vec = collect();
	int cur = 0, cnt = 1;
	auto dfs1 = [&](auto &&self, int c, int p) -> void {
		int v = vec[cur++];
		if (!p) {
			id[c] += (v - 1) * S;
		} else if (p == 1) {
			id[c] += v - T - 1;
			id[c] = X[id[c]];
		}
		if (p && p == len[id[c]] - 1) return;

		int d = 0;
		while (vec[cur] <= T) {
			++d;
			if (!vec[cur]) {
				++cur;
				continue;
			}
			fa[++cnt] = {c, p};
			self(self, cnt, 0);
		}
		deg[c].emplace_back(d);
		self(self, c, p + 1);
	};
	dfs1(dfs1, 1, 0);

	for (int c = 1; c <= cnt; ++c) bel[id[c]] = c;

	if (!Y.empty()) {
		tuple<int, int, int> mn2 = {inf, inf, inf};
		for (int Q = 2; Q <= 200; ++Q) {
			int P = (X.size() + Q - 1) / Q, v = (Y.size() + Q - 2) / (Q - 1);
			chkMin(mn2, {2 + v + max(mnv1, P + Q - 1), P, Q});
		}
		auto [mnv2, P, Q] = mn2;

		for (int t = 0; t < Y.size(); t += Q - 1) {
			for (int i = 0; i < M; ++i) {
				if (len[i] == 1) {
					int p = posY[i], c = p >= t && p < t + Q - 1 ? P + p - t : P + Q - 1;
					insert(i, c);
				} else {
					int x = posX[i] / P, y = posX[i] % P;
					insert(i, x + P);
					for (int j = 1; j < len[i]; ++j) insert(i, y);
				}
			}

			auto vec = collect();
			int cur = 0;
			auto dfs2 = [&](auto &&self, int x, int p, pii f) {
				int v = vec[cur++];
				if (v >= P) {
					if (cur == vec.size() || vec[cur] >= P) {
						if (v != P + Q - 1) {
							fa[++cnt] = {bel[f.first], f.second};
							id[cnt] = Y[v - P + t];
						}
						return;
					} else {
						x += vec[cur];
						x = X[x];
					}
				}
				if (cur == vec.size() || p + 1 == len[x]) return;
				self(self, x, p + 1, {x, p});
				int d = deg[bel[x]][p];
				while (d--) self(self, (vec[cur] - P) * P, 0, {x, p});
			};
			dfs2(dfs2, (vec[0] - P) * P, 0, {0, 0});
		}
	}

	vector<int> res(N - 1);
	int tot = M;
	for (int c = cnt; c; --c) {
		int L = len[id[c]];
		nd[c].resize(L);
		for (int i = L - 2; i >= 0; --i) nd[c][i] = tot++;
		nd[c][L - 1] = id[c];
	}
	for (int c = 1; c <= cnt; ++c) {
		if (c > 1) res[nd[c][0]] = nd[fa[c].first][fa[c].second];
		for (int i = 1; i < len[id[c]]; ++i) res[nd[c][i]] = nd[c][i - 1];
	}
	return res;
}
posted @ 2026-08-12 19:58  P2441M  阅读(39)  评论(0)    收藏  举报