【题解】上海市计算机学会竞赛平台(YACS) 2023年6月乙组题解

前言

考前了,回归两三天

A. 两数归零

a. 暴力策略。即枚举 ( i , j ) (i,j) (i,j),判断 a i + a j = 0 a_{i} + a{j} = 0 ai+aj=0。复杂度 O ( N 2 ) O(N^2) O(N2),可得 30 ∼ 60 30 \sim 60 3060 分;

b. 满分策略。注意到两数之和为 0 0 0,则这两个数必定为相反数。所以开两个数组,一个统计每一个正数的数量,一个统计每一个负数的数量,绝对值相等的两个数的数量之积,再加和即可。值得注意的是,还应该注意 0 0 0,因为 0 + 0 = 0 0+0=0 0+0=0,所以还需要统计 0 0 0 的数量 x x x,最后结果加上 C x 2 C_x^2 Cx2 即可。复杂度 O ( N ) O(N) O(N),但我的代码里用到了一次排序,所以 O ( N log ⁡ N ) O(N \log N) O(NlogN),可得 100 100 100 分。

代码稍微有点臃肿了,凑活着看看吧。

#include <iostream>
#include <algorithm>
#include <unordered_map>
const int N = 3e5 + 9;
int n, a[N], b[N];
int ca, cb, cnt_zero;
std::unordered_map<int, int> mp;
int main() {
    scanf("%d", &n);
    for (int i = 1, x; i <= n; i++) {
        scanf("%d", &x);
        if (x == 0) cnt_zero++;
        if (mp[x] == 0) {
            if (x < 0) a[++ca] = -x;
            else if (x > 0) b[++cb] = x;
        }
        mp[x]++;
    }
    std::sort(a + 1, a + ca + 1);
    std::sort(b + 1, b + cb + 1);
    int ans = 0;
    for (int i = 1; i <= ca; i++) {
        int pos = std::lower_bound(b + 1, b + cb + 1, a[i]) - b;
        if (b[pos] != a[i]) continue;
        ans += mp[-a[i]] * mp[b[pos]];
    }
    ans += cnt_zero * (cnt_zero - 1) / 2;
    printf("%d\n", ans);
    return 0;
}
B. 牛奶供应(四)

这道题只需要提笔算一算就能做。

其实要求的就是:
∑ i = 1 n ( ( m − d i ) × b i [存储费] + p i × b i [材料费] ) \sum _{i=1} ^{n} ((m-d_{i}) \times b_i \text{[存储费]} + p_i \times b_i \text{[材料费]}) i=1n((mdi)×bi[存储费]+pi×bi[材料费])
即:
∑ i = 1 n ( ( m − d i + p i ) × b i ) \sum _{i=1} ^{n} ((m-d_{i}+p_{i}) \times b_i) i=1n((mdi+pi)×bi)
其中, b i b_i bi 为第 i i i 天购买的材料升数。

注意到式子中 m − d i + p i m-d_{i}+p{i} mdi+pi 是定值,所以只需要按照该定值对 i i i 天的花费排序,最小值那天买最多的原料,以此类推即可。

#include <iostream>
#include <algorithm>
using LL = long long;
const int N = 1e5 + 9;
int n, m, l;
int w[N];
struct Node {
    int num, k;
} day[N];
int main() {
    scanf("%d%d%d", &n, &m, &l);
    for (int i = 1, d, p; i <= n; i++) {
        scanf("%d%d%d", &d, w + i, &p);
        day[i] = {i, m - d + p};
    }
    std::sort(day + 1, day + n + 1, [](const Node x, const Node y) {
        if (x.k == y.k) return x.num > y.num;
        return x.k < y.k;
    });
    int i = 1;
    LL ans = 0;
    while (l) {
        if (w[day[i].num] >= l) {
            ans += l * day[i].k;
            l = 0;
        } else {
            ans += w[day[i].num] * day[i].k;
            l -= w[day[i].num];
        }
        i++;
    }
    printf("%lld\n", ans);
    return 0;
}
C. 工作安排

贪心。策略是看每两天前后怎么安排才能更省钱。

#include <iostream>
#include <algorithm>
const int N = 2e5 + 9;
struct Node {
    int t, f;
} task[N];
int main() {
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &task[i].t, &task[i].f);
    std::sort(task + 1, task + n + 1, [](const Node x, const Node y) {
        return x.f*(x.t+y.t)+y.f*y.t > y.f*(x.t+y.t)+x.f*x.t;
    });
    long long ans = 0;
    int cur = 0;
    for (int i = 1; i <= n; i++) {
        cur += task[i].t;
        ans += task[i].f * cur;
    }
    printf("%lld", ans);
    return 0;
}
D. 单词解密

