1. AtCoder Beginner Contest 439 题目解析
最近参加了AtCoder Beginner Contest 439的比赛,这里分享一下A~F题的解题思路和代码实现。作为算法竞赛的入门级比赛,ABC系列非常适合编程新手和算法初学者来练习基础编程能力和算法思维。
2. 题目详解
2.1 A题 - 简单计数
题目描述:
给定一个由小写字母组成的字符串S,统计其中特定字符出现的次数。
解题思路:
这是一道基础的字符串处理题目。我们需要遍历字符串中的每个字符,检查它是否是我们需要统计的目标字符。在本题中,目标字符是'i'和'j'。
代码实现:
cpp复制#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
cin >> s;
int count = 0;
for (char c : s) {
if (c == 'i' || c == 'j') {
count++;
}
}
cout << count << endl;
return 0;
}
注意事项:
- 字符串输入时要注意是否包含空格
- 字符比较是大小写敏感的
- 循环遍历字符串时可以使用range-based for循环简化代码
2.2 B题 - 音乐播放器模拟
题目描述:
模拟一个音乐播放器的基本功能,包括音量调节和播放状态切换,并根据特定条件输出结果。
解题思路:
需要维护两个状态变量:
- 当前音量(初始为0)
- 播放状态(初始为停止)
根据不同的操作指令更新状态,并在每次操作后检查条件:是否正在播放且音量≥3。
代码实现:
cpp复制#include <iostream>
using namespace std;
int main() {
int Q;
cin >> Q;
int volume = 0;
bool is_playing = false;
while (Q--) {
int op;
cin >> op;
if (op == 1) {
volume++;
} else if (op == 2) {
volume = max(0, volume - 1);
} else if (op == 3) {
is_playing = !is_playing;
}
if (is_playing && volume >= 3) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
}
return 0;
}
常见问题:
- 音量减少时要注意不能为负
- 播放状态切换使用逻辑非操作最简洁
- 输出结果要及时,不能累积到最后
2.3 C题 - 同行评审组合
题目描述:
给定N个研究员和M个冲突关系,计算每个研究员可以选择的审稿人组合数。
解题思路:
- 统计每个研究员的冲突人数
- 可用审稿人数 = 总人数 - 1(自己) - 冲突人数
- 组合数计算公式:C(可用人数, 3)
代码实现:
cpp复制#include <iostream>
using namespace std;
const int MAXN = 2e5 + 5;
int conflict[MAXN];
int main() {
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
conflict[a]++;
conflict[b]++;
}
for (int i = 1; i <= n; i++) {
long long available = n - 1 - conflict[i];
long long combinations = available * (available - 1) * (available - 2) / 6;
cout << combinations << " ";
}
return 0;
}
注意事项:
- 组合数计算可能溢出,要用long long
- 冲突关系是双向的,要同时更新两个研究员
- 自己不能作为自己的审稿人
2.4 D题 - 交换与区间求和
题目描述:
实现一个数据结构,支持交换相邻元素和查询区间和。
解题思路:
可以使用前缀和数组或树状数组:
- 前缀和数组:交换时只需更新受影响的位置
- 树状数组:标准的点更新区间查询操作
代码实现(前缀和版):
cpp复制#include <iostream>
using namespace std;
const int MAXN = 2e5 + 5;
int arr[MAXN];
long long prefix[MAXN];
int main() {
int n, q;
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> arr[i];
prefix[i] = prefix[i-1] + arr[i];
}
while (q--) {
int op;
cin >> op;
if (op == 1) {
int x;
cin >> x;
swap(arr[x], arr[x+1]);
prefix[x] = prefix[x-1] + arr[x];
} else {
int l, r;
cin >> l >> r;
cout << prefix[r] - prefix[l-1] << endl;
}
}
return 0;
}
性能分析:
- 前缀和方法:每次交换O(1),查询O(1)
- 树状数组方法:每次交换和查询都是O(logN)
- 根据题目约束选择合适的方法
2.5 E题 - 激光消灭怪物
题目描述:
计算在极角旋转过程中会被激光消灭的怪物数量。
解题思路:
- 对所有点进行极角排序
- 预处理每个点的相同方向区间[L,R]
- 根据起点和终点的位置关系计算覆盖范围
代码实现:
cpp复制#include <iostream>
#include <algorithm>
using namespace std;
struct Point {
long long x, y;
int id;
};
long long cross(const Point &a, const Point &b) {
return a.x * b.y - a.y * b.x;
}
bool cmp(const Point &a, const Point &b) {
bool up_a = (a.y > 0) || (a.y == 0 && a.x > 0);
bool up_b = (b.y > 0) || (b.y == 0 && b.x > 0);
if (up_a != up_b) return up_a;
return cross(a, b) < 0;
}
const int MAXN = 2e5 + 5;
Point points[MAXN];
int pos[MAXN], L[MAXN], R[MAXN];
int main() {
int n, q;
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> points[i].x >> points[i].y;
points[i].id = i;
}
sort(points + 1, points + n + 1, cmp);
for (int i = 1; i <= n; i++) {
pos[points[i].id] = i;
}
L[1] = 1;
for (int i = 2; i <= n; i++) {
if (cross(points[i-1], points[i]) == 0) {
L[i] = L[i-1];
} else {
L[i] = i;
}
}
R[n] = n;
for (int i = n-1; i >= 1; i--) {
if (cross(points[i+1], points[i]) == 0) {
R[i] = R[i+1];
} else {
R[i] = i;
}
}
while (q--) {
int a, b;
cin >> a >> b;
a = L[pos[a]];
b = R[pos[b]];
if (a <= b) {
cout << b - a + 1 << endl;
} else {
cout << n - a + 1 + b << endl;
}
}
return 0;
}
关键点:
- 极角排序要正确处理边界情况
- 预处理L和R数组可以快速查询同方向点
- 环形区间要分两种情况处理
2.6 F题 - 网格重涂
题目描述:
将网格重新涂色满足特定条件,求最小操作次数。
解题思路:
动态规划:
- dp[i][j]表示处理到第i行,且第i行第一个黑格在j列时的最小操作数
- 状态转移要考虑上一行的限制条件
- 使用后缀最小值优化
代码实现:
cpp复制#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 5005;
int grid[MAXN][MAXN];
int row_sum[MAXN][MAXN];
int dp[MAXN][MAXN];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
char c;
cin >> c;
grid[i][j] = (c == '#');
row_sum[i][j] = row_sum[i][j-1] + grid[i][j];
}
}
for (int i = 1; i <= n; i++) {
int min_prev = dp[i-1][n+1];
for (int j = n+1; j >= 1; j--) {
min_prev = min(min_prev, dp[i-1][j]);
int white = row_sum[i][j-1];
int black = (n - j + 1) - (row_sum[i][n] - row_sum[i][j-1]);
dp[i][j] = min_prev + white + black;
}
}
int ans = *min_element(dp[n] + 1, dp[n] + n + 2);
cout << ans << endl;
return 0;
}
优化技巧:
- 倒序枚举j可以高效维护后缀最小值
- 使用前缀和快速计算每行的修改代价
- 初始状态和边界条件要正确处理
3. 比赛总结
这次ABC比赛的题目难度梯度设置合理,涵盖了从基础编程到较难算法的各个层次。对于初学者来说,A-C题是很好的练习题目,而D-F题则需要更深入的算法知识。
在实际编程中,要注意以下几点:
- 仔细阅读题目,理解清楚题意和要求
- 对于大数据量的题目,要选择高效的算法
- 注意边界条件和特殊情况的处理
- 合理使用STL和算法模板可以节省编码时间
通过参加这类比赛,可以快速提升编程能力和算法思维。建议初学者多练习往届比赛题目,逐步提高解题能力。
