算法竞赛补题解析:牌面得分与数字反转问题
2026/9/23 22:29:10 网站建设 项目流程

1. 训练赛补题背景与赛况回顾

作为大二寒假期间参加牛客寒假训练赛的选手,我的水平在参赛选手中属于中上等。每场比赛后,我都会把那些有能力解决但比赛时没能完成的题目进行补题。这次是第一场训练赛的补题记录,我选择了B题和G题两道题目进行深入分析。

比赛过程中,我完成了6道题目,最终排名在1500名左右,这个成绩我已经比较满意了。不过通过赛后补题,我发现这两道题目其实都在我的能力范围内,没能当场解出来主要是因为长时间没有练习导致思维不够敏捷,以及对题意理解出现偏差。

2. B题解析:牌面得分组合问题

2.1 题目理解与赛时误区

B题的题目描述大致是这样的:有两个数组a和b,每个数组包含n个数字。我们需要将a数组的数字与b数组的数字进行某种配对,配对规则是:如果a[i] > b[j],则得1分;如果a[i] <= b[j],则不得分。关键在于,一张b数组的牌如果没有被任何a数组的牌击败,它会一直保留在牌堆中。

我的第一个误区是误以为每张牌只能使用一次。实际上题目并没有这个限制,一张b数组的牌如果没有被击败,可以参与后续的配对。这个理解错误导致我在比赛时越想越复杂,最终没能解出这道题。

2.2 正确解题思路

正确的解法其实非常简洁:

  1. 首先将a数组从大到小排序
  2. 找出b数组中的最小值minb
  3. 统计a数组中大于minb的数字数量numd,和小于等于minb的数字数量numx
  4. 最终结果就是numd! × numx!,即两部分数字排列组合数的乘积

这个解法的核心观察点是:只要a数组中有一个数字大于minb,那么这个数字一定能得到1分,因为它可以击败minb。而小于等于minb的数字无论如何排列都不会影响得分,因为它们无法击败任何b数组的牌(minb是最小的)。

2.3 关键点与注意事项

  1. 取模运算:由于结果可能很大,需要在计算阶乘的过程中不断取模(题目给定的模数是998244353)
  2. 排列顺序:虽然内部可以任意排列,但必须保证所有大于minb的数字都排在小于等于minb的数字前面,这样才能确保minb不会被"浪费"
  3. 时间复杂度:这个解法的时间复杂度是O(n),完全可以处理题目给定的数据范围

注意:在实际编码时,要特别注意数据类型的选用。由于n可能很大,阶乘结果会快速膨胀,所以要使用long long类型来避免溢出。

2.4 代码实现解析

#include<bits/stdc++.h> using namespace std; typedef long long ll; const int MOD=998244353; void solve() { ll n; cin>>n; vector<ll>a(n); vector<ll>b(n); for(auto &t:a) cin>>t; for(auto &t:b) cin>>t; auto temp=min_element(b.begin(),b.end()); ll minb=*temp; ll numd=0,numx=0; for(auto t:a) { if(t>minb) numd++; else numx++; } ll ans=1; for(int i=1;i<=numd;i++) { ans=(ans*i)%MOD; } for(int i=1;i<=numx;i++) { ans=(ans*i)%MOD; } cout<<ans<<endl; }

这段代码清晰地实现了上述思路。首先读取输入数据,然后找到b数组的最小值minb,接着统计a数组中大于和小于等于minb的数字数量,最后计算两个阶乘的乘积并输出。

3. G题解析:最大折叠数问题

3.1 题目理解与赛时困惑

G题的题目要求是:给定两个数字L和R,我们需要找到一个数字X,满足L ≤ X ≤ R,且X的反转数(即数字倒过来读)是所有满足条件的数字中最大的。

我在比赛时的思路过于复杂,试图将各种情况分类处理,导致逻辑混乱,最终只通过了30%的测试用例。实际上,这个问题有更简洁的解法。

3.2 正确解题思路

正确的解法可以总结为以下几个步骤:

  1. 特殊情况处理:如果R是形如100...000的数字(即首位是1,后面全是0),那么:

    • 如果L和R位数不同,最优解是R-1(即99...999)
    • 如果L和R位数相同,最优解只能是1(因为X必须等于R)
  2. 一般情况处理

    • 如果L和R位数不同,将L视为与R同位数的最小数字(即100...001)
    • 从高位到低位比较L和R的每一位数字
    • 找到第一个R[i] > L[i]的位置i
    • 如果i后面的数字不全是9,则将R[i]减1,后面所有位设为9
    • 反转最终得到的数字,并去除前导零

3.3 关键点与注意事项

  1. 数字反转:题目要求的是反转后的数字最大,而不是数字本身最大
  2. 前导零处理:反转后的数字要去除前导零,这是容易被忽略的细节
  3. 边界情况:特别是当R是10的幂次方时,需要特殊处理
  4. 位数差异:当L和R位数不同时,可以简化为只考虑R的情况

