2022牛客冬令营 第一场 题解

A题 九小时九个人九扇门 (数学规律+DP)

我们记一个数的数字根 \(\operatorname{f}(x)\)\(x>0\))的定义如下:

int f(int x) {
 if (x < 10) return x;
 int v = 0;
 while (x) {
     v += x % 10;
     x /= 10;
 }
 return f(v);
}

现在有 \(n\) 个人,第 \(i\) 个人的数字为 \(a_i\)。问对于每个单数 \(x\)(1 到 9),有多少种组合方式,可以使得数字和的数字根为 \(x\) 。(对 998244353 取模)

\(1\leq n \leq 10^5,1\leq a_i\leq 10^9\)

打表发现一个很滑稽,但是又确实正确的规律(数学上的证明方法是同余,类似证明一个数怎么才能被 9 整除一样):一个正数的数字根为 \(x\),说明它对 9 取模的所得值为 \(x\)(如果 \(x=9\),那么说明对 9 取模的值为 0)

那我们对每个人都取模一下,然后分组,就转变成了怎么取数,使得取模值为 \(x\) 的问题了。

求某种计数的组合方案,这是一个有点显然的 DP(出题人说是背包,而且确实是 01 背包的一种变形,但我第一眼想到的是一个普通线性 DP),我对着样例和自己的直觉调出了下面这个状态转移方程(虽然不好说有没有正确性):

\[dp_{i,j}=dp_{i-1,j}+dp_{i-1,(j-a[i]+9) \% 9} \]

#include<bits/stdc++.h>
using namespace std;
const int mod = 998244353;
const int N = 100010;

int n, a[N], dp[N][10];
int main()
{
    //read
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //solve
    for (int i = 1; i <= n; ++i) a[i] %= 9;
    dp[0][0] = 1;
    for (int i = 1; i <= n; ++i)
        for (int j = 0; j < 9; ++j)
            dp[i][j] = (dp[i - 1][j] + dp[i - 1][(j - a[i] + 9) % 9]) % mod;
    //output
    for (int i = 1; i < 9; ++i)
        printf("%d ", dp[n][i]);
    printf("%d", dp[n][0] - 1);
    return 0;
}

B题 炸鸡块君与FIFA22 (倍增)

这题我一开始还想着前缀和维护来着,但是似乎不是很能维护(逃)。

发现一个性质,执行同一段区间上面的操作所带来的分数变化,只和初始分数对 3 取模的值有关,所以我们好像可以分三类来做。

不妨记 \(st_{k,i,j}\) 为开始值(取模)为 k,执行区间 \([i, i + 2^j - 1]\) 上面操作之后发生的值的变化。没错,就是倍增。随后每次询问,直接倍增不断跳就行了,单次复杂度 \(O(\log n)\)

现在问题在于这个类似 ST 表的东西咋构造:

  1. 对于 \(st_{k,i,0}\),直接按照题意构造即可

  2. 对于 \(st_{k,i,j}(j > 0)\),有

    \[st_{k,i,j}=st_{k,i,j-1}+st_{(k + st_{k,i,j-1})\% 3,i + 2^{j-1},j-1} \]

    根据题意可知,全程分数不可能变成负数,所以取模的时候就不用担心负数取模之类的问题了。

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;

int n, q;
char s[N];
int st[3][N][20];
int query(int l, int r, int s) {
    int len = r - l + 1, t = s % 3, p = l;
    for (int k = 19; k >= 0; k--)
        if ((len >> k) & 1)
            s += st[t][p][k], t = s % 3, p += 1 << k;
    return s;
}
int main()
{
    scanf("%d%d", &n, &q);
    scanf("%s", s + 1);
    //build
    for (int i = 1; i <= n; ++i)
        for (int k = 0; k < 3; ++k)
            if (s[i] == 'W')
                st[k][i][0] = 1;
            else if (s[i] == 'L')
                st[k][i][0] = k ? -1 : 0;
            else if (s[i] == 'D')
                st[k][i][0] = 0;
    for (int j = 1; j < 20; ++j)
        for (int i = 1; i + (1 << j) - 1 <= n; ++i)
            for (int k = 0; k < 3; ++k)
                st[k][i][j] = st[k][i][j - 1] + st[(k + st[k][i][j - 1]) % 3][i + (1 << (j - 1))][j - 1];
    //query
    while (q--) {
        int l, r, s;
        scanf("%d%d%d", &l, &r, &s);
        printf("%d\n", query(l, r, s));
    }
    return 0;
}