这题我个人认为我的解法比较的妙,当然其实也是很基本的解法。

一个形如 abc 的字符串,对于字符 b 显然要考虑这几种情况:

a. b 单独一个,构成 b ‾ \overline{b} b

b. b 跟着前面的字符 a,构成 a b ‾ \overline{ab} ab

c. b 跟着后面的字符 c,构成 b c ‾ \overline{bc} bc

但并不是对于所有的字符都有这样的情况。当 a 所表示的数字太大时,情况 b b b 不成立;当 b 太大时,情况 c c c 又不成立。即只要 a b ‾ \overline{ab} ab b c ‾ \overline{bc} bc 都大于 26 26 26 时,只能取情况 a a a

这就是这道题讨论的重点,即什么时候能够选取相邻两个数字构成字母。

为了更好的分析,我们引入一个样例:124666899321

将所有相邻两数字能够构成字母的划出来:

没有横线的部分显然都只有 1 1 1 种划分方法。所以我们只重点讨论有横线的部分。

对于 12 12 12,可以拆分开来看,即 1 1 1 2 2 2,对应字母 ab

后面还接了一个 4 4 4 4 4 4 显然可以跟前面的 2 2 2 构成 24 24 24(x),也可以单独作为 4 4 4(d)。如果从递推的角度看的话,其实本质是在 12 12 12 后面接了一个数字,因为 12 12 12 对应了两种情况,而在 12 12 12 后面加一个数字, 124 124 124 所对应的情况数即为 12 12 12 的情况数加上一个特有的 24 24 24

我们将其抽象化,圈代表单独的,横线代表相邻两个组成的数。

发现了什么?是不是有点似曾相识的感觉?

如果将末尾为圈的看作 α \alpha α 情形,末尾为横线的看作 β \beta β 情形,则 α \alpha α 情形的下一步必定可以有两种,而 β \beta β 情形必定有一种。

发现了什么?是不是有点似曾相识的感觉?

如果将 α \alpha α 情形看作两个月后的兔子,而 β \beta β 情形看作一个月大的兔子,这不就是兔子数列——斐波那契数列吗?

所以,一个长度为 n n n 的,满足 a , b , c a,b,c a,b,c 三种情况的字符串,其可能的情况数就是 F ( n ) F(n) F(n),其中
F ( 0 ) = 1 , F ( 1 ) = 1 , F ( i ) = F ( i − 1 ) + F ( i − 2 ) ( i ≥ 2 , i ∈ N ∗ ) F(0)=1,F(1)=1,F(i)=F(i-1)+F(i-2)(i \geq 2, i \in \N^{*}) F(0)=1,F(1)=1,F(i)=F(i1)+F(i2)(i2,iN)
所以,这题的思路就是:将字符串划分为尽量少的若干部分,使得每个部分都满足任意相邻两个字符都能构成一个小于等于 26 26 26 的数,然后计算这些部分长度 l k l_{k} lk,结果就是 Π k = 1 m F ( l k ) \Pi _{k=1} ^{m} F(l_{k}) Πk=1mF(lk)。其中, m m m 代表划分段数。

吗?

上面的分析中,我们忽略了一个非常重要的数字—— 0 0 0。如果这个字符串中含有 1020,那这个 0 0 0 是需要单独讨论的。因为 0 0 0 不能作为单独的一个(显然不存在第 0 0 0 个字母吧),也不能跟后面的数字合并(显然你不会说 a 是第 01 01 01 个字母吧)。 0 0 0 只有可能和前面的数字合并,成为 10 或者 20。所以,如果碰到 0 0 0,我们还应该单独考虑,将它与前面的字符看作一个整体,而不能够简单地将其放在一个如前面所述的整体中计算。

#include <iostream>
using LL = long long;
const int N = 1e5 + 9;
const LL MOD = 1e9 + 7;
LL fib[N] = {1, 1, 2};
std::string s;
int main() {
    for (int i = 3; i < N; i++)
        fib[i] = (fib[i-1] + fib[i-2]) % MOD;
    std::cin >> s;
    LL ans = 1;
    int cnt = 1;
    for (int i = 1; i < s.size(); i++) {
        int cur = (s[i-1] - '0') * 10 + s[i] - '0';
        if (s[i] == '0') {
            ans = (ans * fib[cnt-1]) % MOD;
            cnt = 1, i++;
        } else if (cur > 26) {
            ans = (ans * fib[cnt]) % MOD, cnt = 1;
        } else cnt++;
    }
    ans *= fib[cnt];
    std::cout << ans % MOD << '\n';
    return 0;
}

代码甚至还比前几题短点。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值