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),我对着样例和自己的直觉调出了下面这个状态转移方程(虽然不好说有没有正确性):
#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 表的东西咋构造:
-
对于 \(st_{k,i,0}\),直接按照题意构造即可
-
对于 \(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\),要求求出:
- 找出区间 \([2,n]\) 上 \(\operatorname{H}(x)\) 的最小值及其对应极值点(存在多个极值点时输出最小一个)
- 找出区间 \([2,n]\) 上 \(\operatorname{H}(x)\) 的最大值及其对应极值点(存在多个极值点时输出最大一个)
\(n=1\) 时候直接输出 -1。
\(1\leq n \leq 10^9\)
根据欧拉函数定义式,我们有(限定条件,\(p\) 必须为质数)
那么有
根据这个函数表达式,我们可以比较显然的得出答案:
- 最大值就是找到 \(p\),\(p\) 是区间 \([2,n]\) 上面最大的质数
- 最小值就是找到 \(N=2*3*5*7*\cdots\),且 \(N \leq n\)
那个最小值倒还好处理,但这个最大值找质数,\(10^9\) 的规模,嘶。。。。。。
不过,有个数学规律:当 \(N\) 足够大的时候,不超过 \(N\) 的质数大约有 \(\frac{N}{\ln N}\) 个,所以:
- 打表:看看交个质数表上去,大概 5kw 个质数,然后自己看着压
- 暴力硬盲:平均每 \(\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 则无解)。证明有点长,但是核心在于这个函数的两个性质:
- \(\operatorname{f}(l,r)=\operatorname{f}(l,mid)+\operatorname{f}(mid+1,r)\)
- \(\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,使得
排序完的数列具有单调性,我们二分一遍即可,单次复杂度 \(O(\log n)\)。
那么,我们每遍历到一个 i,就给答案加上
现在,我们考虑下 \(\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}^{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;
}