C题 Baby's first attempt on CPU(模拟)

CPU存在一个叫做先写后读相关问题:如果第 i 行语句向寄存器写入了某个值,第 j 行向该寄存器读取,那么只有 \(j - i > 3\) 的时候才能读到当时写入的那个值,否则读入的还是旧值,也就是说具有一定延迟性。

现在我们知道有 \(n\) 行语句和他们彼此的依赖情况,问我们需要插入多少空语句(纯粹占用一个时钟空间,别的啥都不干),可以使得它们彼此的依赖消失,能够像理想情况一样运行?

\(1\leq n \leq 100\)

纯睿智模拟题,鬼知道为啥我当时没写(逆天)。

#include<bits/stdc++.h>
using namespace std;
struct Node {
    int a[4];
    bool isEmpty() { return a[1] == 0 && a[2] == 0 && a[3] == 0; }
    void change1() { a[3] = a[2], a[2] = a[1], a[1] = 0; }
    void change2() { a[3] = a[2], a[2] = 0; }
    void change3() { a[3] = 0; }
    void read() { cin >> a[1] >> a[2] >> a[3]; }
};
int main()
{
    vector<Node> vec;
    int n;
    cin >> n;
    for (int i = 0; i < n; ++i) {
        Node t;
        t.read();
        vec.push_back(t);
    }
    int ans = 0;
    for (int i = 0; i < n; ++i)
        while (!vec[i].isEmpty()) {
            ans++;
            vec[i].change1();
            if (i + 1 < n) vec[i + 1].change2();
            if (i + 2 < n) vec[i + 2].change3();
        }
    cout << ans;
    return 0;
}

D题 牛牛做数论 (数论)

