大家好,我是每天都在机房里观察“人类早期驯服代码珍贵影像”的新手教练!
今天下午,机房里传出了一声哀嚎。我们家极具潜力的小信同学,正对着一道看似简单得不能再简单的普及组题目——《田忌赛马》疯狂抓头发。
看到这题,很多初学者的第一反应绝对和小信一模一样:“教练!这题我熟啊!小学二年级语文课本写得明明白白的,用我的下等马去兑掉田忌的上等马,然后用我的上等马打他的中等马……”
01:被九年义务教育“封印”的贪心思维
伴随着这种根深蒂固的“故事思维”,小信一开始在屏幕上敲出的 雏形代码,是极其惨烈的。
他满脑子都是“对应”,于是企图搞一个极其复杂的模拟:
// 小信的脑内小剧场伪代码(极其危险的尝试)
sort(my_h.begin(), my_h.end());
// 题目说田忌按顺序出战,所以我不能随便打乱田忌的马!
for(int i = 0; i < n; i++) {
int cur_tian = tian_h[i]; // 田忌当前出战的马
// 试图在自己的马里找一匹刚好能赢的,或者找一匹最弱的去送死
// 于是嵌套了一个 O(N) 的遍历查找...
}
初学者在做这道题时,100% 会掉进题目挖好的两个深坑:
- 文字陷阱:“田忌会按顺序派出他的马匹”。很多人一看这句话,就不敢给田忌的马排序了。
- 剧情陷阱:“下等马换上等马”。为了还原这个名场面,很多人试图在代码里去精准定位“田忌最强的那匹马”,非要亲自看着自己的炮灰去给大哥送人头。
结果就是,原本简单的逻辑被搞成了 的嵌套循环,在 的数据规模下面前,被评测机无情绞杀!
02:从“语文课”到“算法课”
看小信快把键盘敲冒烟了,我拉了把椅子坐过去,开始了“灵魂拷问”。
回合一:打破“出场顺序”的幻觉
我敲了敲他的屏幕:“小信,我问你,题目最后要求你输出的是什么?”
“最多获胜的轮次啊。”
“要求你输出对阵表了吗?”
“没有。”
“既然不看对阵表,你管田忌先出哪匹马干嘛?他第一局出最强的,和最后一局出最强的,对你最终赢的总局数有影响吗?”
小信愣了一下,猛地拍了一下大腿:“对啊!反正最后算的是总人头数,顺序根本不重要!” 没错!在信息学竞赛中,只要结果与排列顺序无关,第一步永远是想办法给数组排序(Sort),让杂乱无章的数据具备单调性!
回合二:贪心的极致——好钢用在刀刃上
“既然双方的马都从小到大排好序了,咱们从最弱的马开始比。”我继续引导,“假设你当前最弱的马,刚好比田忌最弱的马快一点点,你怎么做?”
小信脱口而出:“那肯定直接吃掉它啊!拿下一分,这两匹马双双退役。” 这正是贪心算法的核心:以最小的代价,换取确定的胜利,绝对不让伤害溢出。
回合三:炮灰的自我修养(高光时刻)
“好,最刁钻的问题来了。”我盯着他,“如果你的最弱马,连田忌的最弱马都跑不赢。按照语文课本,你是不是要把它送去给田忌的最强马当炮灰?”
小信刚想点头,突然意识到了什么,眼神变了。
“等等教练……既然我的这匹马连他最弱的都跑不赢,那说明因为数组排过序,我这匹马是个纯纯的废物,它打不赢田忌剩下的任何一匹马!”小信的思维开始发光,“既然是注定淘汰的废物,我凭什么要费劲心思去找田忌的最强马?我直接当它不存在,把它扔进垃圾桶,用我下一匹稍微强一点的马,继续来死磕田忌这匹最弱的马就行了啊!”
“啪啪啪!”我在机房里忍不住为他鼓掌。 这才是真正的 OIer 思维!抽象剥离了无用的业务逻辑(找最强马),直击数据的本质(指针后移,忽略劣质数据)。
03:极其优雅的双指针
思路彻底打通后,小信文思泉涌。他不仅完美实现了一个时间复杂度仅为 (排序时间)的代码,还给我秀了一把极其干净的现代 C++ 工程素养。
这套 金牌标准 AC 代码,每一个细节都值得反复咀嚼:
#include<bits/stdc++.h>
usingnamespacestd;
intmain(){
// 【高能细节 1:解除 I/O 封印】
// 面向大数据的必备防身术,让 cin/cout 的速度比肩 scanf/printf
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
// 【高能细节 2:拥抱现代 C++ 容器】
// 拒绝 int a[100005],使用 vector 精准分配内存
vector<int> my_h(n), tian_h(n);
for(int i = 0; i < n; ++i) cin >> my_h[i];
for(int i = 0; i < n; ++i) cin >> tian_h[i];
// 【核心步骤 1:排序降维】时间复杂度 O(N log N)
sort(my_h.begin(), my_h.end());
sort(tian_h.begin(), tian_h.end());
// 【核心步骤 2:双指针收割】时间复杂度 O(N)
int my_idx = 0, tian_idx = 0, ans = 0;
// 【高能细节 3:极简的防越界逻辑】
// 为什么只判断 my_idx < n?
// 因为不管赢还是输,my_idx 永远在向右走,它的进度 >= tian_idx。
// my_idx 不越界,tian_idx 就绝对安全!
while(my_idx < n){
if(my_h[my_idx] > tian_h[tian_idx]){
// 赢了:胜场+1,双方最弱马一起出局,指针双双后移
ans++;
my_idx++;
tian_idx++;
} else {
// 输了:我的这匹马是彻底的废物,直接丢弃(指针后移)
// 用我的下一匹马继续死磕田忌当前的这匹马
my_idx++;
}
}
cout << ans;
return0;
}
04:教练笔记
这道题绝对是测试一个人有没有“算法脑”的绝佳试金石。今天咱们的核心收获如下:
- 破除“拟真”执念:在算法竞赛中,不要过度沉迷于还原题目的现实场景(比如还原语文课本里的对阵表)。只要题目只问“最优结果”,贪心 + 排序 往往就是撕开防线的利刃。
- 双指针模型(Two Pointers):对于两个有序数组的匹配问题,双指针是神级武器。它的本质是利用数组的单调性,把原本需要 的嵌套循环匹配,强行降维打压到 的线性扫描。
- 寻找不变量(Invariant)化简代码:在
while 循环条件中,敏锐地察觉到 my_idx >= tian_idx 这个不变式,从而省去了冗余的越界判断,这正是让代码变得优雅的终极秘诀。
今天的机房没有拖拉机,只有双指针开出的满载高铁!学会这一招,下次再遇到田忌赛马,不用孙膑出马,你也能把齐威王安排得明明白白!
#信奥赛 #算法思维 #贪心算法 #C++编程 #双指针 #田忌赛马 #CSP普及组 #刷题复盘