第七届传智杯全国IT技能大赛程序设计赛道 国赛(总决赛)—— 题解

A、研究生组用题:CDEFGH
B组用题:BCDEFG
C组用题:ABCDEF

补题链接:https://ac.nowcoder.com/acm/contest/108576

以下的代码均只给出了核心逻辑部分,并不是完整的代码。

同时代码中的 assert() 语句均为 std 编写过程中,充当validator的语句,提交时可以不写。

为了不产生歧义,这里给出以下 题代码的头文件部分:

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef __int128 lll;
typedef pair<ll, ll> P;
#define x first
#define y second
#define int long long

const int mod = 1e9 + 7;
const int pp = 998244353;

const int dx[8] = {-1, 0, 1, 0, -1, -1, 1, 1}, dy[8] = {0, 1, 0, -1, -1, 1, -1, 1};
const int ddx[8] = {1, 1, 2, 2, -1, -1, -2, -2}, ddy[8] = {2, -2, 1, -1, 2, -2, 1, -1};

ll ksm(ll a, ll b, ll p) {
    ll ans = 1;
    a %= p;
    while(b) {
        if(b & 1) ans = (ans * a) % p;
        b >>= 1;
        a = (a * a) % p;
    }
    return ans % p;
}

A. 小苯的石子游戏

题意:

两个人轮流取石子,每次要么从奇数位置的石子堆里各取一个;要么从偶数位置的石子堆里各取一个;如果两种取法都没法取,则该玩家输掉游戏。 判断谁胜谁负。

知识点:

思维,博弈,模拟。

题解:

注意到,取不了的情况实际上是对应着要取的位置中存在数字 的情况。也就是说要取的石子堆里没有石子了,就取不了了。

那我们首先考虑一个简单的子问题:给定一个数组,两人轮流操作,每个人每次的操作都是给所有数字减一。问谁能先让数组出现

显然结果是固定的,即我们只需要看数组的最小值即可,如果其是奇数,则先手可以,最小值是偶数则是后手可以。

那么考虑,我们本题实际上就是两个上述子问题加起来的结果,因此我们直接分别统计奇偶位置的最小值,加起来判断奇偶性即可。

代码:

void solve() {
    int n;
    cin >> n;
    assert(n <= 2e5);
    int odd = 1e9, even = 1e9;
    for(int i = 1, x; i <= n; i++) {
        cin >> x;
        assert(1 <= x && x <= 1e9);
        if(i & 1) {
            odd = min(odd, x);
        } else {
            even = min(even, x);
        }
    }
    if((odd + even) % 2) {
        cout << "BEN" << endl;
    } else {
        cout << "GEGE" << endl;
    }
}

时间复杂度:

B. 小苯的木棍切割

题意:

给定一个数组,每次所有数字减去数组中的最小值,数组中为 的数字被删除。

求上述过程中,单次操作减去的数字总和最大的一次减去的数字总和。

知识点:

枚举,

题解:

由于每次减去的是数组中的最小值,因此我们直接对数组排序,每次所有数字砍掉

我们会发现,第一次每个数字都减去 ,第二次每个数字都减去 ,但此时 是第一次操作之后的,因此实际上是 ,第三次减去的是 ,但此时 是前两次操作后剩下的,实际上我们会发现就是

因此我们得出结论,第 次操作,所有数字减去的都是

而第 次操作有 个数字参与减法了,因此我们枚举一遍取两者乘积的 即可。

代码:

void solve() {
    int n;
    cin >> n;
    assert(n <= 2e5);
    vector<int> a(n + 1);
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        assert(a[i] <= 1e9);
        assert(a[i] >= 1);
    }
    sort(a.begin() + 1, a.end());

    int ans = 0;
    for(int i = 1; i <= n; i++) {
        ans = max(ans, (a[i] - a[i - 1]) * (n - i + 1));
    }
    cout << ans << endl;
}

时间复杂度:

C.大苯营

题意:

多组查询,给定正整数 ,找出 ,满足: 恰好能构成一个非退化三角形。

