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

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

同时代码中的 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 a, b;
    cin >> a >> b;
    assert(a >= b);
    int k = a / b, c = a % b;
    cout << a << "=" << k << "*" << b << "+" << c << endl;
}

时间复杂度:单组

B. 小苯的括号疑问

题意:给定只含有 "" 和 "" 的括号串,其中有一些位置可能被反转了,问把括号复原成合法括号的方案是否唯一。

知识点:

思维

题解:

实际上会发现括号长什么样都无所谓,答案只和括号长度有关。那么容易发现只有长度为 的括号是有唯一解的,即:

其次奇数一定无解。

再其次大于 的偶数也一定存在形如 "" 和 "" 这样的合法解,因此一定有多解。

代码:

void solve() {
    string s;
    cin >> s;
    int n = s.size();
    if(n & 1) {
        cout << -1 << endl;
    } else if(n == 2) {
        cout << "()" << endl;
    } else {
        cout << "There are multiple solutions" << endl;
    }
}

时间复杂度:单组

C. 小苯的水果园

题意:

给定一个数组,第 秒删除所有出现了恰好 次的 们,问第 秒后数组的长度。

知识点:

前缀和

题解:

1. 期望解:

考虑用 存数字 的出现次数,再用 存所有出现了恰好 次的数字个数,对 数组做前缀和, 此时就变为了:出现次数不超过 的数字个数。此时我们就可以对所有询问 回答了。
需要注意的细节是 的询问需要和

void solve() {
    int n, q;
    cin >> n >> q;
    assert(n <= 2e5);
    assert(q <= 2e5);
    map<int, int> mp;
    for(int i = 1, x; i <= n; i++) {
        cin >> x;
        assert(x <= 1e6);
        mp[x] ++ ;
    }
    vector<int> cnt(n + 1);
    for(auto & [x, y] : mp) {
        cnt[y] += y;
    }
    for(int i = 1; i <= n; i++) {
        cnt[i] += cnt[i - 1];
    }

    while(q -- ) {
        int d;
        cin >> d;
        d = min(d, n);
        cout << n - cnt[d] << " ";
    }
    cout << endl;
}

时间复杂度: 会带有 的复杂度。

2. 暴力解(优雅)

根据鸽笼原理,出现次数不同的数字最多 种,即最坏情况是形如:,数字 恰好出现了 次这样的。
因此我们可以对于每个数字只存一个出现次数,对所有的 {出现次数,这个出现次数的数字个数} 这样的对存入数组后排序(这个数组的长度最多 ),接着枚举 的每一个 ,直接暴力枚举看这个数组到什么时候会满足出现次数第一次大于 ,把这个位置之前的 "这个出现次数的数字个数" 全部加起来就是这个 的答案。把所有 的答案处理好,再 回答每一个询问。

时间复杂度:

(当然事实上这个暴力做法也可以用双指针来优化后面的枚举,因为注意到随着 的增大,这个 数组每次肯定会走的越来越深,因此是单调的,可以在枚举 时双指针求,依然可以做到 的复杂度,带 因为要排序。)

D. 小苯的数组最值

题意:

给定数组,再给定 个区间加操作,现在区间操作会随机等概的失效一个,求在此情况下数组最大值的期望。

知识点:

(前缀和、差分/线段树),逆元,枚举

题解:

由于只有一个区间失效,我们考虑枚举那个失效的区间,因此我们先把所有的操作都操作上去(这里使用差分),再一个个地尝试撤销操作。

撤销时我们发现,如果一个区间完全包含了数组的所有最大值,那么撤销这个操作后一定会改变数组最大值,具体的:区间中的最大值 在撤销后变为了 ,而区间外的最大值不变,且实际上是一个前缀和一个后缀的最大值,即:。此时数组的最大值即为以上三者的最大值。
否则如果当前撤销的区间不包含所有的最大值,那么撤销后是不影响数组最大值的,此时无所谓。

将所有可能的最大值都加起来,再除以 次操作就是期望,即乘上 的逆元。

代码:

