加载中...

2026牛客多校补题汇总

记录多校里值得补的题目。

C

动态维护 \(Kruskal\) 重构树,设计更多复杂的状态

L

在 AC 自动机上做树上启发式合并

L

对于一个有向图,在点数很少,可以直接二进制状压枚举所有子集时,可以 \(O(n*2^{n})\) 求出拓扑序方案数。

int f[1<<n]; // f[state]:只考虑 state 内的点及所有出边,拓扑序方案数
int from[n]; // from[u]:存在连向 u 的有向边的所有点 v 构成的二进制集合
int indeg[n]; // indeg[u]:入度 
f[0] = 0;
for(int i = 0; i < n; i ++){
    if(indeg[i] == 0){
        f[1<<i] = 1;
    }
}
for(int state = 0; state < (1<<n); state ++){
    for(int c = 0; c < n; c ++){
        if(!(state >> c & 1)){
            if((state & from[c]) == from[c]){
                f[state | (1<<c)] += f[state];
                f[state | (1<<c)] %= mod;
            }
        }
    }
}

I

含绝对值的推式子

J

\(set\) 维护集合的启发式合并 / 左偏树高效实现可并堆

code_启发式合并

code_左偏树

C

维护关于二进制子集前缀和的 SOSdp

F

code 用到了三指针,与双指针的区别在于每次枚举某个端点 \(i\) 时,维护的是一段区间 \([l, r]\)\(i,l,r\) 构成三指针),复杂度仍为 \(O(n)\)

A_

后缀树 + 树剖

B_

闵可夫斯基和

A

神秘并查集,不过还是有迹可循的。

一个比较经典的 trick:若规定位置 \(i,j\) 上的元素可交换,那么可以看作位置 \(i, j\) 是连通的。将所有可连通的位置对用并查集合并,则每个集合内的所有位置上的元素可以任意排列。

本题并没有告诉这样的位置对有哪些,但是可以明确一点:对于 \(p_{i} = x\),只考虑 \(<x\) 的所有元素,则位置 \(i-1\) 与位置 \(i+1\) 上的元素可以交换

考虑按值的大小倒序填所有数字。填数字 \(x\) 时,\(x\) 可以填在其初始位置所在集合的任意剩余可用位置;且剩下未填的数字均 \(<x\),可以将位置 \(i - 1\) 所在集合与位置 \(i + 1\) 所在集合合并。

需要注意一点:若在合并两个集合前,发现其中某个集合没有剩余可用的位置,那么这个集合就已经封闭(里面所有元素均 \(>x\)),此时不能再将两个集合合并(该集合没有剩余可用位置可以腾挪到位置 \(i-1\)\(i+1\),两个集合不再互通。虽然没有剩余可用位置说明集合的 \(siz = 0\),看似即使合并也无影响,但是会影响后续的合并过程(这个没有剩余位置的集合作为桥梁,间接合并了均存在剩余可用位置,但不该合并在一起的两个集合))

I

二进制数位 dp,需要设计的状态以及所求信息比较复杂。

需要求:

\[\sum_{i=0}^{n} f(i) * f(i + d) \]

\(f(x)\) 表示 \(x\) 的二进制 \(1\) 个数

状态设计

由于贡献与二进制位有关,显然可以做一个以 \(n\) 为上界的二进制数位 dp(使用常用状态 \(limit\) 表示当前位是否受上界控制)。但是枚举到当前第 \(pos\) 位时,虽然可以枚举当前位的数字 \(0/1\),也知道 \(d\) 的二进制在当前位上的数字,但是我们并不能直接确定最终 \(i + d\) 在当前位上的数字是什么,因为还没有考虑低邻位对当前位的进位。因此,对于本题的数位 dp,我们必须要将每一位是否存在进位作为状态记录下来,这样就可以直接确定 \(i + d\) 在当前位上的数字是什么。

需要注意的是,对每一位设计了是否进位这一状态后,需要检验当前位的实际进位情况是否与枚举的进位情况相匹配,只有枚举与实际一致时才是合法转移。

综上,我们只需要设计 \(pos, limit, carry\) 三种状态:

\(f_{pos,limit,carry}\):第 \([0, pos - 1]\) 位上已经填完数字(按位从高到低考虑),且第 \([pos, len]\) 位在满足所有状态约束的前提下可以随意填数字,所有方案的相关信息(这里的设计见后文)

dfs 返回值的设计