知识点:

构造,位运算,

题解:

小清新构造题,注意到,我们考虑一些特殊的情况,比如等腰三角形,因此我们尝试从三个数字中取两个相等的出来。

不难发现可以取 ,当 的二进制没有任何交集时(即 时),

此时有:

同时有:

因此一定满足:两边之和大于第三边

一定能构成三角形。

因此我们只需要取整数 以内的任意 ,答案 就可以取 ,当然,把这些 位全取上也是可以的。

代码:

void solve() {
    int x;
    cin >> x;

    int y = 0;
    for(int i = 30; i >= 0; i--) {
        if(x >> i & 1) {
            
        } else {
            y |= (1LL << i);
        }
    }
    if(y == 0) {
        y = -1;
    }

    cout << y << endl;
}

时间复杂度:

(如果不存在 位,则无解,即此时 的二进制全为 。)

D. 小苯的排列数

题意:

定义一个数字是 "排列数",当且仅当这个数字的十进制每一位提出来,分离成一个数组以后,数组恰好是一个 到 "数位长度"的排列。 多组询问每次给定区间,找任意一个区间内的 "排列数" 并输出。

知识点:

全排列枚举,二分 / 分类大讨论,贪心。

题解:

首先本题应该有一个分类讨论+贪心的做法,从 最高位开始每次尝试取最小的,如果没办法取下一个则回溯反悔的做法。该做法实现比较困难,且细节繁多,代码量大。

这里给出一个简单做法,实际上我们会发现 "排列数" 很少,只有 这么多个。

因此我们可以直接进行一个预处理,将所有的排列数都搜出来,这一步使用 函数即可,存入数组后排序,接下来每次查询只需要二分不小于 的最小 "排列数",只要结果在 以内即可。

代码:

vector<int> v;
void init() {
    auto trans = [&](vector<int> & p) -> int {
        int ans = 0;
        for(auto & x : p) {
            ans = (ans * 10) + x;
        }
        return ans;
    };

    for(int len = 1; len <= 9; len++) {
        vector<int> p;
        for(int j = 1; j <= len; j++) {
            p.emplace_back(j);
        }
        do {
            v.emplace_back(trans(p));
        } while(next_permutation(p.begin(), p.end()));
    }
    sort(v.begin(), v.end());
}

void solve() {
    int L, R;
    cin >> L >> R;
    auto it = lower_bound(v.begin(), v.end(), L);
    if(it != v.end() && *it <= R) {
        cout << *(it) << endl;
    } else {
        cout << -1 << endl;
    }
}

时间复杂度:。(其中 )。

E. 小苯的字符串染色

题意:

给定 串,要求恰好对 个不同的位置进行 (反转)操作,求最终连续相同段的最大长度。

知识点:

思维,分类讨论,贪心,双指针。

题解:

首先,由于题目求的是纯色子串,而只有两种颜色,因此我们不妨钦定求最长白色子串(即 '')的长度。实现了这一逻辑后,我们把输入的串全部 一遍再求一次,就得到了求最长黑色的答案。

因此我们只需要实现求最长连续 的长度的逻辑即可。

我们发现,题目要求的是,要恰好染 个不同位置,"恰好" 和 "不同" 都是关键点,我们一起来看。

如果串中 的个数小于等于 ,那么贪心地,不难发现的是我们肯定全部用这 次机会把一些 变成 ,而不会去碰那些已经固定的 。 因此在这样的情况下,问题变成了,我们要选择一个尽可能长的区间,使得区间中恰好包含 ,把这些 染色就得到了这个区间长度作为答案。 这实际上是一个非常经典的问题,我们直接双指针维护即可。

那么问题来到了,如果 大于 的个数呢? 此时,由于我们是要 尽可能多,因此我们肯定会先把所有 变成 ,相当于字符串变成全 ,再把 减去原本 的个数。 此时,对于这个全 的字符串,由于 还没用完,因此我们不得不选择一些 把它们变成

