1. Codeforces Round 1086 (Div. 2) 题目解析与实战复盘
上周参加了Codeforces第1086轮Div.2的比赛,从A题一路打到D1题,过程中遇到了不少有趣的算法思维挑战。作为常年混迹Codeforces的老选手,我发现这套题目特别适合用来训练基础算法能力和编码速度。下面就来详细拆解A到D1四道题的解题思路,分享一些比赛中总结的实用技巧。
1.1 比赛概况与题目特点
这场Div.2比赛共包含6道题(A-F),其中D题分为D1(简单版)和D2(困难版)。根据参赛选手反馈,题目难度梯度设置合理:
- A题:典型的签到题,考察基础编程能力
- B题:需要发现规律性的数学题
- C题:贪心算法应用
- D1题:动态规划基础应用
比赛采用标准计分规则,解题时间与错误提交都会影响最终排名。我在实际参赛时,A题一次AC用了4分钟,B题两次提交(第一次边界条件没处理好)总计15分钟,C题卡了半小时才找到正确贪心策略,D1题则在比赛结束前惊险通过。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. A题解析 - 基础实现与边界处理
2.1 题目重述
A题标题为"Sum of Digits",给定一个正整数n,要求找到一个最小的正整数x,使得x的各位数字之和等于n,且x本身能被10^k整除(即末尾至少有k个0)。
输入限制:
- 1 ≤ n ≤ 100
- 0 ≤ k ≤ 8
2.2 解题思路
这道题的核心在于构造满足条件的最小数字。经过分析可以发现:
- 数字要最小,位数应该尽可能少
- 末尾有k个0意味着数字必须是10^k的倍数
- 剩余的数字和需要等于n
最优构造方法是:
- 前面放一个数字1(如果n>1)或保持为空(n=1)
- 中间补足够多的0使总和达到n
- 末尾添加k个0
例如n=5, k=3时的解是5000(数字和5+0+0+0=5,末尾3个0)
2.3 代码实现
cpp复制#include <iostream>
using namespace std;
void solve() {
int n, k;
cin >> n >> k;
if (n == 0) {
cout << 0 << endl;
return;
}
// 构造非零部分
if (n <= 9) {
cout << n;
} else {
cout << 9;
int remain = n - 9;
while (remain > 9) {
cout << 9;
remain -= 9;
}
if (remain > 0) cout << remain;
}
// 添加末尾k个0
for (int i = 0; i < k; ++i) {
cout << 0;
}
cout << endl;
}
int main() {
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
2.4 易错点分析
- 边界情况处理:当n=1时,直接输出10...0(k个0)
- 数字和分配:确保前面的数字和正好等于n,不要多也不要少
- 前导零问题:虽然题目允许前导零,但构造最小数字时应该避免
提示:这类构造题建议先在纸上画出几个测试用例,验证思路正确性再编码,可以节省大量调试时间。
3. B题解析 - 数学规律与公式推导
3.1 题目重述
B题标题为"GCD and LCM",给定三个正整数a, b, c,要求找到任意两个正整数x和y满足:
- gcd(x, y) = a
- lcm(x, y) = b
- x和y都在[1, c]范围内
如果无解则输出-1 -1。
3.2 解题思路
这道题需要运用数论中gcd和lcm的基本性质:
- 对于任意两个数,gcd(x,y) * lcm(x,y) = x * y
- 设x = a * m, y = a * n,其中gcd(m,n)=1
- 则lcm(x,y) = a * m * n = b ⇒ m * n = b/a
解题步骤:
- 首先检查b是否能被a整除,不能则无解
- 计算k = b/a,然后找到两个互质的因数对(m,n)使得m*n=k
- 检查am和an是否都≤c
3.3 代码实现
cpp复制#include <iostream>
#include <algorithm>
using namespace std;
void solve() {
int a, b, c;
cin >> a >> b >> c;
if (b % a != 0) {
cout << "-1 -1\n";
return;
}
int k = b / a;
// 寻找k的互质因数对
for (int m = 1; m * m <= k; ++m) {
if (k % m == 0) {
int n = k / m;
if (__gcd(m, n) == 1) {
int x = a * m;
int y = a * n;
if (x <= c && y <= c) {
cout << x << " " << y << "\n";
return;
}
}
}
}
cout << "-1 -1\n";
}
int main() {
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
3.4 优化技巧
- 因数枚举只需要到sqrt(k),可以节省时间
- 使用__gcd内置函数比手写更快
- 一旦找到符合条件的解立即返回,避免不必要的计算
注意:在Codeforces比赛中,输入输出使用cin/cout时,对于大数据量记得加上ios::sync_with_stdio(false)加速。
4. C题解析 - 贪心算法应用
4.1 题目重述
C题标题为"Array Splitting",给定一个长度为n的数组a和一个整数k,要求将数组分成k个连续非空子数组,使得各子数组和的最大值最小化。
4.2 解题思路
这是典型的"最小化最大值"问题,通常可以使用贪心或二分答案解决。本题采用贪心策略:
- 计算前缀和数组pre
- 计算相邻元素差值diff[i] = a[i+1] - a[i]
- 对diff数组排序,取最大的k-1个差值
- 结果就是总和减去这k-1个最大差值
原理是:最大的k-1个差值对应最优分割点,将数组分成k段后,最大段和最小。
4.3 代码实现
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void solve() {
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
if (k == 1) {
cout << a[n-1] - a[0] << "\n";
return;
}
vector<int> diffs;
for (int i = 1; i < n; ++i) {
diffs.push_back(a[i] - a[i-1]);
}
sort(diffs.begin(), diffs.end(), greater<int>());
int total = a[n-1] - a[0];
for (int i = 0; i < k-1; ++i) {
total -= diffs[i];
}
cout << total << "\n";
}
int main() {
solve();
return 0;
}
4.4 贪心正确性证明
为什么这样贪心是正确的?考虑:
- 如果不分割,总和为a[n-1]-a[0]
- 每分割一次,相当于减去一个相邻差值
- 为了使剩余的最大段和最小,应该减去最大的k-1个差值
这个思路与Kruskal算法求最小生成树有异曲同工之妙。
5. D1题解析 - 动态规划基础应用
5.1 题目重述
D1题是D题的简单版本,标题为"Minimum Path"(简单版)。给定一个n×m的网格,每个格子有一个小写字母。要求从(1,1)走到(n,m),每次只能向右或向下移动,使得路径构成的字符串字典序最小。同时,允许最多改变k个格子的字母为任意小写字母。
5.2 解题思路
这道题需要使用动态规划来解决。定义dp[i][j][l]表示走到(i,j)位置,已经修改了l次字母时,能得到的最小字典序字符串的前缀。
由于直接存储字符串会MLE,我们需要优化状态表示:
- 改为记录到达(i,j)时修改l次后的最小字符串长度
- 使用滚动数组优化空间
- 按步数分层处理
关键观察:
- 字典序比较是从前到后的,所以可以逐层处理
- 同一层的状态可以按字母顺序处理
5.3 代码实现
cpp复制#include <iostream>
#include <vector>
#include <queue>
#include <tuple>
using namespace std;
void solve() {
int n, m, k;
cin >> n >> m >> k;
vector<string> grid(n);
for (int i = 0; i < n; ++i) {
cin >> grid[i];
}
// dp[i][j][l] = minimal string length when at (i,j) with l changes
vector<vector<vector<string>>> dp(n, vector<vector<string>>(m, vector<string>(k+1, string(n+m-1, 'z'))));
dp[0][0][0] = string(1, grid[0][0]);
if (k > 0) {
dp[0][0][1] = "a";
}
for (int step = 1; step < n+m-1; ++step) {
for (int i = 0; i < n; ++i) {
int j = step - i;
if (j < 0 || j >= m) continue;
for (int l = 0; l <= k; ++l) {
string min_str = string(step+1, 'z');
// 从上方转移
if (i > 0) {
string candidate = dp[i-1][j][l] + grid[i][j];
if (candidate < min_str) {
min_str = candidate;
}
if (l > 0) {
candidate = dp[i-1][j][l-1] + 'a';
if (candidate < min_str) {
min_str = candidate;
}
}
}
// 从左方转移
if (j > 0) {
string candidate = dp[i][j-1][l] + grid[i][j];
if (candidate < min_str) {
min_str = candidate;
}
if (l > 0) {
candidate = dp[i][j-1][l-1] + 'a';
if (candidate < min_str) {
min_str = candidate;
}
}
}
if (min_str.size() == step+1) {
dp[i][j][l] = min_str;
}
}
}
}
string ans = string(n+m-1, 'z');
for (int l = 0; l <= k; ++l) {
if (dp[n-1][m-1][l] < ans) {
ans = dp[n-1][m-1][l];
}
}
cout << ans << "\n";
}
int main() {
solve();
return 0;
}
5.4 动态规划优化
实际比赛中,上述代码可能会因为字符串操作导致TLE。更优的实现方式是:
- 不直接存储字符串,而是记录前驱状态
- 按字母顺序逐层扩展
- 使用BFS+优先队列来实现
这种优化可以将时间复杂度从O(nmk)降低到更优的水平。
6. 比赛总结与提升建议
通过这次比赛,我总结了几个关键点可以帮助提升Div.2的比赛表现:
-
A题速通技巧:
- 先花1分钟仔细读题,确保理解所有条件
- 手算2-3个样例验证思路
- 编写代码时注意边界条件
- 目标是5分钟内AC
-
数学题(B题)解题策略:
- 回忆相关数学定理和公式
- 从简单特例入手寻找规律
- 注意整数除法和模运算的特殊情况
-
贪心算法验证方法:
- 尝试证明贪心选择的正确性
- 构造反例测试边界情况
- 如果无法证明,可以先写代码用小数据测试
-
动态规划优化经验:
- 状态设计要尽可能精简
- 使用滚动数组节省空间
- 对于字典序问题,按字母顺序处理
-
比赛时间管理:
- 前30分钟专注解决A、B题
- 中间1小时攻克C题
- 剩余时间尝试D题
- 最后15分钟检查所有提交的代码
对于想要提升Codeforces rating的选手,我建议:
- 每周至少参加2场正式比赛
- 赛后及时复盘所有题目,包括没做出来的
- 建立错题本,记录典型错误和优化思路
- 专项训练薄弱环节(如动态规划、图论等)
这套Round 1086的题目质量很高,特别是D1题对于练习基础DP很有帮助。建议把A-D1四道题都自己实现一遍,然后对比官方题解和其他高分选手的代码,学习更优的实现方式。
