加载中...

2026牛客寒假基础算法训练营2 (CD)

C,D,G 这三道题都是那种看题面就感觉出得特别不错的题,遂来补题解。

D.数字积木

Q: 给定一棵大小为 \(n\) 的树,每个点有权值且构成 \(0 \backsim n - 1\) 的排列。求其所有连通子图的 \(mex\) 之和。

考虑 \(mex = x\) 的连通子图具备的性质:包含权值为 \([0, x - 1]\) 的点,且不包含权值为 \(x\) 的点。其贡献为 \(x\)

考虑计算每一种 \(mex\) 值的连通子图的贡献:这里需要一种极其巧妙的贡献换算:对于 \(\forall\) \(mex = x\) 的连通块,在计算 包含 \(0\) 的连通块个数 时,会被统计一次;在计算 同时包含 \(0,1\) 的连通块个数 时,会被统计一次;在计算 同时包含 \(0,1,2\) 的连通块个数 时,会被统计一次······在计算 同时包含 $0,1\space ...\space mex-1 $ 的连通块个数 时,会被统计一次。对于该连通块,在按上述方式统计的过程中,这个连通块一共被计算了 \(mex\) 次。于是我们会发现上述统计过程与求解原问题是完全等价的。因此问题转化成:对于 \(\forall x \in [0, n - 1]\),计算同时包含权值 \([0, x]\) 的连通块个数

先考虑计算包含权值 \(0\) 的连通块个数:可以通过对以 \(0\) 为根的树进行树形 \(dp\) 来求。假设 \(a_{root} = 0\),定义 \(dp_{u}\):在以 \(u\) 为根的子树中,包含点 \(u\) 的连通块个数,那么 \(dp_{root}\) 即为所求。状态转移:

\[dp_{u} = \prod_{v \in son[u]} (dp_{v} + 1) \]

\(res_{i}\):同时包含权值 \([0, i]\) 的连通块个数。\(res_{0} = dp_{0}\)

\(ans = \sum_{i=0}^{n-1} res_{i}\)

考虑递推 \(res_{1\backsim n-1}\)\(res_{i-1} \rightarrow res_{i}\)):设 \(st_{v} = true\) 表示点 \(v\) 已经加入到了正在维护的连通块中,\(a[u] = i\)。现在欲求 \(res_{i}\),相当于将点 \(u\) 加入到连通块中。那么显然,\(u\) 向上出发到根 \(root\) 这一整条链上的点都必须加入到连通块中(这条链上可能存在已经在连通块中的点,不在连通块的所有点构成这条链的一个后缀)。我们需要将整个连通块看作是一个点,维护好以这个点为根的新 \(dp\) 值,那么这个新 \(dp\) 值就是 \(res_{i}\)。怎么维护呢?我们考虑从上到下,将这条链中还未加入到连通块中的点依次加入到连通块中。假设现在在考虑点 \(x\)\(x\) 加入到连通块中,相当于整个连通块的子树分支集合中去除了子树 \(x\),并添加子树 \(x\) 的所有子树分支(可以在脑海中脑补这个过程,这里不再详细证明了)。去除即除以 \((dp_{x}+1)\),添加即乘以 \(\prod_{v \in son[x]}(dp_{v}+1)\)。按照这个步骤模拟即可。

至此整个题分析完毕,但是这道题还有一个坑点:“去除子树 \(x\)” 这一过程涉及到除法运算,而本题是需要在取模过程中求解的。也就是说,当 \(dp[x] + 1\)\(1e9 + 7\) 的倍数时,由数论可知, \(dp[x] + 1\) 不存在模 \(1e9 + 7\) 意义下的逆元,导致无法进行除法运算(对于模运算,当 \(x \neq 0\) 时,乘 \(x\) 与乘 \(x\) 的逆元是可以相互抵消的;而 \(x = 0\) 时,\(0\) 不存在逆元)。为了解决这个问题,我们需要手动计算在运算过程中当前结果里因子 \(0\) 的数量。若因子 \(0\) 的数量为 \(0\),结果仍是原值;否则说明结果存在因子 \(0\),结果为 \(0\)。具体实现见 code。