由于选择的位置不能重复,因此我们发现原本数组中的 是不能选的,问题变成了: 有一些位置上的 (原本是 的位置)我们需要选择 个,把他们变成 ,最大化连续 的长度。 这个问题,我们反向考虑即可,实际上是,选择一段尽可能长的区间,使得区间中恰好包含了 。并且这些 都是原本在字符串中就是 的那些位置。(其中 是原本字符串中 总的个数。) 那么到这里相信问题已经变的十分明朗了,这个问题本质上和上一种情况一致。都是用双指针维护区间,使得区间中恰好包含若干个

最终的区间长度就是答案。

做好上述函数功能,我们 一遍字符串再次求解取 就是答案了。

代码:

void solve() {
    int n, K;
    string s;
    cin >> n >> K >> s;
    
    auto work = [&](string s) -> int {
        s = " " + s;
        int X = 0, Y = 0, k = K;
        for(int i = 1; i <= n; i++) {
            X += (s[i] == '0');
            Y += (s[i] == '1');
        }

        int ans = 0;
        if(k > X) {
            k -= X;
            Y -= k;
            // cerr << "Y : " << Y << ", k : " << k << endl;
            int cnt = 0;
            for(int i = 1, j = 1; i <= n; i++) {
                cnt += (s[i] == '1');
                while(cnt > Y && j <= i) {
                    if(s[j] == '1') {
                        cnt -- ;
                    }
                    j ++ ;
                }
                if(cnt == Y && j <= i) {
                    ans = max(ans, i - j + 1);
                }
            }
            return ans;
        }

        X = 0;
        for(int i = 1, j = 1; i <= n; i++) {
            X += (s[i] == '0');
            while(X > k && j <= i) {
                if(s[j] == '0') {
                    X -- ;
                }
                j ++ ;
            }
            if(j <= i && X == k) {
                ans = max(ans, i - j + 1);
            }
        }
        return ans;
    };

    int ans = work(s);
    for(int i = 0; i < n; i++) { // flip 一遍
        s[i] ^= 1;
    }
    ans = max(ans, work(s));

    cout << ans << endl;
}

时间复杂度:

F-小苯的物理小球

题意:

给定二维平面上一些和 轴平行的线段,再给定一些自由落体的小球坐标,表示每次查询。如果小球落在线段上,会有 的概率向左/右移动,从线段的一端滚下继续自由落体,直到落在地面上

查询所有小球落在地面上时 横坐标的期望之和。

知识点:

,二分,双指针,离线 / 建 跑拓扑排序 / 记忆化搜索。

题解:

本题有很多做法,例如建 ,在 ,再回答每次查询。 这里给出 的做法,用 维护 数组。

实现上,我们开一个 来维护所有可能 状态,实际上是一个 ,存 小球横坐标, 这个横坐标的期望

首先我们离线所有的查询,然后我们对整个过程按 从大到小,即从上到下维护,每次枚举我们都把上次枚举到这次之间的所有查询的坐标塞入 ,对应期望是

我们只枚举那些有线段的层,这样一来枚举的层数就不会超过 ,对于一层的所有线段,我们枚举所有的线段,在维护的 中找到被当前线段包含的所有点的坐标和对应期望,将期望求和后除以二,分别变成 这两个点扔到 里,即给 扔: 这两个状态。

不停枚举到 即可。

最终我们将 中所有的状态对应的横坐标乘上对应的期望求和即是答案。

本题难点在代码实现。

代码:

void solve() {
    int n, m;
    cin >> n >> m;
    map<int, vector<P>> mp;
    for(int i = 0, l, r, y; i < n; i++) {
        cin >> l >> r >> y;
        mp[y].emplace_back(l, r);
    }
    int inv = ksm(2LL, MOD - 2, MOD);
    map<int, vector<int>> point;
    for(int i = 0, x, y; i < m; i++) {
        cin >> x >> y;
        point[y].emplace_back(x);
    }
    
    vector<vector<P>> a;
    vector<int> step;
    for(auto & [y, v] : mp) {
        sort(v.begin(), v.end());
        a.emplace_back(v);
        step.emplace_back(y);
    }
    while(step.size() && step.back() > (point.rbegin()->first)) {
        step.pop_back();
        a.pop_back();
    }
    reverse(a.begin(), a.end());
    reverse(step.begin(), step.end());
    int N = a.size();

    vector<int> point_y;
    vector<vector<int>> point_x;
    for(auto & [y, p] : point) {
        point_y.emplace_back(y);
        point_x.emplace_back(p);
    }

    set<P> dp;
    for(int i = 0; i < N; i++) {
        auto & v = a[i];
        int Y = step[i];
        while(point_y.size() && point_y.back() >= Y) {
            for(auto & x : (point_x.back())) {
                dp.emplace(x, 1);
            }
            point_x.pop_back();
            point_y.pop_back();
        }
        for(auto & [l, r] : v) {
            P lp = {l + 1, -1};
            P rp = {r, -1};
            auto L = dp.lower_bound(lp), R = dp.lower_bound(rp);
            // if(L == R) continue;
            vector<P> del;
            int sum = 0;
            for(auto it = L; it != R; it++) {
                sum += it->second;
                sum %= MOD;
                del.emplace_back(*it);
            }
            sum = (sum * inv) % MOD;
            for(auto & p : del) {
                dp.erase(p);
            }
            dp.emplace(l, sum);
            dp.emplace(r, sum);
        }
    }
    while(point_y.size()) {
        for(auto & x : point_x.back()) {
            dp.emplace(x, 1);
        }
        point_x.pop_back();
        point_y.pop_back();
    }

    int ans = 0;
    for(auto & [x, y] : dp) {
        ans += x * y % MOD;
        ans %= MOD;
    }
    assert(ans <= 1e9);
    cout << ans << endl;
}

时间复杂度:

本题还有一些很好实现的做法,读者可以自行思考。

G. 小苯的地下城寻宝

题意:

给定 点的树,每个点有一个数字,现要选一个点集,满足以下所有条件 :

号点一定选;

点集中所有点的深度不同;

按照深度对点集排序后,相邻两点的数字都满足 ,即不互素;

求不同的点集个数。

知识点:

,容斥,

题解:

不难发现地下城实际上是一棵树,因为我们只对不同的深度对应的点感兴趣,因此我们先跑个 可以直接按照深度 将点都存起来,本质上其实就是 序,存到一个二维数组 中,其中 就表示所有深度为 的点们组成的

接着我们把这个数组当成一个类似二维矩阵的东西,每个 实际上都是二维矩阵中的一列,只是与之不同的是 这个”矩阵“每一列的长度不一定相同,我们对每一列单独做 转移。

具体的,我们定义 表示考虑到 号点并且路径以 号点结尾(即最后一个取的是 号房间的宝藏)的方案数,则我们转移需要用上层的 值来转移,但这里我们并不能直接暴力转移,否则再枚举上层点后复杂度会退化到平方级别,我们考虑存储一个 数组,其中 表示考虑到上一层结束的时候,所有 值(即宝藏数量)为 的倍数那些房间的 值之和。这样一来我们转移时只需要枚举 的因子 ,将其对应的 加上即可,这里需要容斥的转移,因此我们提前处理一个容斥系数 即可。

在将一整层的 都转移完后,我们还需要更新 数组,因此我们需要一个 来记录这一层所有 值对 的贡献。

执行完所有转移后, 就是答案,注意由于第一个点必须选,因此我们初始化 即可。

代码:
const int m = 1e5;
vector<int> coef(m + 1, 1);
vector<vector<int>> d(m + 1);
void init(int n) {
    coef[1] = 0;
    for(int i = 1; i <= n; i++) { // 预处理 j 的所有因子 d[j]
        for(int j = i; j <= n; j += i) {
            d[j].emplace_back(i);
            if(j > i) {
                coef[j] -= coef[i]; // 预处理容斥系数
            }
        }
    }
    int idx = -1, mx = 0;
    for(int i = 1; i <= n; i++) {
        if(d[i].size() > mx) {
            mx = d[i].size();
            idx = i;
        }
    }
}

