ACGO 排位赛#12 - 题目解析

别问为什么没有挑战赛#11,因为挑战赛#11被贪心的 Yuilice 吃掉了(不是)。

本次挑战赛难度相比较前面几次有所提升。

爆料:小鱼现在已经入职了研发部门,排位赛的算分级制将在未来的几场排位赛中做出重大改变。之后就不会出现大家段位都很低的情况了。对于程度好的同学,稍微打一两把排位赛段位就会有很大的提升。

第一题 - 50%, 50%

题目链接跳转:点击跳转

STL 大法真好,用自带的 string.size() 方法就可以快速求出一个字符串的长度。具体思路见代码,根据题目要求模拟就好了。

本题的 AC 代码如下:

#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 代码如下,代码并没有使用函数,见谅:

#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 代码如下:

#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]\)。竖着排列之后就是这个样子的:

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 代码如下:

#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,在每次获取头节点的时候判断某一个烟花是否刚好在该时间点燃放,如果烟花正好燃放,我们就把这个烟花的坐标加入到队列之中。

while(arr[cnt].z <= t.z && cnt <= k){
vis[arr[cnt].x][arr[cnt].y] = 1;
que.push(arr[cnt]); cnt ++;
}

由于每一个烟花每一个单位时间只会影响附近的一个格子,说明在放入烟花的时候队列中队头和队尾的权重是相同的,这样就保证队列内的元素权重一定是单调递增的。这这种情况下,这道题就转换成了使用 BFS 来求多源无权最短路的条件。

本题的 AC 代码如下:

#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 路径】(洛谷链接:链接跳转)。

对于每一层来说,具体的实现方法和函数如下:

  1. cover(dist, grid, "#*");

    • distgrid中标记为"#""*"的所有障碍物进行覆盖处理。这一步的作用是标记出所有不可通行的区域,为后续的距离计算或路径规划提供基础。
  2. getMinDist(dist, grid, "#");

    • 计算从当前所在位置到grid中标记为"#"(障碍物)位置的最短距离。这一步的作用是为后续的路径规划获取到障碍物的距离信息,方便下一步进行路径选择。
  3. hamilton(dist, grid);

    • distgrid上执行哈密顿路径(Hamiltonian Path)的计算,目的是找到一条经过所有目标点的路径。这一步的作用是根据前面的距离信息,找到一条经过所有必经点的路径。
  4. cover(dist, grid, "#.");

    • distgrid中标记为"#"(障碍物)和"."(空地)的部分进行覆盖处理。这一步是为了确保后续的距离计算排除已标记的障碍物,并识别可通行的路径。
  5. getMinDist(dist, grid, "#");

    • 再次计算从当前所在位置到grid中标记为"#"的最短距离。这一步是为了更新经过路径覆盖处理后的最短距离,确保得到最新的距离信息。

除此之外就是一些小的细节优化了,具体请参阅代码注释。

本题的 AC 代码如下(本代码参考了黄老师的 std,并加以修改(我是不会说我数组传来传去把自己传死了,讨厌指针的一天)),若需要更详细的解答,可以 参考本文

#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;
}