void solve() {
    int n, q;
    cin >> n >> q;
    assert(n <= 5e5);
    assert(q <= 5e5);
    vector<int> a(n + 1);
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        assert(a[i] >= 1);
        assert(a[i] <= 1e9);
    }
    vector<int> b(n + 2);
    vector<int> L(n + 1), R(n + 1), D(n + 1);
    for(int i = 1; i <= q; i++) {
        cin >> L[i] >> R[i] >> D[i];
        assert(L[i] <= R[i]);
        assert(L[i] >= 1 && L[i] <= n);
        assert(R[i] >= 1 && R[i] <= n);
        b[L[i]] += D[i];
        b[R[i] + 1] -= D[i];
    }

    vector<int> pre(n + 1);
    for(int i = 1; i <= n; i++) {
        b[i] += b[i - 1];
        a[i] += b[i];
        pre[i] = max(pre[i - 1], a[i]);
    }
    vector<int> suf(n + 2);
    for(int i = n; i > 0; i--) {
        suf[i] = max(suf[i + 1], a[i]);
    }

    int ans = 0;
    for(int i = 1; i <= q; i++) {
        int l = L[i], r = R[i], d = D[i];
        if(pre[l - 1] == pre[n] || suf[r + 1] == suf[1]) {
          // 说明当前区间没有包含全部的数组最大值
            // cout << "no_cover_mx : " << i << endl;
            ans += pre[n];
        } else {
            ans += max(pre[n] - d, max(pre[l - 1], suf[r + 1]));
        }
        ans %= pp;
    }
    ans *= ksm(q, pp - 2, pp);
    ans %= pp;

    cout << ans << endl;
}

时间复杂度:

E. 小苯的网络配置

题意:

给定无向图,对于每条边求其是否位于某个 的最短路上。

知识点:

图论,枚举

题解:

我们考虑 求最短路中的松弛操作,不难发现:对于每条边 ,如果此边在某条最短路上的话,则一定满足 ,即最短路从 ,再从 ,再从 。或者将 反过来的上式成立。这个条件反过来也成立,因此做法就呼之欲出了。

我们考虑先跑个从 出发的 ,预处理出 到所有点的最短路 ,再跑一个 出发的 ,求出 到所有点的最短路记作 。接着只需要枚举每条边 进行 即可。

代码:
void solve() {
    int n, m;
    cin >> n >> m;
    assert(n <= 3e5);
    assert(m <= 6e5);
    vector<vector<P>> g(n + 1);
    vector<array<int, 3>> edge(m + 1);
    for(int i = 1, u, v, w; i <= m; i++) {
        cin >> u >> v >> w;
        assert(u <= n && u >= 1);
        assert(v <= n && v >= 1);
        assert(w >= 1 && w <= 1e9);
        g[u].emplace_back(v, w);
        g[v].emplace_back(u, w);

        edge[i][0] = u;
        edge[i][1] = v;
        edge[i][2] = w;
    }

    auto dijkstra = [&](int s) -> vector<int> {
        vector<int> d(n + 1, 1e18);
        priority_queue<P, vector<P>, greater<>> q;
        q.emplace(0, s);
        while(q.size()) {
            auto [dist, u] = q.top();
            q.pop();
            if(d[u] <= dist) continue;
            d[u] = dist;
            for(auto & [v, w] : g[u]) {
                if(d[v] > dist + w) {
                    q.emplace(dist + w, v);
                }
            }
        }
        return d;
    };

    auto d1 = dijkstra(1), dn = dijkstra(n);

    auto check = [&](int u, int v, int w) -> bool {
        return d1[n] == d1[u] + dn[v] + w;
    };

    for(int i = 1; i <= m; i++) {
        int u = edge[i][0], v = edge[i][1], w = edge[i][2];
        if(check(u, v, w) || check(v, u, w)) {
            cout << 1;
        } else {
            cout << 0;
        }
    }
    cout << endl;
}

复杂度: (瓶颈在 算法。)

F.小苯的糖果游戏