void solve() {
    int n;
    cin >> n;
    assert(n <= 1e5);
    vector<int> a(n + 1);
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        assert(a[i] <= n);
        assert(a[i] >= 1);
    }

    vector<vector<int>> t(n + 1);
    vector<int> fa(n + 1);
    for(int i = 1, f; i <= n; i++) {
        cin >> f;
        fa[i] = f;
        assert(f >= 0);
        if(i == 1) {
            assert(f == 0);
        }
        assert(f <= n);
        t[f].emplace_back(i);
    }

    vector<int> dep(n + 1, -1);
    vector<vector<int>> g(n + 1);
    queue<int> q;
    q.emplace(1);
    while(q.size()) {
        auto u = q.front();
        dep[u] = dep[fa[u]] + 1;
        g[dep[u]].emplace_back(u);
        q.pop();
        for(auto & v : t[u]) {
            q.emplace(v);
        }
    }

    while(g.back().size() == 0) {
        g.pop_back();
    }

    vector<ll> dp(n + 1), s(n + 1);
    // dp[i] : 以 i 号点结尾的所有合法路径条数
    // s[j] : 一层层考虑到当前时,a 值为 j 倍数的 dp 值之和
    dp[1] = 1;
    for(auto & vec : g) {
        unordered_map<int, ll> ns;
        for(auto & u : vec) {
            for(auto & fac : d[a[u]]) {
                dp[u] += s[fac] * coef[fac];
                dp[u] = (dp[u] % MOD + MOD) % MOD;
            }
            if(dp[u] > 0) {
                for(auto & fac : d[a[u]]) {
                    ns[fac] += dp[u];
                    ns[fac] %= MOD;
                }
            }
        }
        for(auto & [dp_val, cnt] : ns) {
            s[dp_val] += cnt;
            s[dp_val] %= MOD;
        }
    }

    ll ans = 0;
    for(int i = 1; i <= n; i++) {
        ans += dp[i];
        ans %= MOD;
    }
    cout << ans << endl;
}

/*



*/

signed main () {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    init(m);
    int _ = 1;
    cin >> _;
    while(_ -- ) {
        solve();
    }
    return 0;
}

时间复杂度:,其中 表示 以内因子个数最多数字的因子, 的复杂度源自 ,本题没有卡 ,实测会比 快一些。

H. 智乃的

题意:

给定 点的树,求满足端点权值差不大于 的所有简单路径的极差之和

知识点:

启发式合并,并查集,线段树,笛卡尔树,(树分治)

题解:

首先可以一眼树分治,但是代码实现起来难度较高,可以使用毛毛虫火箭战术硬堆数据结构,验题人使用了树分治方法通过。

首先注意到 ,即极大值和极小值没必要同时算出,可以先求所有路径的极大值计算正贡献,减去所有路径的最小值计算负贡献。

然后接下来我们像剥洋葱一样,做如下的思维过程

将树上问题放到数组中考虑

思考一个限制条件的参数为特殊值时的更简单版本问题

对于这个题来说, 是常见的解题思路,毕竟一个 问题如果数组上解不了,那么树上一定解不了,因为树包括单链。对于 考虑参数的特殊值,无非就两个, 或者 ,这里考虑 对于解题没有太大帮助,会使问题转换成 ,所以考虑 的情况,即没有限制条件,改成求全部的区间/简单路径。

的情况

转化后的更简单版本的问题是:在数组上求

还是拆开,先求 ,这个问题显然是一个分治算法,但这个分治貌似并不需要从数组的中间切开,因为这样做很麻烦,需要维护前后缀的多个信息。但是只要每次找到数组的最大值 ,假设它的下标是 ,数组长度是 ,那么 一定是 个区间的最大值,并且求完以后数组被分成了两个新的区间,然后继续递归处理。

该算法过程也被称为笛卡尔树,是一种将 问题与 问题相互转化的手段。

的情况

