2021 浙江省赛(TeamVP)


比赛相关信息

比赛信息:

比赛名称: 2022年冬训第一场团队赛(使用2021年浙江省省赛试题)
比赛地址:vjudgeGym
榜单回顾: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 $ 满足:

\[\left \lceil \frac{m}{n_k} \right \rceil = \begin{cases} 2 & ,n_k \in [49, 80] \\ 3 & ,n_k \in [33, 49) \\ 4 & ,n_k \in [25, 33) \\ 5 & ,n_k \in [20, 25) \\ 6 & ,n_k \in [17, 20) \\ 7 & ,n_k \in [14, 17) \\ 8 & ,n_k \in [13, 14) \\ 9 & ,n_k \in [11, 13) \\ 10 & ,n_k \in [10, 11) \\ 11 & ,n_k \in [9, 10) \\ 12 & ,n_k \in [9, 10) \\ … & ,…… \end{cases}\]

分块

不难发现,随着 \(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 成文


posted @ 2022-01-08 20:01  hh2048  阅读(216)  评论(0编辑  收藏  举报