既然要计算 \(\sum_{i} f(i) * f(i + d)\) 的值,显然我们不能只维护这个信息,还需要单独维护 \(\sum_{i} f(i)\)\(\sum_{i} f(i + d)\),只有共同维护这些信息才能转移。推导一会儿转移式后就会发现,还需要维护 “方案数” 这种信息。

  • \(cnt\) 表示未填的所有位的合法方案数;
  • \(i\) 的当前位(第 \(pos\) 位)数字为 \(bit1\)\(i + d\) 当前位数字(由 \(i\) 的当前位数字、\(d\) 的当前位数字与低邻位的进位三者相加得到)为 \(bit2\)
  • \(A\) 表示所有可能方案的 \(f(i)\) 之和,\(B\) 表示所有可能方案的 \(f(i + d)\) 之和;\(AB\) 表示所有可能方案的 \(f(i) * f(i + d)\) 之和
  • 对于每种方案,设 \(A_{j}, B_{j}\) 分别表示方案 \(j\)\(f(i), f(i + d)\) 的值

\(cnt, A, B, AB\) 由已经计算的可转移情形 \(pre\) 递推到当前待计算情形 \(cur\):(这里 \(pre.cnt,pre.A,pre.B,pre.AB\) 为所有源转移的汇总)

  • \(cur.cnt \leftarrow \sum_{k} pre.cnt_{k} = pre.cnt\)
  • \(cur.A \leftarrow \sum_{j=1}^{pre.cnt} (pre.A_{j} + bit1) = pre.A + bit1 * pre.cnt\)
  • \(cur.B \leftarrow \sum_{j=1}^{pre.cnt} (pre.B_{j} + bit2) = pre.B + bit2 * pre.cnt\)
  • \(cur.AB \leftarrow \sum_{j=1}^{pre.cnt} (pre.A_{j} + bit1) * (pre.B_{j} + bit2) = pre.AB + bit2 * pre.A + bit1 * pre.B + bit1 * bit2 * pre.cnt\)
const int mod = 998244353;
struct Node{
    ll cnt; // 合法方案数
    ll A; // 所有方案的 f(i) 之和
    ll B; // 所有方案的 f(i + d) 之和
    ll AB; // 所有方案的 f(i) * f(i + d) 之和
};

void solve()
{
    ll n, x;
    cin >> n >> x;

    vector<int> bit_n, bit_x;
    for(int i = 62; i >= 0; i --) bit_n.pb(n >> i & 1), bit_x.pb(x >> i & 1);
    int len = 63;

    vector<vector<Node>> f(63, vector<Node>(2, {-1, -1, -1, -1}));
    
    function<Node(int,bool,bool)> dfs = [&](int pos, bool limit, bool carry) -> Node{
        if(pos == len) return Node{(carry == 0), 0, 0, 0}; // 再后面为空,没有任何位需要考虑,f 值为 0,方案数为 1
        if(!limit && f[pos][carry].cnt != -1) return f[pos][carry];
        int up = (limit ? bit_n[pos] : 1);
        Node cur = {0, 0, 0, 0};
        for(int i = 0; i <= up; i ++){ // 决策当前位填什么数字
            for(int pre_carry = 0; pre_carry <= 1; pre_carry ++){ // 决策上一位对当前位的进位
                int s = i + pre_carry + bit_x[pos]; // i + d 在当前位的实际总贡献(可能存在进位)
                int need_carry = (s >> 1); // i + d 在当前位产生的实际进位
                if(carry == need_carry){ // 需要比较是否与枚举当前的进位一致
                    Node pre = dfs(pos + 1, limit && i == up, pre_carry);
                    int bit1 = i; // i 当前位上的值
                    int bit2 = (s & 1); // i + d 当前位上的值
                    cur.cnt = (cur.cnt + pre.cnt) % mod;
                    cur.A = (cur.A + pre.A + pre.cnt * bit1) % mod;
                    cur.B = (cur.B + pre.B + pre.cnt * bit2) % mod;
                    cur.AB = (cur.AB + pre.AB + pre.B * bit1 + pre.A * bit2 + pre.cnt * bit1 * bit2) % mod;
                }
            }
        }
        if(!limit) f[pos][carry] = cur;
        return cur;
    };
    cout << dfs(0, true, 0).AB << "\n";
}

J

糖水不等式 + 凸包二分 trick + 分块

这里主要记录凸包二分 trick。

凸包二分 trick:要找到查询点 \((x, y)\) 关于某个点集内所有点的最大斜率,显然一定是该点集的凸包上某一点(若不是,则对应直线一定会将凸包切分成两个部分,总是存在某一侧凸包上的点更优)。

只需要找到点集内的某个点,使得所在直线右侧不存在点集内的任意点,该点即为所求(斜率最小时改成左侧即可,证明可以自己手玩)。

二分的 check 写法:利用下凸包(或上凸包)斜率的单调性,对形成凸包的单调栈进行二分,每次检查当前二分到的凸包边所在直线与查询点的位置关系,找到一对相邻且相对位置关系不同的边,它们的交点即为所求。

A

利用了二进制高位贪心的性质,比较常见与巧妙。

C