注意到刚刚 的情况,实际上我们没有实际遍历分治划分的子区间,我们直接得到了 ,现在有一个 的限制条件,那我们可以做一个完整的分治算法,将分治区间的其中一侧使用权值线段树维护,另一侧查询权值线段树 这段区间有多少元素。

做完了么?

并没有,因为这个分治算法并不是从中间均匀切开,这将导致不允许我们遍历分治两侧的较大区间,否则复杂度将退化为

考虑这么一个经典问题,每次合并两段区间,但是合并的时候需要遍历较小一侧区间,时间复杂度是,这种思想被称为启发式合并

所以可以在一开始,就先建好 棵线段树,然后在分治返回合并区间的时候,暴力遍历尺寸较小的线段树,将线段树中的元素往大线段树里面合并。

本题的两种代码实现

使用多颗动态开点线段树,或者使用线段树合并的技巧

建立真的笛卡尔树,在笛卡尔树上使用 算法,这样的好处是不需要动态开点,并且只有 棵线段树,且该写法的常数较小

考虑放到树上

注意到这个算法好像天然就支持树上,无需编写额外代码支持树上操作。

从上述思考过程中得到的启示,既然数组上并不一定中分,而是从极值处分开更好实现,那么放到树上,同样也是不使用重心做树分治,而是按照极值切分

本题的核心 是在分治算法中,如果不需要对分治较大一侧做枚举操作,则没必要均等划分两个分治的子集,尤其是在考虑 的场景下,按照极值划分会使问题变得更简单。

代码:

#include <bits/stdc++.h>

using namespace std;
const int MAXN = 200005;
const int M = 6e4;

class segmentTreeMgr
{
private:
    struct Node
    {
        int sum, ch[2];
    };
    int tot, _N;
    vector<Node> data;
    stack<int> stk;

    int _query(const int root, const int l, const int r, const int L, const int R)
    {
        if (!root)return 0;
        if (L == l && R == r)return data[root].sum;
        int mid = (L + R) >> 1;
        if (r <= mid)return _query(data[root].ch[0], l, r, L, mid);
        else if (l > mid)return _query(data[root].ch[1], l, r, mid + 1, R);
        else return _query(data[root].ch[0], l, mid, L, mid) + _query(data[root].ch[1], mid + 1, r, mid + 1, R);
    }

    void _dump(const int root, vector<pair<int, int>> &v, const int L, const int R)
    {
        if (L == R)
        {
            v.emplace_back(L, data[root].sum);
            return;
        }
        int mid = (L + R) >> 1;
        if (data[root].ch[0])_dump(data[root].ch[0], v, L, mid);
        if (data[root].ch[1])_dump(data[root].ch[1], v, mid + 1, R);
    }

    void _add(int &root, const int pos, const int v, const int L, const int R)
    {
        if (!root)root = newNode();
        if (L == R)
        {
            data[root].sum += v;
            return;
        }
        int mid = (L + R) >> 1;
        if (pos <= mid)_add(data[root].ch[0], pos, v, L, mid);
        else _add(data[root].ch[1], pos, v, mid + 1, R);
        data[root].sum = data[data[root].ch[0]].sum + data[data[root].ch[1]].sum;
    }

public:
    explicit segmentTreeMgr(int N)
    {
        _N = N;
        tot = 0;
        data.resize(N * 64);
    }

    int newNode()
    {
        int ret;
        if (!stk.empty())
        {
            ret = stk.top();
            stk.pop();
        } else
        {
            ret = ++tot;
            assert(tot < data.size());
        }
        memset(&data[ret], 0, sizeof(Node));
        return ret;
    }

    void remove(int root)
    {
        if (data[root].ch[0])remove(data[root].ch[0]);
        if (data[root].ch[1])remove(data[root].ch[1]);
        stk.push(root);
    }

    void dump(int root, vector<pair<int, int>> &v)
    {
        _dump(root, v, 1, _N);
    }

    int query(int root, int l, int r)
    {
        l = max(l, 1);
        r = min(r, _N);
        return _query(root, l, r, 1, _N);
    }