提示:在处理这类数字问题时,将数字转换为字符串处理通常会更方便,可以轻松访问每一位数字。

3.4 代码实现解析

#include<bits/stdc++.h> using namespace std; typedef long long ll; void solve() { ll l,r; cin>>l>>r; string L=to_string(l); string R=to_string(r); ll numl=L.size(); ll numr=R.size(); // 处理特殊情况:R是100...000 if(R[0]=='1') { bool all_zero = true; for(int i=1;i<R.size();i++) { if(R[i]!='0') all_zero=false; } if(all_zero) { if(numl<numr) { cout<<r-1<<endl; return; } if(numl==numr) { cout<<1<<endl; return; } } } // 处理位数不同的情况 if(numl<numr) { L = string(numr-1,'0'); L = "1" + L; } // 寻找第一个不同的位置 for(int i=0;i<numr;i++) { if(R[i]!=L[i]) { bool all_nine = true; for(int j=i+1;j<numr;j++) { if(R[j]!='9') all_nine=false; } if(all_nine) { reverse(R.begin(),R.end()); cout<<R<<endl; return; } else { for(int j=i+1;j<numr;j++) { R[j]='9'; } R[i]--; reverse(R.begin(),R.end()); // 去除前导零 int start=0; while(start<R.size() && R[start]=='0') start++; if(start==R.size()) cout<<"0"; else { for(int j=start;j<R.size();j++) cout<<R[j]; } cout<<endl; return; } } } // 如果L和R完全相同,直接反转 reverse(R.begin(),R.end()); // 去除前导零 int start=0; while(start<R.size() && R[start]=='0') start++; if(start==R.size()) cout<<"0"; else { for(int j=start;j<R.size();j++) cout<<R[j]; } cout<<endl; }

这段代码完整实现了上述思路。它首先处理特殊情况,然后处理一般情况,最后对结果进行反转和去除前导零的操作。代码结构清晰,逻辑严谨。

4. 比赛经验与反思

4.1 题意理解的重要性

这两道题目给我的最大教训就是:仔细阅读题目。在B题中,我因为对题目规则的理解错误导致完全走偏;在G题中,我因为没有准确把握"反转数"这个核心概念而陷入复杂的分类讨论。

在实际比赛中,建议:

  1. 至少阅读题目两遍
  2. 用自己的话复述题目要求
  3. 用简单例子验证自己的理解
  4. 特别注意题目中的限制条件和特殊说明

4.2 思维方式的优化

在解决G题时,我的思路过于碎片化,试图为每一种可能的情况编写特殊处理逻辑。这种"分情况讨论"的方法虽然有时有效,但往往会导致代码复杂且容易出错。

更好的方法是:

  1. 寻找问题的本质和规律
  2. 尝试用统一的逻辑处理大多数情况
  3. 只对真正特殊的边界情况进行单独处理
  4. 在编码前先用几个测试用例验证思路的正确性

4.3 编码实践建议

  1. 变量命名:使用有意义的变量名(如numd表示大于minb的数量,numx表示小于等于的数量)
  2. 模块化:将不同功能的代码分离,如将特殊情况的判断单独处理
  3. 注释:对关键步骤添加简要说明,方便后期review
  4. 测试:编写代码时同步考虑测试用例,特别是边界情况

5. 算法竞赛训练建议

5.1 日常训练方法

  1. 定期练习:保持每周至少10小时的专注练习时间
  2. 分类突破:针对薄弱环节(如贪心、动态规划)进行专项训练
  3. 赛后复盘:每场比赛后分析所有错题和未完成的题目
  4. 代码重构:对于AC的题目,尝试用不同的方法重新实现

5.2 比赛策略

  1. 题目选择:先快速浏览所有题目,从简单题开始
  2. 时间分配:设定每道题的时间上限,超时就暂时跳过
  3. 调试技巧:学会使用print调试和assert验证中间结果
  4. 心态管理:保持冷静,不要因为一道题卡住而影响整体发挥

5.3 资源推荐

  1. 在线判题系统:牛客网、Codeforces、AtCoder、LeetCode
  2. 学习平台:OI Wiki、CP-Algorithms、GeeksforGeeks
  3. 书籍推荐:《算法竞赛入门经典》、《挑战程序设计竞赛》
  4. 社区交流:加入算法竞赛相关的QQ群、Discord群组

通过这次训练赛的补题过程,我深刻认识到自己在思维全面性和代码实现能力上的不足。这两道题目虽然现在看起来解法很清晰,但在比赛的高压环境下,要保持清晰的思路并不容易。这需要更多的练习和经验积累。在接下来的训练中,我会更加注重对题目本质的理解和简洁高效解法的探索。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询