IOI2026
前提紧要:本套题只有 D1T3 是我独立做出来的,其它都对着 AI 才理解了题解,代码可能和题解有些相似,但是可以保证是我自己写的。请大家不要在讨论区随意膜拜或者发布极端恶评谢谢。
D1T1
个人认为比较详细的一篇题解。
47pts
我们可以从大到小对于所有的叶节点 \(x\) 不断插入权值为 \(x\) 的球,直到不能插入位置,于是我们发现这一步对这棵树进行了链剖分。
然后我们注意到,如果存在 \(p_1<p_2<p_3<\cdots <p_k\),满足 collcter 函数返回序列的第 \(p_i\) 为 \(x\),则这些节点构成 \(1\sim x\) 的链,于是 \(p_i\) 为 \(p_{i+1}\) 的父亲。证明可从球的移动过程直接得到。
100 pts
\(47\) 分的做法虽然直观,但每个叶子用一个唯一值会导致 \(B\) 过大。为了减少 \(B\),我们需要让多个叶子共用一套值域,这就是下面分块思想的由来。
首先,对于每个叶子 \(x\),反复调用 insert(x, 0) 直到不能继续插入,这样子我们可以得到 \(1\sim x\) 的链的长度 len[x]。考虑对于 len[x] > 1 的叶子和 len[x] = 1 的叶子分开处理。
len[x] > 1
我们可以插入插入一个 \(x\) 在这条链的顶端,插入一些 \(y\) 在链中间,插入一个 \(z\) 在叶子。那么令 \(x < y < z\),我们就能惊喜的发现,这一条链的 collcter 函数返回的序列中是这样子的一个状态:\([x, \cdots, y, \cdots, y, \cdots, z]\),如果把 \(y\) 看成 #,\(x\) 看成 (,\(z\) 看成 ),这样这个序列就是一个匹配的括号序列,并且匹配的两个括号一定为一条链的顶端和叶子。
我们假设一共有 \(a\) 个 len[x] > 1 的叶子节点,为了平衡值域和操作次数,令 \(k = \lceil\sqrt a\rceil\)。然后写出带余除法 \(x\div k=p\cdots q\),令 \((x,y,z)=(p,k,k+q+1)\)。
这样的好处有两个:
- 由于 \(k\ge \sqrt a\),于是 \(k > p\),满足单调性。
- 如果知道了 \(x,z\) 就可以通过 \(x=kp+q\) 的柿子反推出编号。
len[x] = 1
仿照上面的方法,取 \(k'=\lceil\sqrt b\rceil\),\(b\) 表示 len[x] = 1 的叶子节点的个数。
然后写出带余除法 \(x\div k'=p\cdots q\),考虑枚举 \(p\),向节点 \(x\) 插入 \(q\),如果在括号匹配的过程中找到了这个 \(q\) 就能够快速反推出 \(x\)。
由于 \(q\) 的值域只有 \(k'\) 种,而 \(p\) 通过枚举区分,所以不同叶子的 \(q\) 可能相同,但在同一轮枚举中,我们只会插入那些 \(p\) 等于当前枚举值的叶子,因此 \(q\) 相同不会混淆——因为同一个 \(q\) 值最多对应一个叶子,而且 collect() 对子节点排序时,相同值的多个球可能顺序任意,但我们在后续处理中通过栈匹配可以唯一确定每个叶子对应的位置,不会产生歧义。
处理序列
我们说了,这个序列是一个括号序,于是考虑使用栈来处理它。为了区分两类叶子节点,可以对第一列插入的时候进行权值的平移。然后对于每个节点就能偶显然易见的判断出来它是哪一种节点。
-
如果是
len = 1类叶子,直接还原编号挂在栈顶下面。 -
如果是
len > 1类链顶,直接压栈并且记录商。 -
如果是
len > 1类分隔符,压栈后记录它为分隔符。 -
如果是
len > 1类叶子:- 根据栈记录的第二维跳过所有的分隔符,直接遍历栈找到链顶并还原叶子编号(不要弹出)。
- 把叶子挂在栈顶的下面。
- 把链上的节点弹出并且记录他们属于那个链和深度方便转移编号。
复杂度分析
\(C=2\sqrt a+2\sqrt b+1=2\sqrt a+2\sqrt{m-a}+1\),最大值当 \(2a=m\) 时取到,此时 \(C=41\),可以通过。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, m; // n: 总节点数,m: 叶子数
int len[N]; // len[i]: 叶子 i 到根的链长
int id[N], mod[N][2]; // mod[i][0] = 商, mod[i][1] = 余数 (分块用)
int fa[N][2]; // fa[i][0]: 最终父节点;fa[i][1]: 临时存序列中相邻关系
int stk[N][2]; // 栈,[0]存节点下标,[1]存附属信息(商或分隔标记)
int pos[N][2]; // pos[i][0]: 属于哪个叶子链,pos[i][1]: 在该链中的深度
bool hav[N]; // hav[i] 标记叶子 i 是否还没确定父节点
int n1, n2; // n1: 分隔符权值;n2: len == 1 叶子的轮数上限
int k1, k2; // k1: len > 1 的分块大小;k2: len == 1 的分块大小
vector<int> a, b; // a: len > 1 的叶子;b: len == 1 的叶子
vector<int> qwq[3010]; // qwq[leaf][depth] = 该链上深度为 depth 的节点编号
vector<int> res, ans; // res: collect() 返回值;ans: 最终父节点数组
bool insert(int U, int X);
std::vector<int> collect();
// 处理一次 collect 返回的序列 res,模拟括号匹配,更新 fa
void update(int u) {
int top = 0;
for (int i = 0; i < (int)res.size(); i++) {
fa[i][1] = -1;
int x = res[i];
// len == 1 叶子:直接挂到栈顶节点下
if (x < k2) {
fa[b[u * k2 + x]][0] = stk[top][0];
}
// len > 1 链顶:压栈并记录商
else if (x < n1) {
fa[i][1] = stk[top][0];
stk[++top][0] = i;
stk[top][1] = x - k2 + 1;
}
// 分隔符:压栈标记
else if (x == n1) {
fa[i][1] = stk[top][0];
stk[++top][0] = i;
stk[top][1] = 0;
}
// len > 1 叶子:弹出分隔符,找到链顶,还原叶子编号
else {
int ls = stk[top][0];
int otp = top;
while (!stk[top][1]) top--;
int nw = k1 * (stk[top][1] - 1) + (x - n1 - 1);
if (hav[a[nw]]) fa[a[nw]][0] = ls;
top = otp;
int dep = len[a[nw]];
while (!stk[top][1]) {
pos[stk[top][0]][0] = a[nw];
pos[stk[top][0]][1] = --dep;
top--;
}
pos[stk[top][0]][0] = a[nw];
pos[stk[top][0]][1] = --dep;
stk[top--][1] = 0;
}
}
// 将叶子直接连接到对应链上的节点
for (int i = 0; i < m; i++) {
if (fa[i][0] != -1 && hav[i]) {
fa[i][0] = qwq[pos[fa[i][0]][0]][pos[fa[i][0]][1]];
hav[i] = 0;
}
}
// 处理序列中相邻节点的父子关系
for (int i = 0; i < (int)res.size(); i++) {
if (fa[i][1] != -1) {
int v = qwq[pos[i][0]][pos[i][1]];
int f = qwq[pos[fa[i][1]][0]][pos[fa[i][1]][1]];
fa[v][0] = f;
}
}
}
std::vector<int> find_structure(int M){
n = 0, m = M;
// 第一次试探,求每个叶子的链长
for(int i = 0; i < m; i++) {
while (insert(i, 0)) len[i]++;
if (len[i] > 1) a.push_back(i);
else b.push_back(i);
n += len[i];
hav[i] = 1;
fa[i][0] = -1;
}
// 给每条链分配实际节点编号(从根往下,根固定为 n-1)
int idx = n - 1;
for (int i = 0; i < m; i++) {
qwq[i].push_back(0);
for (int j = 1; j < len[i]; j++)
qwq[i].push_back(idx--);
}
ans.resize(n - 1);
collect(); // 清空机器
int A = a.size(), B = b.size();
k1 = max(1, (int)sqrtl(A));
k2 = max(1, (int)sqrtl(B));
// 预处理每个叶子的商和余数
for (int i = 0; i < A; i++) {
id[a[i]] = i;
mod[a[i]][0] = i / k1;
mod[a[i]][1] = i % k1;
}
for (int i = 0; i < B; i++) {
id[b[i]] = i;
mod[b[i]][0] = i / k2;
mod[b[i]][1] = i % k2;
}
// n1 为分隔符权值(大于所有链顶权值),n2 为枚举轮数
n1 = k2 + (A - 1) / k1 + 1;
n2 = max(0, (B - 1) / k2);
// 主循环:每轮插入所有 len > 1 叶子,再插入当前轮次的 len == 1 叶子
for (int i = 0; i <= n2; i++) {
for (int x : a) {
insert(x, k2 + mod[x][0]); // 链顶 '('
for (int j = 2; j < len[x]; j++)
insert(x, n1); // 分隔符 '#'
insert(x, n1 + 1 + mod[x][1]); // 叶子 ')'
}
for (int x : b) {
if (mod[x][0] == i)
insert(x, mod[x][1]); // len == 1 叶子,权值为余数
}
res = collect();
update(i);
}
for (int i = 0; i < n - 1; i++)
ans[i] = fa[i][0];
return ans;
}

浙公网安备 33010602011771号