code

C.炮火轰炸

\(x_{i}, y_{i} \leq 1e3\),则可以通过从外侧边界 \(bfs\) 的方式处理出所有在炮火圈外侧的位置,进而 \(O(1)\) 处理每个查询。但是 \(x_{i}, y_{i} \leq 1e6\),地图范围太大,无法这样做。

考虑对于所有由雷构成的连通块,将 外侧与雷相邻的空闲位置内侧与雷相邻的空闲位置 分别打上标记 \(0\)\(1\)。显然,这些位置的数量均不会超过 \(8n\),可以暴力打标记。现在的问题在于如何快速定位到这些位置,规避掉一些非必要位置的搜索。

对于标记每个雷连通块的外侧位置,可以从位于该连通块最左上的雷的八连通左上角处开始,搜索紧贴该连通块的外侧所有空闲位置(标记 0)。由上述,搜索的总格数不超过 \(O(8n)\)。然后按照类似方式去搜索紧贴该连通块内侧的所有空闲位置(标记 1)。最后对于每个查询坐标 \((x, y)\),set + 二分 \(4\) 个方向最近的标记是否均为 \(1\) 。具体解法见 官解blog,自己补了很长时间,最后通过率也只能止步于 \(79.31\%\),核心原因在于无法将已经在炮火圈中的雷删除。以下为官解代码。

#include<bits/stdc++.h>

using namespace std;

int main(){
    int n, q;
    cin >> n >> q;
    vector<set<int>> st(1e6 + 2, set<int>());
    for(int i = 1; i <= n; i++){
        int x, y;
        cin >> x >> y;
        st[x].insert(y);
    }
    vector<array<int, 2>> d = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
    vector<map<int,int>> mp(1e6 + 2, map<int, int>());
    vector<set<int>> mx(1e6 + 2, set<int>()), my = mx;
    auto in = [&](int x, int y){
        auto itx = mx[x].lower_bound(y);
        auto ity = my[y].lower_bound(x);
        if(itx == mx[x].end() || ity == my[y].end()) return 0;
        return min(mp[x][*itx], mp[*ity][y]);
    };
    auto dfs = [&](auto &&dfs, int x, int y, int cnt)->void{
        mp[x][y] = cnt;
        mx[x].insert(y);
        my[y].insert(x);
        for(auto &[dx, dy] : d){
            int xx = x + dx;
            int yy = y + dy;
            if(xx < 0 || yy < 0) continue;
            if(st[xx].count(yy)) continue;
            if(mp[xx].count(yy)) continue;
            int tag = 0;
            for(int dx = -1; dx <= 1; dx++){
                for(int dy = -1; dy <= 1; dy++){
                    if(xx + dx >= 0) tag |= st[xx + dx].count(yy + dy);
                }
            }
            if(tag) dfs(dfs, xx, yy, cnt);
        }
    };
    for(int x = 1; x <= 1e6; x++){
        for(auto &y : st[x]){
            if(in(x, y)) continue;
            for(int dx = -1; dx <= 1; dx++){
                for(int dy = -1; dy <= 1; dy++){
                    int xx = x + dx;
                    int yy = y + dy;
                    if(xx < 0 || yy < 0) continue;
                    if(st[xx].count(yy)) continue;
                    if(mp[xx].count(yy)) continue;
                    if(min(dx, dy) >= 0) dfs(dfs, xx, yy, 1);
                    else dfs(dfs, xx, yy, 0);
                }
            }
        }
    }
    while(q--){
        int x, y;
        cin >> x >> y;
        if(in(x, y)) cout << "YES" << endl;
        else cout << "NO" << endl;
    }
}

G.宝藏拾取

woc? 手写平衡树?官解看不懂一点,还是蹲一位大佬的民间题解再补吧qwq。。。

posted @ 2026-02-07 02:26  小橘奏  阅读(43)  评论(0)    收藏  举报