ACGO 排位赛#12 - 题目解析
别问为什么没有挑战赛#11,因为挑战赛#11被贪心的 Yuilice 吃掉了(不是)。
本次挑战赛难度相比较前面几次有所提升。
爆料:小鱼现在已经入职了研发部门,排位赛的算分级制将在未来的几场排位赛中做出重大改变。之后就不会出现大家段位都很低的情况了。对于程度好的同学,稍微打一两把排位赛段位就会有很大的提升。
第一题 - 50%𝐴𝐼, 50%𝐻𝑢𝑚𝑎𝑛
题目链接跳转:点击跳转
STL 大法真好,用自带的 string.size()
方法就可以快速求出一个字符串的长度。具体思路见代码,根据题目要求模拟就好了。
本题的 AC 代码如下:
cpp
#include <iostream>
#include <cmath>
using namespace std;
/*
PS: 信心倍增题目。
写完这道题会让你觉得这场比赛特别简单。但其实不是。
*/
int n, cnt;
string str;
int main(){
cin >> n;
for (int i=1; i<=n; i++){
cin >> str;
if (str.size() > 5) cnt++;
}
int ans = ceil(1.0 * cnt / n * 100);
printf("%d%%AI, %d%%Human\n", ans, 100 - ans);
return 0;
}
第二题 - 统计区间内奇数与偶数的数量
题目链接跳转:点击跳转
对于这种区间的问题,可以先考虑一部分。如果要求出从 \([1, L]\) 区间内满足条件的数字我们应该怎么办?假设 \(L\) 是一个偶数,那么 \([1, L]\) 区间的奇数和偶数就应该是 \(L / 2\)。相同地,假设 \(L\) 是一个奇数,那么 \([1, L]\) 区间的奇数和偶数就分别是 \(L / 2 + 1\) 和 \(L / 2\)。注意到 \(L / 2 + 1\) 和 \(L / 2\) 两个公式都可以被简写成 \((L + 1) / 2\)。
设 \(odd(L)\) 和 \(even(L)\) 分别为区间 \([1, L]\) 的奇数个数和偶数个数。那么可以得出结论,如果要求 \([L, R]\) 区间的奇数个数,答案就应该是 \(odd(R) - odd(L-1)\) 和 \(even(R) - even(L-1)\)。
本题的 AC 代码如下,代码并没有使用函数,见谅:
cpp
#include <iostream>
using namespace std;
/*
思维水题。
稍微想一下找找规律就好了。
*/
int q, l, r, k;
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> q;
while(q--){
cin >> k >> l >> r;
if (k == 1) cout << ((r + 1) >> 1) - ((l) >> 1) << endl;
else cout << (r >> 1) - ((l - 1) >> 1) << endl;
}
return 0;
}
第三题 - 火星背包 II
题目链接跳转:点击跳转
一道反向 \(0/1\) 背包的模板题目。通常,\(0/1\) 背包问题的目标是选择若干物品,使得这些物品的总重量不超过背包容量,并且这些物品的总价值最大化。而反向 \(0/1\) 背包问题则是反其道而行之,其目标是选择若干物品,使得这些物品的总重量不低于给定的最小值,并且这些物品的总价值最小化。
主要就是状态的定义比较难:设 \(dp[j]\) 表示恰好凑出总重量为 \(j\) 的最小代价。那么我们可以得到如下的状态转移方程:
\[dp_j = \min(dp_j, dp_{j-w_i} + v_i); \]
根据状态的定义,我们将 \(dp\) 数组一开始全部初始化为正无穷大,同时另 \(dp_0 = 0\),表示恰好凑出重量为 \(0\) 的物品的最小代价为 \(0\),正无穷大代表暂时还没有答案,需要在后续的循环中更新。
之后就是跑一遍正常的 Knapsack 01 代码就好了。在输出答案的时候,我们需要找到一个满足条件 \((dp[i] \le m)\) 的最大值。用一个 for
循环就可以搞定了。
本题的 AC 代码如下:
cpp
#include <iostream>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
/*
反向 01 背包题目。
问题不大,X03 的同学们在集训营做过类似的题目(maybe 仅限杭州八月一期)。
*/
int n, m, sum;
int dp[100005], ans;
int w[1005], v[1005];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i=1; i<=n; i++){
cin >> w[i] >> v[i];
sum += v[i];
}
memset(dp, 0x7f, sizeof dp);
dp[0] = 0;
for (int i=1; i<=n; i++){
for (int j=sum; j>=v[i]; j--){
dp[j] = min(dp[j], dp[j - v[i]] + w[i]);
}
}
for (int i=1; i<=sum; i++){
if (dp[i] <= m) ans = i;
}
cout << ans << endl;
return 0;
}
第四题 - 可分数列
题目链接跳转:点击跳转
题目很简单,但需要仔细想一会儿才行。这道题的解决思路就是 找规律 !没错,就是找一下规律就好了。我们只需要关注如何将这 \(4N + 2\) 个数列在去掉两个元素后可以被平均分成 \(N\) 个等差数列。
通过手动模拟的方式,注意到我们只需要把这些数字竖着排列就可以解决问题。另每一列有四个元素,但其中有两列有五个元素(代表 \(4N + 2\) 的 \(+2\) 部分),共有 \(N\) 列。例如数列 \([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27]\)。竖着排列之后就是这个样子的:
text
1 7 13 19 25
3 9 15 21 27
5 11 17 23
可以看到这些元素构成了三个等差数列,且两个等差数列的长度为 \(5\)。那么对于长度为 \(4\) 的等差数列我们完全可以不用管,直接输出就好了。而对于这两个长度为 \(5\) 的等差数列,我们考虑从这两个等差数列中各删除一个数字来构成新的等差数列。综合来看,我们发现删除元素 \(3\) 和 \(25\) 后,新的两个等差数列依旧满足题目限定的条件。
再多列举几个例子,发现规律也是相通的。那么因此我们可以得出结论,对于任意一个长度为 \(4N + 2\) 的等差数列,只需要删除这个等差数列的第 \(2\) 项和第 \(4N + 1\) 项,剩下的 \(4N\) 个元素一定可以组成 \(N\) 个长度为 \(4\) 的等差数列。
关于本题的更多信息,可以阅读 アイドル 老师的题解作为补充:链接跳转。
找到规律后代码就很好写了,本题的 AC 代码如下:
cpp
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
int n, x, d;
signed main(){
cin >> n >> x >> d;
if (n == 1){
cout << -1 << endl;
return 0;
}
cout << 2 << " " << 4 * n + 1 << endl;
for (int i=0; i<n; i++){
int t = x + i * d;
if (i == 1) t += n * d;
for (int j=0; j<4; j++)
cout << t + j * n * d << " ";
cout << endl;
}
return 0;
}
第五题 - 花火大会
题目链接跳转:点击跳转
这道题的输出不是故意为难大家,因为输出实在太大了,超出了 CF 限定了 64MB 的限制,因此只能将输出地图改为了输出地图的哈希值(对于没有学过哈希的同学来说,输出答案也是一个比较困难的事情)。
本道题的难度其实不大,就是一个多源不同权的无权最短路问题。但由于数据量比较大,使用普通的优先队列维护会超时(别问我是怎么知道的,我一开始就用了优先队列,然后在 #13 测试点就 TLE 了),因此我们需要考虑如何找到一个 workaround 来替换优先队列。
注意到我们只需要另外再使用一个 while
,在每次获取头节点的时候判断某一个烟花是否刚好在该时间点燃放,如果烟花正好燃放,我们就把这个烟花的坐标加入到队列之中。
cpp
while(arr[cnt].z <= t.z && cnt <= k){
vis[arr[cnt].x][arr[cnt].y] = 1;
que.push(arr[cnt]); cnt ++;
}
由于每一个烟花每一个单位时间只会影响附近的一个格子,说明在放入烟花的时候队列中队头和队尾的权重是相同的,这样就保证队列内的元素权重一定是单调递增的。这这种情况下,这道题就转换成了使用 BFS 来求多源无权最短路的条件。
本题的 AC 代码如下:
cpp
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int MOD = 998244353;
int n, m, k, cnt = 2;
struct node{
int x, y, z;
} arr[1005];
queue<node> que;
int dis[5005][5005];
int vis[5005][5005];
int dx[] = {0, 1, -1, 0};
int dy[] = {1, 0, 0, -1};
bool cmp(node a, node b){
return a.z < b.z;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
for (int i=1, x, y, z; i<=k; i++)
cin >> arr[i].x >> arr[i].y >> arr[i].z;
sort(arr+1, arr+1+k, cmp);
que.push(arr[1]);
vis[arr[1].x][arr[1].y] = 1;
while(!que.empty()){
node t = que.front();
que.pop();
if (dis[t.x][t.y]) continue;
dis[t.x][t.y] = t.z;
while(arr[cnt].z <= t.z && cnt <= k){
vis[arr[cnt].x][arr[cnt].y] = 1;
que.push(arr[cnt]); cnt ++;
}
for (int i=0; i<4; i++){
int cx = dx[i] + t.x;
int cy = dy[i] + t.y;
if (cx < 1 || cy < 1 || cx > n || cy > m) continue;
if (dis[cx][cy]) continue;
if (vis[cx][cy]) continue;
vis[cx][cy] = 1;
que.push((node){cx, cy, t.z+1});
}
}
long long ans = 0, p = 1;
for (int i=1; i<=m; i++)
p = (p * 233) % MOD;
for (int i=1; i<=n; i++){
for (int j=1; j<=m; j++){
p = (p * 233) % MOD;
ans = (ans + (dis[i][j] * p) % MOD) % MOD;
}
}
cout << ans << endl;
return 0;
}
第六题 - 剑之试炼
题目链接跳转:点击跳转
这道题超级恶心,我发自内心地对那些在比赛过程中 AC 此题目的同学表示尊敬。
思路上非常好想,由于所有的怪兽都要打,且打每一个怪兽的时间都是固定的,因此我们可以在一开始就先预处理出打完所有怪兽的时间,需要优化的就只有赶路的时间了。
先考虑每一层的情况,对于任意一层来说,关键点就是找到一个最优的路径(从某一个起点出发,经过所有的点后再传送到下一层的最短路径)即可。学习过状态压缩动态规划的同学可以很容易想到模板题【最短 Hamilton 路径】(洛谷链接:链接跳转)。
对于每一层来说,具体的实现方法和函数如下:
-
cover(dist, grid, "#*");
- 将
dist
和grid
中标记为"#"
或"*"
的所有障碍物进行覆盖处理。这一步的作用是标记出所有不可通行的区域,为后续的距离计算或路径规划提供基础。
- 将
-
getMinDist(dist, grid, "#");
- 计算从当前所在位置到
grid
中标记为"#"
(障碍物)位置的最短距离。这一步的作用是为后续的路径规划获取到障碍物的距离信息,方便下一步进行路径选择。
- 计算从当前所在位置到
-
hamilton(dist, grid);
- 在
dist
和grid
上执行哈密顿路径(Hamiltonian Path)的计算,目的是找到一条经过所有目标点的路径。这一步的作用是根据前面的距离信息,找到一条经过所有必经点的路径。
- 在
-
cover(dist, grid, "#.");
- 对
dist
和grid
中标记为"#"
(障碍物)和"."
(空地)的部分进行覆盖处理。这一步是为了确保后续的距离计算排除已标记的障碍物,并识别可通行的路径。
- 对
-
getMinDist(dist, grid, "#");
- 再次计算从当前所在位置到
grid
中标记为"#"
的最短距离。这一步是为了更新经过路径覆盖处理后的最短距离,确保得到最新的距离信息。
- 再次计算从当前所在位置到
除此之外就是一些小的细节优化了,具体请参阅代码注释。
本题的 AC 代码如下(本代码参考了黄老师的 std,并加以修改(我是不会说我数组传来传去把自己传死了,讨厌指针的一天)),若需要更详细的解答,可以 参考本文:
cpp
#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
#include <vector>
using namespace std;
int n, m, k;
string map[30][105];
int dist[105][105];
struct node{
int x, y, w;
bool friend operator < (node a, node b){
return a.w > b.w;
}
};
const int dx[] = {1, 0, -1, 0};
const int dy[] = {0, 1, 0, -1};
/*
PS:本来想用纯数组加指针的方式做的。
后来写着写着把自己写死掉了,还是 vector 方便。
普通的二维数组传来传去简直要我的命。
::: 虽然我的编译器见不得 c++11 的标准,每次都给我提 warning。头大。
*/
// 转换,将每一个怪兽的血量都转换成对应的数字。
int calc(char c){
if (c == '.') return 0;
if (c == 'R') return 1;
if (c == 'B') return 3;
if (c == 'D') return 8;
if (c == 'S') return 24;
if (c == 'G') return 36;
return 0x7f7f7f7f;
}
// 最普通的广度优先搜索算法:
// 记录从 sx, sy 出发,到当前层 grid 的每一个点的最短路径。
// 其中 block 代表无法走的格子。
vector<vector<int> > bfs(vector<string> &grid, int sx, int sy, string block){
vector<vector<int> > dist(n, vector<int>(m, 0x7f7f7f7f));
dist[sx][sy] = 0;
struct node{
int x, y;
}; queue<node> que;
que.push((node){sx, sy});
while(!que.empty()){
node t = que.front();
que.pop();
for (int i=0; i<4; i++){
int cx = t.x + dx[i];
int cy = t.y + dy[i];
if (cx < 0 || cy < 0 || cx >= n || cy >= m) continue;
// 字符串函数,判断当前格子是否是障碍物,如果是障碍物的话就忽略。
if (block.find(grid[cx][cy]) != string::npos) continue;
if (dist[cx][cy] < 0x7f7f7f7f) continue;
dist[cx][cy] = dist[t.x][t.y] + 1;
que.push((node){cx, cy});
}
}
return dist;
}
// 将 dist 数组根据 grid 复原。
// 如果当前位置无法被走到,则将 dist 更新为无穷大。
void cover(vector<vector<int> > &dist, vector<string> &grid, string block){
for (int i=0; i<n; i++){
for (int j=0; j<m; j++){
if (block.find(grid[i][j]) != std::string::npos)
dist[i][j] = 0x7f7f7f7f;
}
}
return ;
}
// dijkstra 最短路算法。
// 主要作用是计算并更新每个节点到其余节点的最短距离,并通过广度优先搜索(BFS)算法来实现。
// dist[i][j] 是从一开始设定的起点到达 (i, j) 的累积代价,考虑了路径上经过的每个格子的代价。
void getMinDist(vector<vector<int> > &dist, vector<string> &grid, string block){
priority_queue<node> que;
// 一开始把所有起点都入队。
for (int i=0; i<n; i++){
for (int j=0; j<m; j++){
que.push((node){i, j, dist[i][j]});
}
}
while(!que.empty()){
node t = que.top();
que.pop();
if (dist[t.x][t.y] < t.w) continue;
for (int i=0; i<4; i++){
int cx = t.x + dx[i];
int cy = t.y + dy[i];
if (cx < 0 || cy < 0 || cx >= n || cy >= m) continue;
if (block.find(grid[cx][cy]) != string::npos) continue;
// 如果已经存在更优解了,就忽略。
if (dist[cx][cy] <= t.w + 1) continue;
dist[cx][cy] = t.w + 1;
que.push((node){cx, cy, t.w + 1});
}
}
}
// 计算汉密尔顿路径
// 即从起点出发走完所有的点最后再回来的路径最短路。
void hamilton(vector<vector<int> > &dist, vector<string> &grid){
struct node{
int x, y;
}; vector<node> vec;
for (int i=0; i<n; i++){
for (int j=0; j<m; j++){
if (grid[i][j] == '*')
vec.push_back((node){i, j});
}
}
int k = vec.size();
vector<vector<int> > f(k);
// f 数组用于计算每一个关键节点(怪兽)之间的最短路。
for (int i=0; i<k; i++){
int sx = vec[i].x;
int sy = vec[i].y;
auto toOther = bfs(grid, sx, sy, "#");
for (int j=0; j<k; j++){
f[i].push_back(toOther[vec[j].x][vec[j].y]);
}
}
// 对于 Hamilton 路径不熟悉的,直接去搜洛谷模板题先看一眼。
// X04-01 的同学应该是学过的(毕竟是我挑的题目)。
vector<vector<int> > dp(1 << k, vector<int>(k, 0x7f7f7f7f));
for (int i=0; i<k; i++){
int sx = vec[i].x;
int sy = vec[i].y;
dp[1 << i][i] = dist[sx][sy];
}
for (int i=0; i<(1<<k); i++){
for (int j=0; j<k; j++){
if (~i >> j & 1) continue;
for (int l=0; l<k; l++){
if (~(i ^ 1 << j) >> l & 1) continue;
dp[i][j] = min(dp[i][j], dp[i ^ 1 << j][l] + f[l][j]);
}
}
}
for (int i=0; i<k; i++){
int sx = vec[i].x;
int sy = vec[i].y;
dist[sx][sy] = dp[(1 << k) - 1][i];
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
vector<vector<string> > grids(k, vector<string>(n));
for (auto &grid : grids){
for (auto &row : grid){
cin >> row;
}
}
int cost = k - 1;
for (auto &grid : grids){
for (auto &row : grid){
for (auto &c : row){
if (c != '#' && c != '.'){
cost += calc(c);
c = '*';
}
}
}
}
vector<vector<int> > dist = bfs(grids[0], 0, 0, "#*");
/*
先排除掉某些障碍物和关键点,以计算初步的最短路径。
再使用 hamilton 函数来处理关键点之间的路径规划,得到所有关键点的最优路径。
最后进一步排除空白区域和障碍物,重新计算网格中各点之间的最短路径,得到最终结果。
*/
for (auto &grid : grids){
cover(dist, grid, "#*");
getMinDist(dist, grid, "#");
hamilton(dist, grid);
cover(dist, grid, "#.");
getMinDist(dist, grid, "#");
}
// 计算答案。
int ans = 0x7f7f7f7f;
for (int i=0; i<n; i++){
for (int j=0; j<m; j++){
ans = min(ans, dist[i][j]);
}
}
cout << ans + cost << endl;
return 0;
}