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;
}
posted @ 2026-08-29 22:28  tangtianyao0123  阅读(21)  评论(3)    收藏  举报