1. Codeforces Round 1081 (Div.2) 题解与思考
作为一名算法竞赛选手,参加Codeforces比赛是提升编程能力的重要途径。最近我参加了Codeforces Round 1081 (Div.2)的比赛,虽然只AC了A题,但通过赛后补题,对B-E题的解法有了深入理解。本文将详细解析这五道题的解题思路和实现细节,希望能帮助到同样在算法竞赛道路上探索的朋友们。
1.1 A. String Rotation Game
这道题要求我们对字符串进行循环旋转,使得相邻相同字符的数量最大化。题目看似简单,但需要考虑旋转过程中得分变化的规律。
1.1.1 问题分析
给定一个字符串s,我们可以进行任意次数的循环旋转操作。每次旋转将字符串最后一个字符移动到第一个位置。得分定义为字符串中相邻相同字符的对数。例如,"aabb"的初始得分为2(aa和bb)。
关键观察点:
- 初始得分可以通过遍历字符串直接计算
- 每次旋转时,如果首尾字符相同,旋转后会破坏这对相邻字符,得分减1
- 如果新移动到首位的字符与其后字符相同,得分加1
1.1.2 算法实现
cpp复制#include<iostream>
#include<string>
using namespace std;
int main() {
int t, n;
cin >> t;
while (t--) {
cin >> n;
string s;
cin >> s;
int Size = s.size();
int brick = 1;
for (int i = 0; i < Size-1; ++i) {
if (s[i] != s[i + 1]) {
++brick;
}
}
int cur = brick;
int res = brick;
bool flag = 0;
if (s[0] == s[Size - 1]) {
flag = 1;
}
for (int i = Size - 1; i >= 1; --i) {
cur = brick;
if (s[i-1] == s[i]) {
++cur;
}
if(flag){
--cur;
}
res = max(cur, res);
}
cout << res << endl;
}
}
1.1.3 复杂度分析
时间复杂度:O(n^2),其中n是字符串长度。对于每个测试用例,我们需要遍历字符串两次。
空间复杂度:O(1),只使用了常数级别的额外空间。
注意:在实际比赛中,对于n≤1000的数据规模,这个复杂度是完全可接受的。但如果n更大,可能需要寻找线性解法。
1.2 B. Flipping Binary String
这道题要求我们通过特定操作将二进制字符串转换为全0字符串。操作规则是:选择一个位置,翻转除该位置外的所有字符。
1.2.1 问题分析
关键观察点:
- 每个字符被翻转的次数等于不被选中的次数
- 对于偶数长度的字符串,总能通过适当操作得到全0字符串
- 对于奇数长度的字符串,当且仅当1的个数为偶数时才可能有解
解题思路:
- 统计字符串中0和1的个数
- 根据字符串长度和1的个数的奇偶性决定操作方案
- 输出需要翻转的位置索引
1.2.2 算法实现
cpp复制#include<iostream>
#include<vector>
#include<string>
#define int long long
using namespace std;
signed main() {
int t, n;
cin >> t;
while (t--) {
cin >> n;
string m;
int zero = 0;
int one = 0;
vector<int>list_zero;
vector<int>list_one;
cin >> m;
int Size = m.size();
for (int i = 0; i < Size;i++) {
if (m[i] == '1') {
++one;
list_one.push_back(i+1);
}
else {
++zero;
list_zero.push_back(i + 1);
}
}
if (one == 0) {
cout << 0 << endl;
continue;
}
if (one % 2 == 0) {
cout << list_one.size() << endl;
for (auto i : list_one) {
cout << i << " \n"[i == list_one.back()];
}
continue;
}
else {
if (zero & 1) {
cout << list_zero.size() << endl;
for (auto i : list_zero) {
cout << i << " \n"[i == list_zero.back()];
}
continue;
}
else {
cout << "-1" << endl;
}
}
}
return 0;
}
1.2.3 复杂度分析
时间复杂度:O(n),需要遍历字符串统计0和1的个数。
空间复杂度:O(n),需要存储0和1的位置索引。
提示:这道题的关键在于发现翻转操作的数学性质。在实际编码时,注意处理边界情况,如全0字符串或全1字符串。
1.3 C. All-in-one Gun
这道题结合了贪心算法和前缀和技巧,要求我们在有限次交换的情况下,找到击败敌人的最优策略。
1.3.1 问题分析
题目给定:
- n个子弹,每个子弹有伤害值和换弹时间
- 允许交换一对子弹的位置
- 敌人有h点生命值
- 目标是找到最少发射次数击败敌人
解题思路:
- 预处理前缀和数组,计算前i发子弹的总伤害
- 维护左最小和右最大数组,用于快速找到最优交换对
- 计算交换后的最大可能伤害
- 根据敌人生命值决定发射策略
1.3.2 算法实现
cpp复制#include<iostream>
#include<vector>
#define int long long
using namespace std;
signed main() {
int t, n, h, k;
cin >> t;
while (t--) {
cin >> n >> h >> k;
vector<int>left_min(n, 0);
vector<int>right_max(n, 0);
vector<int>sum(n, 0);
vector<int>num(n, 0);
vector<int>Max_sum(n, 0);
for (int i = 0; i < n; ++i) {
cin >> num[i];
}
int Size = num.size();
left_min[0] = num[0];
for (int i = 1; i <= Size - 1; ++i) {
if (num[i] < left_min[i - 1]) {
left_min[i] = num[i];
}
else {
left_min[i] = left_min[i - 1];
}
}
right_max[Size - 1] = num[Size - 1];
for (int i = Size - 2; i >= 0; --i) {
if (num[i] > right_max[i + 1]) {
right_max[i] = num[i];
}
else {
right_max[i] = right_max[i + 1];
}
}
Max_sum[0] = right_max[0];
sum[0] = num[0];
for (int i = 1; i < Size; ++i) {
sum[i] = sum[i - 1] + num[i];
if (i == Size - 1) {
Max_sum[i] = sum[i];
}
else {
Max_sum[i] = max(sum[i], sum[i] - left_min[i] + right_max[i + 1]);
}
}
int Time = 0;
if (h >= sum[Size - 1]) {
Time += (h / sum[Size - 1]) * (k + Size);
h = h % sum[Size - 1];
if (h == 0) {
Time = Time - k;
cout << Time << endl;
continue;
}
}
for (int i = 0; i <= Size - 1; ++i) {
if (h <= Max_sum[i]) {
Time += i + 1;
break;
}
}
cout << Time << endl;
}
}
1.3.3 复杂度分析
时间复杂度:O(n),需要三次遍历数组预处理各种信息。
空间复杂度:O(n),需要多个辅助数组存储中间结果。
实际应用:这种预处理技巧在解决区间查询问题时非常有用。类似的思路可以应用于股票买卖、资源分配等问题。
1.4 D. Cost of Tree
这道树形DP问题要求我们计算每个节点作为根时,其子树中所有节点的权值与深度乘积之和,并允许进行一次子树重组操作。
1.4.1 问题分析
题目给定:
- 一棵n个节点的树
- 每个节点有权值a_i
- 可以执行一次操作:将任意子树移动到任意位置
- 需要为每个节点u计算最优的Σ(a_v * d(u,v)),其中d(u,v)是u到v的距离
解题思路:
- 使用深度优先搜索遍历树
- 维护两个DP数组:
- dp[u][0]表示不进行操作时的值
- dp[u][1]表示进行操作后的最优值
- 考虑将子树连接到最深分支的情况
1.4.2 算法实现
cpp复制#include <bits/stdc++.h>
using namespace std;
#define int long long
#define LINF 1e18
void solve() {
int n;
cin >> n;
vector<int>a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
vector<vector<int>> adj(n + 1, vector<int>());
for (int i = 1; i < n; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
vector<int> sum(n + 1), len(n + 1);
vector < vector<int>>dp(n + 1, vector<int>(2));
auto dfs = [&](auto&& dfs, int u, int fa)->void {
len[u] = 1;
int d = 0;
multiset<int, greater<>>s;
for (auto v : adj[u]) {
if (v == fa) {
continue;
}
dfs(dfs, v, u);
sum[u] += sum[v];
s.insert(len[v]);
len[u] = max(len[u], len[v] + 1);
dp[u][0] += dp[v][0];
d = max(d, dp[v][1] - dp[v][0]);
dp[u][1] += dp[v][0];
}
dp[u][0] += sum[u];
for (auto v : adj[u]) {
if (v == fa) {
continue;
}
s.erase(s.find(len[v]));
if (s.empty()) {
s.insert(len[v]);
continue;
}
int l = *s.begin();
dp[u][1] = max(dp[u][1], dp[u][0] + l * sum[v]);
s.insert(len[v]);
}
dp[u][1] = max(dp[u][1], dp[u][0] + d);
sum[u] += a[u];
};
dfs(dfs,1, 1);
for (int i = 1; i <= n; ++i) {
cout << dp[i][1] << " ";
}
cout << endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
}
1.4.3 复杂度分析
时间复杂度:O(n log n),因为使用了multiset来维护子树深度。
空间复杂度:O(n),与树的大小线性相关。
经验分享:树形DP问题通常需要后序遍历处理子树信息。这道题的难点在于如何高效计算最优子树重组方案,使用multiset来维护深度信息是一个巧妙的做法。
1.5 E. Swap to Rearrange
这道图论问题要求我们通过交换操作使两个数组互为排列,并找出最优的交换顺序。
1.5.1 问题分析
题目给定:
- 两个数组a和b
- 允许的操作:交换a和b中相同位置的元素
- 目标:使b成为a的一个排列
关键观察:
- 如果某个元素的总数不是偶数,则无解
- 可以将问题建模为图论问题,其中节点代表元素值
- 需要找到欧拉回路来确定交换顺序
1.5.2 算法实现
cpp复制#include <bits/stdc++.h>
using namespace std;
struct Edge{
int to;
int id;
bool dir;
};
void solve(){
int n;
cin >> n;
vector<int> a(n + 1), b(n + 1);
vector<int> num(n + 1, 0);
for(int i = 1; i <= n; i++){
cin >> a[i];
num[a[i]]++;
}
for(int i = 1; i <= n; i++){
cin >> b[i];
num[b[i]]++;
}
for(int i = 1; i <= n; i++){
if(num[i] % 2 != 0){
cout << -1 << '\n';
return;
}
}
vector<vector<Edge>> adj(n + 1);
for(int i = 1; i <= n; i++){
if(a[i] != b[i]){
adj[a[i]].push_back({b[i], i, true});
adj[b[i]].push_back({a[i], i, false});
}
}
vector<bool> vis(n + 1, false);
vector<int> head(n + 1, 0);
vector<int> ans;
for(int i = 1; i <= n; i++){
if(head[i] < adj[i].size()){
stack<int> stk;
stk.push(i);
while(!stk.empty()){
int u = stk.top();
bool found = false;
while(head[u] < adj[u].size()){
Edge e = adj[u][head[u]++];
if(!vis[e.id]){
vis[e.id] = true;
stk.push(e.to);
if(!e.dir)
ans.push_back(e.id);
found = true;
break;
}
}
if(!found)
stk.pop();
}
}
}
cout << ans.size() << '\n';
for(int i = 0; i < ans.size(); i++)
cout << ans[i] << ' ';
cout << '\n';
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t;
cin >> t;
while(t--)
solve();
return 0;
}
1.5.3 复杂度分析
时间复杂度:O(n),每个边只会被访问一次。
空间复杂度:O(n),需要存储图的邻接表。
解题技巧:将数组转换问题建模为图论问题是算法竞赛中的常见技巧。这道题的难点在于发现欧拉回路与交换操作之间的关系。在实际编码时,注意处理边的方向标记。
1.6 比赛总结与反思
这次比赛我只AC了A题,暴露了几个问题:
- 对B题的数学性质分析不够深入,没能及时想到奇偶性的关键
- C题的贪心策略想到了,但实现时细节处理不够熟练
- D、E题涉及的高级算法(树形DP、图论)掌握不牢固
改进计划:
- 加强数学思维训练,特别是数论和组合数学
- 系统学习树形DP和图论算法
- 多做Atcoder和Codeforces的虚拟比赛,提高实战能力
对于刚接触Div.2比赛的选手,我的建议是:
- 先确保能稳定解决A、B题
- 针对C题多练习贪心和基本DP
- 逐步学习更高级的算法和数据结构
- 每次比赛后认真补题,理解优秀选手的解法
这次比赛虽然成绩不理想,但通过赛后补题学到了很多。算法竞赛是一场马拉松,保持学习和反思的态度才能持续进步。