2021 浙江省赛(TeamVP)
比赛相关信息
比赛信息:
比赛名称: 2022年冬训第一场团队赛(使用2021年浙江省省赛试题)
比赛地址:vjudge, Gym
榜单回顾:Board
比赛过程回顾:
A | B | C | D | E | F | G | |
---|---|---|---|---|---|---|---|
提交次数 | 1 | 0 | 1 | 0 | 0 | 4 | 0 |
首次提交时间 | 00:09:12 | 03:11:10 | 3:44:28 | ||||
首A时间 | 00:09:12 | 03:11:10 | N | ||||
最终通过数 | 403 | 11 | 307 | 26 | 2 | 102 | 106 |
H | I | J | K | L | M | ||
---|---|---|---|---|---|---|---|
提交次数 | 0 | 0 | 7 | 0 | 2 | 4 | |
首次提交时间 | 4:06:35 | 0:27:13 | 1:38:31 | ||||
首A时间 | N | 0:55:52 | 2:56:48 | ||||
最终通过数 | 5 | 57 | 213 | 7 | 192 | 291 |
第一题开题开的是 L 题,望尘洁找到思路,便交由他单独解,Wida和寒翁贺继续开题。
没过几分钟,寒翁贺开题发现 A 题可以秒切,看过榜单后发现确实是第一道打卡题,于是全队又读了一遍题并秒切,但这个时候已经有极多人过了,时间上被拉开。
寒翁贺和望尘洁继续攻克 L,Wida从后往前继续开题,相继发现 M 和 C 可解,交流后Wida开 C 题,寒翁贺开 M 题。
望尘洁 L 题第一发爆 W,众人回归,共克 L 。寒翁贺作出 hack 数据后Wida更新思路,和望尘洁一起攻 L 。
L 题过后,观察榜单发现开题无误。Wida和望尘洁一起攻 C ,寒翁贺继续攻 M。期间Wida在两端都参与了部分讨论,但迟迟没能再过题。
这个时候的榜单前几非常可怕,大家心情已经有些慌乱。期间,寒翁贺和Wida用多种方法尝试切 M 均无收获。
接近三小时时,寒翁贺大致推出 M 题结论,Wida大致找到 C 题突破口,在三小时附近陆续过掉这两题。
之后,在分析过题目后,队伍决定双开 F 和 J 。由于 J 题题目综合度较高,由Wida和寒翁贺一起尝试,望尘洁一人攻 F , 多种思路均不得解,直到比赛结束。
部分题解与小结
A - League of Legends
小评
水题,应该是秒切的题目,速度较慢的原因其一在于开题较晚,其二在于测试了多组样例而迟迟不敢动手上交(主要还是此前没有进行过省赛的训练,导致对出题特性不够熟练)。本科组最快 2 分钟切出,高中组最快 1 分钟切出。
AC代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int x, a, b;
int main() {
for(int i = 1; i <= 5; i ++) {
cin >> x;
a += x;
}
for(int i = 1; i <= 5; i ++) {
cin >> x;
b += x;
}
if(a >= b) cout << "Blue";
else cout << "Red";
return 0;
}
C - Cube
小评
一道比较简单的打卡题,本科组最快 14 分钟切出。
这道题我们队伍卡了很久,究其原因是读题的两个人迟迟没有发现题目所求的内容是“是否能组成一个正六边形(A cube is a regular hexahedron)”,约有一个多小时的时间都在做无用功。在读对题目后,发现之前错误的思路有一定可取之处,才得以在短时间内通过这一题。
题意
给定八个点的坐标,判断它们能否组成一个正六边形。
思路
正六边形的性质:给定八个顶点,不考虑顺序两两组合,一共可以出56条边,这些边按照长度可以分成三种—— \(12 * 2\) 条棱,\(12 * 2\) 条面的对角线,\(4 * 2\) 条内部对角线(其中乘以 \(2\) 的原因在于组合顺序问题)。
故:只需要将所有边的长度计算出,然后按照长度进行桶排序,若恰好有三个不同的桶,且桶中边的数量恰好为 \(24,24,8\) ,即可唯一确定一个正六边形。
AC代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for(int i = (int)(a); i <= (int)(b); i ++)
map<int, int> v;
int a[10], b[10], c[10], T;
void count(int x, int y) {
int ans = (a[x] - a[y]) * (a[x] - a[y]) + (b[x] - b[y]) * (b[x] - b[y]) + (c[x] - c[y]) * (c[x] - c[y]);
v[ans] ++;
}
int main() {
cin >> T;
while(T -- > 0) {
v.clear();
FOR(i, 1, 8) {
cin >> a[i] >> b[i] >> c[i];
}
FOR(i, 1, 8) {
FOR(j, 1, 8) {
if(i == j) continue;
count(i, j);
}
}
int Num[100] = {};
for(auto it : v) {
Num[it.second] ++;
}
if(Num[24] == 2 && Num[8] == 1) cout << "YES\n";
else cout << "NO\n";
}
}
F - Fair Distribution
小评
这道题有一定难度,赛后个人评价应当是铜牌题之一。没能顺利过题的原因还是在于数论相关的算法学习的过少,导致没能快速的找到本题的出题思路。本科组最速 36, 高中组最速 34,总体来说并不属于难题,只要想通了思路能够很快的切掉。
题意
有 \(n\) 个机器人要匹配 \(m\) 根镭射棒。规定操作:
- 可以减去任意个机器人(但不能减为 \(0\) )。
- 可以新增任意根镭射棒
- 每次操作消耗一枚金币
要求使得:镭射棒的数量必须是机器人数量的倍数。输出最少消耗的金币数量。
思路
分类讨论发现
- 当机器人数量 \(n\) 多于镭射棒数量 \(m\) 时,将机器人数量减到等于镭射棒数量就是答案。
- 当机器人数量 \(n\) 少于镭射棒数量 \(m\) 时,没有直接的规律,需要仔细讨论。
暴力打表
不妨假设,机器人的数量唯一确定,对于不同的机器人数量 \(n_k\) ,找到匹配的、符合要求的最少镭射棒数量。举例说明,当 \(n=80,m=97\) 时,建表如下:
\(\left \lceil \frac{m}{n_k} \right \rceil\) | 机器人数量 \(n_k\) | 期望镭射棒数量 \(\left \lceil \frac{m}{n_k} \right \rceil * n_k\) | 需要花费的金币数量 \(W\) |
---|---|---|---|
2 | 49 | 98 | 31+ 1 |
3 | 48 | 144 | 32+47 |
47 | 141 | 33+44 | |
… | … | … | |
3 | 33 | 99 | 47+ 2 |
… | … | … | … |
5 | 24 | 120 | 56+23 |
23 | 115 | 57+18 | |
… | … | … | |
20 | 100 | 60+ 3 | |
… | … | … | … |
10 | 10 | 100 | 70+ 3 |
11 | 9 | 99 | 71+ 2 |
13 | 8 | 104 | 72+ 7 |
… | … | … | … |
至此,我们推演 \(W\) 的公式: \(W = (n - n_k) + (\left \lceil \frac{m}{n_k} \right \rceil * n_k - m) = n_k * (\left \lceil \frac{m}{n_k} \right \rceil - 1) + (n - m)\) 。这一步的时间复杂度为 \(\mathcal {O}(n)\) ,纯纯的超时。稍加分析,我们发现,当 \(n_k\) 变化时,有相当一部分\(\left \lceil \frac{m}{n_k} \right \rceil\) 的计算结果是重复的,只需要找到关键的几个 \(n_k\) 即可。而寻找这几个关键点就用到了分块的思想。举例说明,当 \(n=80,m=97\) 时:$\left \lceil \frac{m}{n_k} \right \rceil $ 满足:
分块
不难发现,随着 \(n_k\) 的减少, \(W\) 单调递减,所以,每一个块的左边界 \(L_i\) 即为关键点。而分块的精准时间复杂度是 \(\mathcal {O} (2 * \sqrt{n} - 1)\) ,近似于 \(\mathcal {O} (\sqrt{n})\) ,符合数据范围。
现在,当务之急是如何求出每个块的左右范围。
引入方法(参考链接:【模板】整数分块 ):
设每一个分块 \(D_i\) 的左右区间为 \(L_i,R_i\) ,我们有公式
\[R_i = \begin{cases} \frac{n}{\frac {n}{L_i}} & ,向下取整 \\ \frac{n - 1}{\frac {n - 1}{L_i}} & ,向上取整 \\ \end{cases}\]
向上取整
引入向上取整的版子:
$\left \lceil \frac{m}{n} \right \rceil $ 使用代码描述为
(m + n - 1) / n
。
答案
再次推演 \(W\) 的公式: \(W = L_i * (\frac{m + L_i - 1}{L_i} - 1) + (n - m) = L_i * \frac{m - 1}{L_i} +n - m\) 。
AC代码
点击查看代码
void Solve() {
ans = 0x3f3f3f3f3f3f3f3f;
cin >> n >> m;
if(n >= m) {
cout << n - m << endl;
return;
}
for(LL L = 1, R; L <= n; L = R + 1) {
R = (m - 1) / ((m - 1) / L);
ans = min(ans, (m - 1) / L * L + n - m);
}
cout << ans << endl;
}
J - Grammy and Jewelry
小评
打卡图论 + DP题,整体来说解题的思路非常清晰。但我们队伍最后其实没能解出来,完全背包多了一个循环导致超时,迪杰克斯拉没有注意无向图条件(最后使用 BFS 绕过了这个问题),究其原因还是在于对于中阶算法过于不熟悉。
题意
有一张无向图,规定条件:
- 除了 \(1\) 号结点外其余所有节点上都有无限数量的宝藏,价值各不相同;
- 你一趟可以搬运 \(1\) 个数量的宝藏;
- 从 \(1\) 号节点出发,搬运宝藏回到 \(1\) 号节点,视为获得该价值的宝藏;
- 途径每一条边都需要花费 \(1\) 单位时间;
分别求出 \(1\) 到 \(T\) 时间能够搬运的最大宝藏价值。
思路
由于宝藏数量无限,可以看作是完全背包模型:时间视为背包容量;从起点走到某一点,再从该点回到起点(单源最短路径),花费的时间视为物品体积;某点宝藏的价值即为物品价值。
即:迪杰克斯拉 / BFS计算单源最短路径 + 完全背包模型。但是需要注意,BFS 版子中根节点的深度需要置为 \(1\) 。
AC代码(BFS)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 10010, M = 100010;
int head[N], ver[M], Next[M], d[N], a[M], f[N];
bool v[N];
int n, m, tot, t;
void add(int x, int y) {
ver[++ tot] = y, Next[tot] = head[x], head[x] = tot;
}
void bfs() {
memset(d, 0, sizeof(d));
queue<int> q;
q.push(1); d[1] = 1; //初始深度设为 1
while(q.size() > 0) {
int x = q.front(); q.pop();
for(int i = head[x]; i; i = Next[i]) {
int y = ver[i];
if(d[y]) continue;
d[y] = d[x] + 1;
q.push(y);
}
}
}
int main() {
cin >> n >> m >> t;
for (int i = 2; i <= n; i++) cin >> a[i];
int x, y;
for (int i = 1; i <= m; i++){
cin >> x >> y;
add(x, y);
add(y, x);
}
bfs();
for (int i = 1; i <= n; i++) d[i] = (d[i] - 1) * 2; //减去根节点多加的深度
for (int i = 2; i <= n; i++) {
for (int j = d[i]; j <= t; j++) {
f[j] = max(f[j], f[j - d[i]] + a[i]);
}
}
for (int i = 1; i <= t; i ++) cout << f[i] << " ";
return 0;
}
AC代码(迪杰克斯拉)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 10010, M = 100010;
int head[N], ver[M], edge[M], Next[M], d[N], a[M], f[N];
bool v[N];
int n, m, tot, t;
priority_queue<pair<int, int> >q;
void add(int x, int y, int z) {
ver[++ tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
void dj() {
memset(d, 0x3f, sizeof(d));
memset(v, 0, sizeof(v));
q.push(make_pair(0, 1)); d[1] = 0;
while(q.size()) {
int x = q.top().second; q.pop();
if(v[x]) continue;
v[x] = 1;
for(int i = head[x]; i; i = Next[i]) {
int y = ver[i], z = edge[i];
if(d[y] > d[x] + z) {
d[y] = d[x] + z;
q.push(make_pair(-d[y], y));
}
}
}
}
int main() {
cin >> n >> m >> t;
for (int i = 2; i <= n; i++) cin >> a[i];
int x, y;
for (int i = 1; i <= m; i++){
cin >> x >> y;
add(x, y, 1);
add(y, x, 1);
}
dj();
for (int i = 1; i <= n; i++) d[i] *= 2;
for (int i = 2; i <= n; i++) {
for (int j = d[i]; j <= t; j++) {
f[j] = max(f[j], f[j - d[i]] + a[i]);
}
}
for (int i = 1; i <= t; i ++) cout << f[i] << " ";
return 0;
}
L - String Freshman
小评
打卡题之一,cjb 出的水题果然还是没让我失望,在纠正了早期的错误思路后,非常快速的切掉了。总体来说确实是打卡题难度。
题意
为了找出给定字符串 \(S\) 中给定小串 \(T\) 出现的数量,cjb 写了一份代码,但是对于一些数据会出错。请你判断给定的 \(T\) 是否会使这份代码出错。
思路
先考虑正确思路:即 KMP 算法,对于 \(T\) 的第一个字母,其在 \(S\) 中的每一次出现都需要单独比对判断(下称判断入口);比对完毕后,跳至 \(S\) 的下一个判断入口,开始下一轮比对。
而所给出的代码每一次比对后不会跳回。故只要给定的 \(T\) 的第一个字母不止出现过一次,其在与 \(S\) 比对时就会覆盖一些判断入口,就会出错——只需要判断所给定的 \(T\) 的第一个字母出现的次数即可。
AC代码
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n;
char x;
string S;
int main() {
cin >> n >> x >> S;
if((int)S.find(x) == -1) cout << "Correct";
else cout << "Wrong Answer";
return 0;
}
M - Game Theory
小评
超级神奇的一道题,刚开始由我开题,但是我看了五遍都没能理解这道题什么意思,于是就给了寒翁贺解,中途也有数次合作共解,尝试了概率论等多种解法,但均没能顺利解出。最后通过暴力打表,大致得到结论——恒为 \(0\) 。于是寒翁贺开始尝试证明,但迟迟没能成功,最后抱着试一试的想法提交了,结果过了。总之这是一道把全队都吓得不轻的题。
题意
老师和学生玩猜数游戏,老师在 \(1\) 到 \(20\) 间随机出数,学生足够聪明,在不知道老师出数的前提下想要获得最大的积分。积分的获取方式如下:
- 要给对方自己所出数值的积分数。
- 数值大的一方获胜,输家给对方 \(10\) 点积分。
求解: \(n\) 轮后老师的积分。
思路
由于学生足够聪明,他们必定会想让自己出的数比老师大 \(1\) 。但是老师随机出数,学生不知道老师出了什么,所以学生也是随机出数。至此,双方均随机出数。而又因为积分的获取方式是公平的,故老师的积分期望恒为 \(0\) 。
AC代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
cout << "0.0000\n";
return 0;
}
文 / WIDA
2022.01.11 成文
首发于WIDA个人博客,仅供学习讨论
更新日记:
2022.01.11 成文