题意:两个人分别从一段不交的前缀和后缀中选两个子序列,满足子序列的和相同,问有多少种不同的选法。定义两种选法相同,当且仅当两种选法的子序列选择完全相同,同时两种选法的前后缀也完全相同;否则两种选法不同。

知识点:

背包 ,枚举。

题解:

我们考虑枚举最终游戏结束时,两人相邻的终点位置,我们枚举这个位置不妨记作 ,那么问题就变成了:我现在有一个长度为 的数组和一个长度为 的数组,我要从两者中分别取一些数字,使得和相等,有多少种取法,那么就变成了经典的背包,我们可以分解成一前一后两个部分,对前缀和后缀分别做背包,把最终答案加起来。

我们考虑先做一遍前缀的背包,但不滚动数组优化,记录成 ,表示考虑 这段前缀的,取一些数字的和为 的方案数。

记录好后我们再倒着枚举一遍,这一次可以用滚动数组优化了,不妨记作 数组,表示后缀考虑到当前位置时取出来的数字和为 的方案数(当然也是可以不用滚动优化的),正常做好背包后,在每一步都枚举一遍所有的 接着把答案加上一个 即可。

代码:
void solve() {
    int n;
    cin >> n;
    int s = 0;
    vector<int> a(n + 2, 1e9);
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        s += a[i];
    }
    vector<vector<int>> dp(n + 1, vector<int>(s + 1));
    dp[0][0] = 1;
    for(int i = 1; i <= n; i++) {
        dp[i] = dp[i - 1];
        for(int j = a[i]; j <= s; j++) {
            dp[i][j] = (dp[i][j] + dp[i - 1][j - a[i]]) % pp;
        }
    }

    ll ans = 0;
    vector<int> f(s + 1);
    f[0] = 1;
    for(int i = n + 1; i > 0; i--) {
        for(int j = s; j >= a[i]; j--) {
            f[j] = (f[j] + f[j - a[i]]) % pp;
        }
        for(int j = 0; j <= s; j++) {
            ans += 1LL * f[j] * dp[i - 1][j] % pp;
        }
    }
    ans %= pp;
    cout << ans << endl;
}

时间复杂度:

G.小苯的神奇背包

题意:

给定物品(体积,价值) 数组,支持三种操作:

1.单点修物品体积
2.单点修物品价值
3.查询区间中的物品任意放入背包的最大价值
定义背包中物品的体积异或和 不为 就可以放入,总价值为价值的加法和。

知识点:

线段树,位运算(异或)

题解:

注意到物品体积均为非负数,因此加入更多的物品一定会更优,考虑最优情况,我们尝试将区间中所有物品都放入,此时如果区间物品的体积异或和不为 则一定最优,答案就是区间物品的价值和

因此贪心的,我们尽量把区间选满,但如果区间中物品的价值异或和恰好为 ,那么根据异或的性质,也容易想到我们只要扔掉一个最小的体积不为 的物品,就一定能改变整体的异或和,那么改变之后肯定不为 了。(如果读者不清楚为什么改变之后不是 ,请读者先学习异或运算性质)。

因此做法就比较明显了,我们用线段树维护:
区间物品的体积异或和

区间物品的价值加法和

以及区间中体积不为 的物品的最小价值

即可完成本题。

代码:
int n, q;
vector<int> v, w;
struct Node {
    int l, r, sum, mn, xor_sum;
};
vector<Node> tr;

void push_up(int u) {
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
    tr[u].xor_sum = tr[u << 1].xor_sum ^ tr[u << 1 | 1].xor_sum;
    tr[u].mn = min(tr[u << 1].mn, tr[u << 1 | 1].mn);
}

void build(int u, int l, int r) {
    if(l == r) {
        if(v[l] == 0) {
            tr[u] = {l, r, w[l], 1000000000000000000LL, v[l]};
        } else {
            tr[u] = {l, r, w[l], w[l], v[l]};
        }
    } else {
        tr[u] = {l, r};
        int mid = (l + r) >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        push_up(u);
    }
}