【题目全解】ACGO排位赛#12的更多相关文章

  1. (lintcode全部题目解答之)九章算法之算法班题目全解(附容易犯的错误)

    --------------------------------------------------------------- 本文使用方法:所有题目,只需要把标题输入lintcode就能找到.主要是 ...

  2. 九度oj题目&amp;吉大考研11年机试题全解

    九度oj题目(吉大考研11年机试题全解) 吉大考研机试2011年题目: 题目一(jobdu1105:字符串的反码).    http://ac.jobdu.com/problem.php?pid=11 ...

  3. IOS-UITextField-全解

    IOS-UITextField-全解   //初始化textfield并设置位置及大小   UITextField *text = [[UITextField alloc]initWithFrame: ...

  4. Sql Server函数全解<五>之系统函数

    原文:Sql Server函数全解<五>之系统函数  系统信息包括当前使用的数据库名称,主机名,系统错误消息以及用户名称等内容.使用SQL SERVER中的系统函数可以在需要的时候获取这些 ...

  5. js系列教程2-对象、构造函数、对象属性全解

    全栈工程师开发手册 (作者:栾鹏) 快捷链接: js系列教程1-数组操作全解 js系列教程2-对象和属性全解 js系列教程3-字符串和正则全解 js系列教程4-函数与参数全解 js系列教程5-容器和算 ...

  6. js系列教程1-数组操作全解

    全栈工程师开发手册 (作者:栾鹏) 快捷链接: js系列教程1-数组操作全解 js系列教程2-对象和属性全解 js系列教程3-字符串和正则全解 js系列教程4-函数与参数全解 js系列教程5-容器和算 ...

  7. Echarts数据可视化series-scatter散点图,开发全解+完美注释

    全栈工程师开发手册 (作者:栾鹏) Echarts数据可视化开发代码注释全解 Echarts数据可视化开发参数配置全解 6大公共组件详解(点击进入): title详解. tooltip详解.toolb ...

  8. Echarts数据可视化series-heatmap热力图,开发全解+完美注释

    全栈工程师开发手册 (作者:栾鹏) Echarts数据可视化开发代码注释全解 Echarts数据可视化开发参数配置全解 6大公共组件详解(点击进入): title详解. tooltip详解.toolb ...

  9. Echarts数据可视化series-effectscatter特效散点图,开发全解+完美注释

    全栈工程师开发手册 (作者:栾鹏) Echarts数据可视化开发代码注释全解 Echarts数据可视化开发参数配置全解 6大公共组件详解(点击进入): title详解. tooltip详解.toolb ...

  10. jquery系列教程7-自定义jquery插件全解:对象函数、全局函数、选择器

    点击打开: jquery系列教程1-选择器全解 jquery系列教程2-style样式操作全解 jquery系列教程3-DOM操作全解 jquery系列教程4-事件操作全解 jquery系列教程5-动 ...

随机推荐

  1. GitHub 创始人资助的开源浏览器「GitHub 热点速览」

    你是否注意到,现在主流的浏览器如 Chrome.Edge.Brave 和 Opera 都采用了谷歌的 Chromium 引擎?同时,谷歌每年不惜花费数十亿美元,确保其搜索引擎在 Safari 中的默认 ...

  2. 洛谷P6397

    [COI2008] GLASNICI 题意描述 输入 3.000 2 0.000 6.000 输出 1.500 点拨 二分答案的题一般来说可以用答案去检验假设. 对于这道题,每一个信使的最佳走法是保证 ...

  3. elementplus弹窗可拖拽draggable,点击空白处不消失close-on-click-modal,modal是否去掉遮罩层

    <el-dialog :modal="false" v-model="dialogVisible" title="" width=&q ...

  4. C# Expression详解(高级)

    LINQ在本地查询IEnumerbale主要是用委托来作为传参,而解析型查询IQueryable则用Expression来作为传参: public static IEnumerable<T> ...

  5. 如何安装 Arch Linux 操作系统?

    Arch Linux Install 安装 到使用 Arch 说明前面或多或少已经接触过 Debian 系列和 Red Hat 系列相关 Linux 发行版,对于虚拟化软件 VirtualBox 如何 ...

  6. OI生涯回忆&退役之后

    一个人的命运啊,当然要靠自我奋斗,但是也要考虑到历史的进程 --<庄子·秋水> 好吧,现在是2024年7月24日,我现在正坐在某编程机构的办公室电脑旁,写下这些文字,是啊,我已经退役将近两 ...

  7. MySQL 跨服务器关联查询

    如果您需要在 MySQL 中关联查询位于不同服务器的表(跨服务器关联查询),您可以考虑使用 MySQL 的联机查询(Federated MySQL).联机查询允许您在一个服务器上访问和查询另一个服务器 ...

  8. scratch打乒乓球源码免费下载

    点击下载

  9. 免费正版 IntelliJ IDEA license 详细指南

    一.前言 IntelliJ IDEA 一直是我非常喜欢的 IDE 自从用上之后就回不了头了,但是 Ultimate 版本的费用十分昂贵,其实 JetBrains 自己就提供了6种免费申请授权的方式:本 ...

  10. Linux podman容器实验

    要求 1.登录到指定的镜像仓库拉取镜像 2.以普通用户contsvc运行 3.配置 systemd-journald 服务,要求永久保留日志数据(物理机,创建文件mkdir /var/log/jour ...