共有 \(T\) 组数据。(\(1\leq T\leq 100\)

\(\phi(x)\) 为欧拉函数,现在我们定义 \(\operatorname{H}(x)=\dfrac{\phi(x)}{x}\),那么对于给定的 \(n\),要求求出:

  1. 找出区间 \([2,n]\)\(\operatorname{H}(x)\) 的最小值及其对应极值点(存在多个极值点时输出最小一个)
  2. 找出区间 \([2,n]\)\(\operatorname{H}(x)\) 的最大值及其对应极值点(存在多个极值点时输出最大一个)

\(n=1\) 时候直接输出 -1。

\(1\leq n \leq 10^9\)

根据欧拉函数定义式,我们有(限定条件,\(p\) 必须为质数)

\[\phi(N)=N*\prod_{p|N}(1-\frac{1}{p}) \]

那么有

\[\operatorname{H}(N)=\frac{\phi(N)}{N}=\prod_{p|N}(1-\frac{1}{p}) \]

根据这个函数表达式,我们可以比较显然的得出答案:

  1. 最大值就是找到 \(p\)\(p\) 是区间 \([2,n]\) 上面最大的质数
  2. 最小值就是找到 \(N=2*3*5*7*\cdots\),且 \(N \leq n\)

那个最小值倒还好处理,但这个最大值找质数,\(10^9\) 的规模,嘶。。。。。。

不过,有个数学规律:当 \(N\) 足够大的时候,不超过 \(N\) 的质数大约有 \(\frac{N}{\ln N}\) 个,所以:

  1. 打表:看看交个质数表上去,大概 5kw 个质数,然后自己看着压
  2. 暴力硬盲:平均每 \(\ln N\) 个数里面就有一个质数,那么我们可以大约 \(O(\sqrt{N}\ln N)\) 的复杂度(大多数不见得能跑满)求出这个最大值,配合 \(T\) 组数据,勉勉强强可以过(补充:出题人就是采取了这一性质,\(10^9\) 以内任意两个相邻质数的距离都在 282 以内)

我采用了第二个方式,时间 43ms,还可以接受。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const LL pr[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
bool isPrime(LL x) {
    for (LL i = 2; i * i <= x; ++i)
        if (x % i == 0) return false;
    return true;
}
void solve() {
    LL n;
    cin >> n;
    if (n == 1) {
        puts("-1");
        return;
    }
    LL Min, Max;
    //min
    Min = 1;
    for (int i = 0; ; ++i) {
        Min *= pr[i];
        if (Min > n) {
            Min /= pr[i];
            break;
        }
    }
    //max
    for (LL x = n; x >= 2; x--)
        if (isPrime(x)) {
            Max = x;
            break;
        }
    printf("%lld %lld\n", Min, Max);
}
int main()
{
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}

E题 炸鸡块君的高中回忆 (模拟)

\(T\) 组数据(\(1\leq T \leq 10^5\))。

现在有 \(n\) 个人在校门外,需要刷学生卡来进入,但是学生卡仅有 \(m\) 张。现在有一个策略:每次进去 \(m\) 个人,然后派一个人出来(带着所有的学生卡),循环往复,直到所有人进去为止。

进去需要一个时间单位,出来也需要一个,问至少需要多长时间才能全部进入?

\(1\leq m \leq n \leq 10^9\)

在仅有一张学生卡的情况下,人数超过 1 时无解。

\(m>1\) 时,显然答案为 \(\lceil\dfrac{n-m}{m-1}\rceil*2+1\)。(抛开最后一次不谈,每轮实际上仅减少 \(m-1\) 人)

#include<bits/stdc++.h>
using namespace std;
int solve() {
    int n, m;
    scanf("%d%d", &n, &m);
    if (m == 1) return n == 1 ? 1 : -1;
    return ((n - 2) / (m - 1)) * 2 + 1;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) printf("%d\n", solve());
    return 0;
}

F题 中位数切分(数学证明)

\(T\) 组数据。(\(1\leq T \leq 20\)

给定一个长度为 \(n\) 的数列 \(\{a_n\}\),问能否将其划分为若干段,使得每一段的中位数都大于 \(m\)?(序列长度为偶数的时候取中间偏小那一个作为中位数)如果能,则输出可能的最大段数,否则输出 -1。

\(1\leq n \leq 10^5,1\leq a_i,m\leq 10^9\)

我一开始陷在什么什么对顶堆啥的里面,无法自拔了就离谱。

我们进行一个转化,将数列里面每一个数进行变换:大于 m 的就记为 1,反之变为 0,然后问题就变成了:能否将数列分成若干段,使得每一段里面 1 的数量超过 0?

本题的官方解法是一个数学定理:记 \(\operatorname{f}(l,r)\) 为区间内 1 减去 0 的数量,那么 \(f(1,n)\) 即为本体答案(小于等于 0 则无解)。证明有点长,但是核心在于这个函数的两个性质:

  1. \(\operatorname{f}(l,r)=\operatorname{f}(l,mid)+\operatorname{f}(mid+1,r)\)
  2. \(\operatorname{f}(l,r)>0\) 表示区间 \([l,r]\) 能作为合格区间

那么区间上限就是 \(\operatorname{f}(l,r)\),且确实存在一个这样的划分方法。

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, m, a[N];
//
int solve()
{
    //read
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //solve
    int res = 0;
    for (int i = 1; i <= n; ++i) 
        res += a[i] >= m ? 1 : -1;
    return res > 0 ? res : -1;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) printf("%d\n", solve());
    return 0;
}

据说这题还可以 DP 后树状数组优化,就不管它了。

G题 ACM is all you need

不是很懂,直接搬运官方题解。

  • 反过来考虑 ,对于位置 i ,可以发现能够使得变换后 \(f_i<\min(f_{i-1},f_{i+1})\)的 b 值一定是连续的一段(但可能会到正无穷,这种情况可以规定一个比较大的数代替正无穷),如 [6, 10, 6] 中 10 的位置所对应 b 值的取值区间是 [9, +inf] ;
  • 于是,我们对于每个 i,可以求出区间 \([l_i,r_i]\) 表示若 b 在这个区间里取值就可以使得位置 i 满足条件;
  • 我们发现,这样问题其实就转变成了给出 n - 2 个区间,求被区间覆盖最多的点被覆盖的次数(令 b 取该点的值,则覆盖该点的区间所对应的位置都可以取到最小值),这是一个比较经典的问题,可以通过对区间端点排序后遍历解决。

H题 牛牛看云

给定长度为 \(n\) 的数列 \(\{a_n\}\),求 \(\sum\limits_{i=1}^n\sum\limits_{j=i}^n|a_i+a_j-1000|\)

\(3 \leq n \leq 10^6, 0 \leq a_i \leq 1000\)

方法一:数学推导,排序,二分

我们先思考一手怎么求 \(\sum\limits_{i=1}^n\sum\limits_{j=1}^n|a_i+a_j-1000|\)(问就是看错题了)。

我们将数列 \(\{a_n\}\) 从小到大进行排序(显然不会对答案造成影响),随后做一次前缀和维护一下。

这时候,我们 for 一遍 i,对于每个 i,我们要找到相对应的 p,使得

\[\begin{cases} a_i+a_j-1000<0,1\leq j < p\\ a_i+a_j-1000>0,p\leq j\leq n \end{cases} \]

排序完的数列具有单调性,我们二分一遍即可,单次复杂度 \(O(\log n)\)

那么,我们每遍历到一个 i,就给答案加上

\[\sum\limits_{j=1}^{p-1}(1000-a_i-a_j)+\sum\limits_{j=p}^{n}(a_i+a_j-1000)\\ =(n-2p+2)(a_i-1000)+\sum\limits_{j=p}^{n}a_j-\sum\limits_{j=1}^{p-1}a_j \]

现在,我们考虑下 \(\sum\limits_{i=1}^n\sum\limits_{j=i}^n|a_i+a_j-1000|\)\(\sum\limits_{i=1}^n\sum\limits_{j=1}^n|a_i+a_j-1000|\) 啥关系。

\[\sum\limits_{i=1}^n\sum\limits_{j=1}^n|a_i+a_j-1000| \\ =\sum\limits_{i=1}^n\sum\limits_{j=1}^{i-1}|a_i+a_j-1000|+\sum\limits_{i=1}^n\sum\limits_{j=i+1}^n|a_i+a_j-1000|+\sum\limits_{i=1}^n|a_i+a_i-1000| \\ =2\sum\limits_{i=1}^n\sum\limits_{j=i+1}^n|a_i+a_j-1000|+\sum\limits_{i=1}^n|2a_i-1000| \\=2\sum\limits_{i=1}^n\sum\limits_{j=i}^n|a_i+a_j-1000|-\sum\limits_{i=1}^n|2a_i-1000| \]

\(\sum\limits_{i=1}^n\sum\limits_{j=1}^{i-1}|a_i+a_j-1000|=\sum\limits_{i=1}^n\sum\limits_{j=i+1}^{n}|a_i+a_j-1000|\) 由轮换对称性得来,不明显,但是确实成立。

综上,我们即可求出答案,全局复杂度 \(O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
//这题卡long long,我看数据规模,一开始以为可以直接int来着
#define LL long long
int n, a[N];
LL s[N];
#define sum(l, r) (s[r] - s[l - 1])
int find(int x) {
    if (a[n] < x) return n + 1;
    return lower_bound(a + 1, a + n + 1, x) - a;
}
int main()
{
    //read
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //init
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; ++i)
        s[i] = s[i - 1] + a[i];
    //solve
    LL ans = 0;
    for (int i = 1; i <= n; ++i) {
        int p = find(1000 - a[i]);
        ans += (n - 2 * p + 2) * (a[i] - 1000) + sum(p, n) - sum(1, p - 1);
    }
    for (int i = 1; i <= n; ++i)
        ans += abs(2 * a[i] - 1000);
    printf("%lld", ans / 2);
    return 0;
}

方法二:数学推导,桶

突然发现,我们上面那个方法似乎没用到 \(0\leq a_i\leq 1000\) 的限制,那么说明肯定还有个更简单的写法。

\(n\leq 10^5,0\leq a_i\leq 1000\),说明肯定有一堆重复值,我们开个大小为 1000 的桶来统计一下,这样就可以不用二分,直接统计出那个值,然后再数学推导一下(可能有不用数学推导的方法,我怀疑)即可。(可惜出题人卡掉了倒序 \(O(1000n)\) 的写法,很烦

#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
#define LL long long
int n, a[N], cnt[1010];
int main()
{
    //read
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //init
    for (int i = 1; i <= n; ++i)
        cnt[a[i]]++;
    long long ans = 0;
    for (int i = 0; i <= 1000; ++i) {
        ans += 1LL * cnt[i] * cnt[i] * abs(2 * i - 1000);
        for (int j = i + 1; j <= 1000; ++j)
            ans += 2LL * cnt[i] * cnt[j] * abs(i + j - 1000);
    }
    for (int i = 1; i <= n; ++i)
        ans += abs(2 * a[i] - 1000);
    printf("%lld", ans / 2);
    return 0;
}

I题 B站与各唱各的 (数学概率,逆元)

\(T\) 组数据。(\(1\leq T\leq 10^4\)

现在有 \(n\) 个人,他们打算唱一首有 \(m\) 句歌词的歌。

每个人都会先去独立唱一遍,每句歌词独立且等可能的选择唱或者补不唱。唱完后,将所有歌汇集到一起,如果某个歌词被所有人都唱了(或者所有人都没唱),那么这个歌词就唱失败了。

求能够唱出来的歌词的数学期望。

\(1\leq n,m\leq 10^9\)

显然,某歌词唱失败的概率为 \(\dfrac{2}{2^n}\),所以最后的数学期望显然为 \(m*(1-\dfrac{2}{2^n})\)

开个 long long,做一遍逆元即可(我很难相信这题这么简单,但是开题人数这么少)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const LL mod = 1e9 + 7;
LL power(LL a, LL b) {
    LL res = 1;
    while (b) {
        if (b & 1) res = res * a % mod;
        b >>= 1;
        a = a * a % mod;
    }
    return res;
}
LL inv(LL x) {
    return power(x, mod - 2);
}
int main()
{
    int T;
    cin >> T;
    while (T--) {
        LL n, m;
        cin >> n >> m;
        cout << m * (power(2, n) - 2 + mod) % mod * inv(power(2, n)) % mod <<endl;
    }
    return 0;
}

J题 小朋友做游戏 (贪心)

\(T\) 组数据。(\(1\leq T\leq 10^3\)

现在有 \(A\) 个安静的小朋友和 \(B\) 个吵闹的小朋友,老师想要选 \(n\) 个小朋友坐成一圈玩游戏,但是不能让两个吵闹的小朋友坐在一起(否则会吵起来)。

每个小朋友都有一个快乐度 \(v_i\),老师想要让选中的小朋友的快乐度尽可能大,请求出最大值(如果根本不能选出这么多小朋友则输出 -1)。

\(2\leq A,B\leq 10^4,3\leq n \leq A + B,1\leq v_i \leq 10^4\)

保证 \(\sum A+B\leq 2*10^5\)

根据数学规律,吵闹小朋友的数量不能超过 \(\lfloor\frac{n}{2}\rfloor\) 个。

那我们把小朋友按照快乐度排个序,优先选快乐度大的,然后注意全程吵闹小朋友不能超过限定数量即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 20010;
int A, B, n;
struct Node {
    int type, happy;
    bool operator < (const Node &rhs) const {
        return happy < rhs.happy;
    }
};
priority_queue<Node> q;

int solve()
{
    //read
    scanf("%d%d%d", &A, &B, &n);
    while (!q.empty()) q.pop();
    for (int i = 1; i <= A; ++i) {
        int x;
        scanf("%d", &x);
        q.push((Node){1, x});
    }
    for (int i = 1; i <= B; ++i) {
        int x;
        scanf("%d", &x);
        q.push((Node){2, x});
    }
    //solve
    int cnt1 = 0, cnt2 = 0, ans = 0;
    while (!q.empty() && cnt1 + cnt2 < n) {
        int type = q.top().type, happy = q.top().happy;
        q.pop();
        if (type == 1) cnt1++, ans += happy;
        else if (cnt2 + 1 <= n / 2) cnt2++, ans += happy;
    }
    return cnt1 + cnt2 < n ? -1 : ans;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) printf("%d\n", solve());
    return 0;
}

K题 冒险公社(DP)

简单来说就是 \(dp_{i,j,k,l}\) 表示当前在 i 岛, i,i-1,i-2分别为 l,k,j 时候绿岛最大值,进行状态转移,详情可见代码。

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
char str[N];
int dp[N][27];
// 0G 1R 2B
int calcGreen(int x) { return (x % 3 == 0) + (x / 3 % 3 == 0) + (x / 9 % 3 == 0); }
int calcRed  (int x) { return (x % 3 == 1) + (x / 3 % 3 == 1) + (x / 9 % 3 == 1); }
int cnt(int x) { return calcGreen(x) - calcRed(x); }
bool suitable(int i, int j) {
    return (str[i] == 'R' && cnt(j) < 0) || (str[i] == 'G' && cnt(j) > 0)
            || (str[i] == 'B' && cnt(j) == 0);
}
bool can(int j, int k) { return k % 3 == (j / 3 % 3) && (k / 3 % 3 == j / 9 % 3); }
int main()
{
    int n;
    cin >> n >> (str + 1);
    // f初始化为负无穷
    memset(dp, 0xc0, sizeof(dp));

    for (int j = 0; j < 27; j++)
        if (suitable(3, j)) dp[3][j] = calcGreen(j);
    for (int i = 4; i <= n; i++)
        for (int j = 0; j < 27; j++)
            if (suitable(i, j))
                for (int k = 0; k < 27; k++)
                    if (can(j, k))
                        dp[i][j] = max(dp[i][j], dp[i - 1][k] + (j % 3 == 0));

    int ans = -1;
    for (int j = 0; j < 27; j++)
        ans = max(ans, dp[n][j]);

    cout << (ans < 0 ? -1 : ans) << endl;
    return 0;
}

L题 牛牛学走路 (模拟)

\(T\) 组数据(\(1\leq T \leq 100\))。

一个点在坐标 (0, 0) 处,现在它将遵循一串长为 \(n\) 的移动指令进行移动(UDLR), 求出全流程中距离原点最远的距离。

\(1\leq n \leq 1000\)

一边动一边记录即可,复杂度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n;
char s[N];
void solve() {
    scanf("%d", &n);
    scanf("%s", s + 1);
    double x = 0, y = 0;
    double ans = 0;
    for (int i = 1; i <= n; ++i) {
        char c = s[i];
        if (c == 'U') y += 1;
        else if (c == 'D') y -= 1;
        else if (c == 'L') x -= 1;
        else if (c == 'R') x += 1;

        ans = max(ans, sqrt(x * x + y * y));
    }
    printf("%.10lf\n", ans);
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) solve();
    return 0;
}
posted @ 2022-01-28 22:23  cyhforlight  阅读(37)  评论(0编辑  收藏  举报