void update(int u, int k, int x, int op) {
    if(tr[u].l == tr[u].r) {
        assert(tr[u].l == k);
        if(op == 1) {
            tr[u].xor_sum = x;
            if(tr[u].xor_sum == 0) {
                tr[u].mn = 1e18;
            } else {
                tr[u].mn = tr[u].sum;
            }
        } else {
            tr[u].sum = x;
            if(tr[u].xor_sum) {
                tr[u].mn = x;
            }
        }
        return ;
    }
    int mid = (tr[u].l + tr[u].r) >> 1;
    if(k <= mid) {
        update(u << 1, k, x, op);
    } else {
        update(u << 1 | 1, k, x, op);
    }
    push_up(u);
}

Node ask(int u, int l, int r) {
    if(tr[u].l >= l && tr[u].r <= r) {
        return tr[u];
    }
    int mid = (tr[u].l + tr[u].r) >> 1;
    if(r <= mid) {
        return ask(u << 1, l, r);
    } else if(l > mid) {
        return ask(u << 1 | 1, l, r);
    }
    auto L = ask(u << 1, l, r);
    auto R = ask(u << 1 | 1, l, r);
    Node ans = {L.l, R.r, L.sum + R.sum, min(L.mn, R.mn), L.xor_sum ^ R.xor_sum};
    return ans;
}

void solve() {
    cin >> n >> q;
    tr.assign(4 * n + 1, {});
    v.assign(n + 1, 0);
    w.assign(n + 1, 0);
    for(int i = 1; i <= n; i++) {
        cin >> v[i] >> w[i];
    }
    build(1, 1, n);
    while(q -- ) {
        int op;
        cin >> op;
        if(op < 3) {
            int k, x;
            cin >> k >> x;
            update(1, k, x, op);
        } else {
            int l, r;
            cin >> l >> r;
            auto res = ask(1, l, r);
            int ans = res.sum;
            if(res.xor_sum == 0) {
                ans -= res.mn;
            }
            ans = max(0LL, ans);
            cout << ans << endl;
        }
    }
}

时间复杂度: 就是朴素不带懒标的线段树的复杂度。

H. 小苯的数字成鸡

题意:

区间中满足:除个位数以外的数字的乘积恰好等于个位数位的数字个数。

知识点:数位 ,分类讨论
题解:

首先不难发现一件事,个位数不会超过 ,也就是说前面很多数字的乘积不会超过 ,这实际上是一个比较强的限制,让我们可以直接给出 数组的状态设置: 表示目前填到第 位,前面的数字乘积为 ,最后一个填的数字为 的数字个数。(这里的 是从高位到低位填的。)

可以放心的开成 这样一来状态数不超过 个。

接着我们可能会放心地顺手写出形如: 的记忆化搜索,

(其中 维护当前填的数位的乘积, 维护填的最后一个数位, 表示是否填到顶格, 表示当前是否还在填前导 。)

其中由于 不会超过 ,因此我们可能会有一些形如 的特判。
最终发现也通过了样例,但提交却获得了 的反馈。

事实上这是因为,我们没有考虑 这一类的数字,它同样也是符合要求的,但注意到他的前缀 在考虑到前 位时会得到最坏为 的结果,这样的 被我们上述的 直接跳过了统计,因此我们的答案比正确答案要少。

因此正确的方式是,首先还是先做一次上述的 ,但我们只考虑 的情况,统计出答案后。我们再考虑 的情况,那我们会发现由于 ,因此这个数字一定是末尾为 的,我们干脆直接将 去除以 下取整,用这个结果跑一边另一个数位 ,另一个什么数位 呢?
实际上就是:数位中只要含有 就合法。
因此我们实际上要做两个不同的数位 ,并将这两者的答案加起来,由于两者考虑的 值域完全不同,因此一定是不重不漏的。

(当然,验题时也有验题人给出了只需要一次 的做法,读者可以自行思考。)

代码
int dp1[20][11][11], dp2[20][2];