有关拓扑排序的图论建模问题。这里需要利用异或约束,将所有异色二元组作为点,\(0/1\) 边权代表连接的两个二元组的偏序关系是否相同(每个点也有权值 \(0/1\),表示左/右哪一边更大)。建好无向图后进行类似二分图染色的 DFS,得到一种满足全部约束的可行方案,进而得到所有待排序对象两两的偏序关系,然后就可以建出有向图进行拓扑排序,得到答案。

I_

题解给的 \(O(n)\) 解法很神秘,不过线段树优化建图应该也可以做。

C_

数论题

E

后缀树上做树形 dp。后缀树的建立可以用 SA 或 SAM,这里把两种方法都学一下:后缀树

dp 转移部分 (SA解法)

pmzW4Qx.png

code_SA建后缀树
code_SAM建后缀树

关于本题 SAM 解法更详细的题解可以见这篇:blog

F_

比较复杂的树上问题,和树的直径有关。

K_

应该是比较简单的费用流

L_

很难的博弈题,与 SG 函数有关,感觉值得补一下。

A_

构造

C_

神秘结论题

D

维护集合的启发式合并,但不是特别板。

核心在于:如果存在多个人,在仍存活的情况下同时到达了同一个点,则这些人均会消失。故对于每棵子树,只需要启发式合并维护有关到达时间的集合,合并过程中若存在两个人属于同一时间,则删除该到达时间的所有人。这样每种时间只会容纳最多一个人,答案即为结点 \(1\) 剩下的所有人。

由于每种时间恰好对应了某个人,故使用 map 维护集合是最好的选择。

code_启发式合并

还可以用线段树合并来做,思路与实现方式与启发式合并是类似的。

code_线段树合并

F_

set 维护五元组合并

I

简单的写法是根号复杂度暴力枚举,但是二分答案套三分 check 的方法同样值得记录。

需要稍微复杂些的推式子,推导出二次项系数的正负,进而确定函数为上凸还是下凸。

code_二分+三分

K_

看过题人数是不可做题,但是知识点包含期望 + 线段树分治 + NTT,感觉值得补。

L_

unknown

B

考察概率论、微积分。

首先考虑如何计算第 \(1\) 轮的贡献:即对于 \(n\) 个取值范围在 \([0, m]\) 之间任意实数的随机变量取最大值。

由于取值范围是实数而不是整数,可以考虑用微积分求解:设当前最大值为 \(x\)\(x\) 取最大值的概率为 \(p_{x}\),则答案为:

\[\int_{0}^{m}x*p_{x} \]

其中 \(p_{x} = n * (\frac{x}{m})^{n-1}*\frac{dx}{m}\)

  • 枚举 \(n\) 个变量中的哪个作为最大值 \(x\)
  • 该变量取 \(x\) 的概率为 \(\frac{dx}{m}\)
  • 其他 \(n-1\) 个变量 \(\le x\) 的概率为 \((\frac{x}{m})^{n-1}\)

因此 \(k = 1\) 时,答案为 \(\frac{nm}{n+1}\)

现在考虑如何求解 \(k > 1\) 时的情况:注意到,对于原有随机变量与新生成随机变量的取值,与每次拿取哪个变量是无关的。 因此,每次从 \(n\) 个随机变量中取最大值,取 \(k\) 次,等价于从 \(n + k - 1\) 个随机变量中取前 \(k\)。而每个变量是在 \([0, m]\) 内独立均匀分布的,因此可以看作 \(n + k - 1\) 个随机变量,将 \([0, m]\) 分割成 \(n + k\) 条线段,每条线段长度均为 \(\frac{m}{n + k}\)(满足独立均匀分布条件),且第 \(i\) 大的随机变量取值为 \(\frac{n+k-i}{n+k}\)。对前 \(k\) 大求和即可。

E_

神秘题,好像在省赛见过类似形式的题

F_

SAM + 回滚莫队

G_

三角剖分、闵可夫斯基和

J_

点双 + 线段树分治

K

从这道题学到了线性递推 \(1\backsim n\) 每个数的 \(popcount\) trick:

// 递推式
f[i] = f[i - lowbit(i)] + 1

直接 \(O(2^{3n}*(3n)^{3})\) 的状压 dp 显然超时,需要尽可能地缩减无效状态的枚举:

  • 二进制状态 \(state\)\(1\) 的数量只有是 \(3\) 的倍数时才有效
  • 考虑转移 \(state\) 时,无需三重循环枚举 \(i,j,k\)。由于已经选中的队员必然已经分配到了某一队,故只需要枚举某个固定队员,再枚举他的两个队友是谁即可,这样不会漏掉需要转移的状态,且只需要二重循环枚举。
posted @ 2026-08-06 10:51  小橘奏  阅读(10)  评论(0)    收藏  举报