    void add(int &root, int pos, int v)
    {
        _add(root, pos, v, 1, _N);
    }

    void clear()
    {
        tot = 0;
        while (!stk.empty())stk.pop();
    }
};

int n, k;
pair<int, int> a[MAXN];
vector<int> G[MAXN];
bool vis[MAXN];
int fa[MAXN], rt[MAXN];
long long ans;

void init(segmentTreeMgr &mgr)
{
    mgr.clear();
    for (int i = 1; i <= n; ++i)
    {
        vis[a[i].second] = false;
        fa[a[i].second] = a[i].second;
        rt[a[i].second] = 0;
        mgr.add(rt[a[i].second], a[i].first, 1);
    }
}

int findf(int x)
{
    return fa[x] == x ? x : fa[x] = findf(fa[x]);
}

void merge(segmentTreeMgr &mgr, int x, int y, long long val)
{
    auto fx = findf(x);
    auto fy = findf(y);
    if (mgr.query(rt[fx], 1, n) < mgr.query(rt[fy], 1, n))
    {
        swap(fx, fy);
    }
    vector<pair<int, int>> p;
    mgr.dump(rt[fy], p);
    for (auto &[i, j]: p)
    {
        ans += val * mgr.query(rt[fx], i - k, i + k) * j;
    }
    for (auto &[i, j]: p)
    {
        mgr.add(rt[fx], i, j);
    }
    mgr.remove(rt[fy]);
    fa[fy] = fx;
}

void go(segmentTreeMgr &mgr, const pair<int, int> &now, int op)
{
    vis[now.second] = true;
    for(auto&i:G[now.second])
    {
        if (vis[i])
        {
            merge(mgr, now.second, i, now.first * op);
        }
    }
}
int main()
{

    scanf("%d %d", &n, &k);
    segmentTreeMgr seg(M);
    for (int i = 1; i <= n; ++i)
    {
        scanf("%d", &a[i].first);
        a[i].second = i;
    }
    for (int i = 1; i < n; ++i)
    {
        int x, y;
        scanf("%d %d", &x, &y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    sort(a + 1, a + 1 + n);
    init(seg);
    for (int i = 1; i <= n; ++i)
    {
        go(seg, a[i], 1);
    }
    init(seg);
    for (int i = n; i; --i)
    {
        go(seg, a[i], -1);
    }
    printf("%lld\n", ans << 1);
    return 0;
}


/*
3 0
2 1 1
1 2
1 3

3 1
2 1 1
1 2
1 3

 * */

时间复杂度:,其中 表示值域

#传智杯##国赛#
全部评论
c当时打表看到偶数都可以用1,然后奇数模拟就行
点赞 回复 分享
发布于 昨天 19:28 四川
想问一下,验题组是否对《D. 小苯的排列数》这题跑了java代码,代码逻辑跟题解一样,但就是超时。
点赞 回复 分享
发布于 昨天 18:54 江西
好家伙,这B题竟然是求每一次数组减去的数的总和最大,我赛时一直以为max(ans,a[i]-a[i-1]),一直过不了,签到题也不会,我当时都怀疑人生了
点赞 回复 分享
发布于 昨天 14:22 江西
当时c题是这样写的
点赞 回复 分享
发布于 昨天 14:16 江西
这个c题我赛时的写法是找x的二进制位最低位的0,找到了直接赋值给y然后退出循环,然后没找到返回-1不知道为什么当时一直过不了啊,然后我还写了一个check函数,检查,写了check函数的代码也过不了,为啥
点赞 回复 分享
发布于 昨天 13:55 江西
A,研究生组用题:CDEFGH B组用题:BCDEFG C组用题:ABCDEF 补题链接很快就上了,大家不要着急
点赞 回复 分享
发布于 昨天 10:38 北京

相关推荐

昨天 11:22
门头沟学院 C++
点赞 评论 收藏
分享
评论
4
4
分享

创作者周榜

更多
牛客网
牛客企业服务