void solve() {
    int L, R;
    cin >> L >> R;

    string s;
    auto dfs1 = [&](auto && self, int u, int mul, int choose, bool limit, bool lead0) -> int {
        if(u == s.size()) {
            return lead0 == 0 && choose != -1 && choose == mul;
        }
        if(limit == 0 && lead0 == 0 && dp1[u][mul][choose] != -1) return dp1[u][mul][choose];
        int ans = 0;
        int up = limit ? (s[u] - '0') : 9;
        for(int i = 0; i <= up; i++) {
            if(lead0 && i == 0) {
                ans += self(self, u + 1, -1, -1, limit & (i == up), 1);
            } else if(i > 0) {
                if(choose == -1) {
                    ans += self(self, u + 1, -1, i, limit & (i == up), lead0 & (i == 0));
                } else {
                    if(mul == -1) {
                        ans += self(self, u + 1, choose, i, limit & (i == up), lead0 & (i == 0));
                    } else if(mul * choose <= 9) {
                        ans += self(self, u + 1, mul * choose, i, limit & (i == up), lead0 & (i == 0));
                    }
                }
            }
        }
        if(limit == 0 && lead0 == 0) dp1[u][mul][choose] = ans;
        return ans;
    };

    auto dfs2 = [&](auto && self, int u, bool zero, bool limit, bool lead0) -> int {
        if(u == s.size()) {
            return lead0 == 0 && zero;
        }
        if(!lead0 && !limit && dp2[u][zero] != -1) return dp2[u][zero];
        int ans = 0;
        int up = limit ? (s[u] - '0') : 9;
        for(int i = 0; i <= up; i++) {
            if(lead0 && i == 0) {
                ans += self(self, u + 1, 0, limit & (i == up), 1);
            } else {
                ans += self(self, u + 1, zero | (i == 0), limit & (i == up), lead0 & (i == 0));
            }
        }
        if(!lead0 && !limit) dp2[u][zero] = ans;
        return ans;
    };

    auto get = [&](int x) -> int {
        s = to_string(x);
        memset(dp1, -1, sizeof dp1);
        int ans = dfs1(dfs1, 0, -1, -1, 1, 1);
        memset(dp2, -1, sizeof dp2);
        s.pop_back(); // x /= 10; 的效果
        ans += dfs2(dfs2, 0, 0, 1, 1);
        return ans;
    };

    cout << get(R) - get(L - 1) << endl;

    // auto check = [&](int x) -> bool {
    //     if(x < 11) return 0;
    //     int mul = 1, back = x % 10;
    //     // if(back == 0) return 0;
    //     x /= 10;
    //     while(x) {
    //         mul *= (x % 10);
    //         x /= 10;
    //     }
    //     // if(mul == 0) return 0;
    //     return back == mul;
    // };

    // // b-f 暴力做法对拍
    // int ans1 = 0;
    // for(int i = L; i <= R; i++) {
    //     if(check(i)) {
    //         ans1 ++ ;
    //         // cerr << i << ", ";
    //     }
    // }
    // cerr << endl;
    // cerr << ans1 << endl;
}

时间复杂度:,复杂度没有展现出第二个 的复杂度,原因是其相当于第一个 几乎可以忽略不计,对于第一个 来说, 的数位长度,三个 中的前两个分别是 的取值范围,也就是 的状态个数,第三个 是转移的边数。

(这个复杂度实际上也是上限,真实情况跑不到这么多。)

Thanks

#传智杯##题解##传智杯省赛#
全部评论
E题是不是卡spfa?
1 回复 分享
发布于 2024-12-30 15:08 湖南
看榜上北方工业大学好多高手不到一个小时ak
点赞 回复 分享
发布于 2024-12-30 12:54 广西
哥哥有补题链接吗,话说D题我用线段树为什么过不去
点赞 回复 分享
发布于 2024-12-30 16:15 天津
补题链接https://ac.nowcoder.com/acm/contest/99909 研究生组,A组用题: BDEFGH B组用题: ACDEFG C组用题: ABCDEF
点赞 回复 分享
发布于 2024-12-30 18:51 陕西

相关推荐

2024-12-29 14:34
门头沟学院 golang
投递兴业数金等公司9个岗位
点赞 评论 收藏
分享
评论
点赞
4
分享
牛客网
牛客企业服务