题解: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;
}

浙公网安备 33010602011771号