P1650 田忌赛马
题目描述
我国历史上有个著名的故事: 那是在 2300 2300 2300 年以前。齐国的大将军田忌喜欢赛马。他经常和齐王赛马。他和齐王都有三匹马:常规马,上级马,超级马。一共赛三局,每局的胜者可以从负者这里取得 200 200 200 银币。每匹马只能用一次。齐王的马好,同等级的马,齐王的总是比田忌的要好一点。于是每次和齐王赛马,田忌总会输 600 600 600 银币。
田忌很沮丧,直到他遇到了著名的军师――孙膑。田忌采用了孙膑的计策之后,三场比赛下来,轻松而优雅地赢了齐王 200 200 200 银币。这实在是个很简单的计策。由于齐王总是先出最好的马,再出次好的,所以田忌用常规马对齐王的超级马,用自己的超级马对齐王的上级马,用自己的上级马对齐王的常规马,以两胜一负的战绩赢得 200 200 200 银币。实在很简单。
如果不止三匹马怎么办?这个问题很显然可以转化成一个二分图最佳匹配的问题。把田忌的马放左边,把齐王的马放右边。田忌的马 A 和齐王的 B 之间,如果田忌的马胜,则连一条权为 200 200 200 的边;如果平局,则连一条权为 0 0 0 的边;如果输,则连一条权为 − 200 -200 −200 的边……如果你不会求最佳匹配,用最小费用最大流也可以啊。 然而,赛马问题是一种特殊的二分图最佳匹配的问题,上面的算法过于先进了,简直是杀鸡用牛刀。现在,就请你设计一个简单的算法解决这个问题。
输入格式
第一行一个整数 n n n ,表示他们各有几匹马(两人拥有的马的数目相同)。第二行 n n n 个整数,每个整数都代表田忌的某匹马的速度值($0 \le $ 速度值 ≤ 100 \le 100 ≤100)。第三行 n n n 个整数,描述齐王的马的速度值。两马相遇,根据速度值的大小就可以知道哪匹马会胜出。如果速度值相同,则和局,谁也不拿钱。
输出格式
仅一行,一个整数,表示田忌最大能得到多少银币。
输入输出样例 #1
输入 #1
3
92 83 71
95 87 74
输出 #1
200
说明/提示
数据规模与约定
- 对于 20 % 20\% 20% 的数据, 1 ≤ N ≤ 65 1\le N\le 65 1≤N≤65;
- 对于 40 % 40\% 40% 的数据, 1 ≤ N ≤ 250 1\le N\le 250 1≤N≤250;
- 对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 2000 1\le N\le2000 1≤N≤2000。
题解:(贪心)
一、贪心策略详解
采用如下贪心策略来最大化田忌的得分:
- 将田忌和齐王的马按速度从高到低排序
- 使用双指针:
tfirst
和tendd
分别指向田忌最快和最慢的马;qfirst
和qendd
分别指向齐王最快和最慢的马。
- 每次比较当前最快的两匹马,根据情况做出决策:
决策逻辑如下:
情况 | 策略 | 得分变化 |
---|---|---|
田忌快马 > 齐王快马 | 直接比一场,赢了 | +200 |
田忌快马 < 齐王快马 | 用田忌最慢的马去输掉齐王最快的马(牺牲) | -200 |
田忌快马 == 齐王快马 | 查看最慢马是否能赢对方最慢马: - 能赢 → 最慢马 vs 最慢马,赢 - 否则 → 牺牲最慢马 | +200 或 -200 |
二、贪心策略的正确性证明
1. 当田忌快马 > 齐王快马时:直接比赛
这是显然正确的。因为如果不打这一场,而是用这匹好马去对齐王的一匹慢马,虽然也能赢,但会浪费这匹快马的优势,可能影响后续更关键的比赛。
结论:此时应该直接比,赢得 200 分。
2. 当田忌快马 < 齐王快马时:用最慢马去牺牲
田忌的快马不能赢,那么它最多只能和对方慢马打成平局或输给对方慢马。但如果我们用最慢马去对齐王快马,就能保留住田忌的快马用于后续比赛。
结论:牺牲最慢马是明智的选择。
3.当田忌快马 == 齐王快马时:(核心)
这时候我们需要考虑如何安排当前马匹以获得最大收益。
(a) 如果田忌最慢马 > 齐王最慢马:
那就让这两匹马比赛,赢得 200 分,保留快马应对后面的对手。
(b) 如果田忌最慢马 <= 齐王最慢马:
那无论如何都赢不了,不如用这匹慢马去对齐王快马,避免浪费其他好马。
结论:在这种情况下,优先考虑用最慢马赢对方最慢马;否则就牺牲最慢马。
- 注意这里 牺牲最慢马的 代码逻辑(不要对 平局 实施 报复性扣分)
综合以上三点,贪心策略是局部最优且全局最优的。
三、完整代码
#include <iostream>
#include <vector>
#include <algorithm>
using ll = long long;
using namespace std;
int main() {
ll n;
cin >> n; // 输入马的数量
// 定义两个数组存储田忌和齐王的马的速度
vector<ll> t(n); // 田忌的马
vector<ll> q(n); // 齐王的马
// 输入田忌的马的速度
for (ll i = 0; i < n; i++) cin >> t[i];
// 输入齐王的马的速度
for (ll i = 0; i < n; i++) cin >> q[i];
// 将田忌和齐王的马按速度从大到小排序(降序)
sort(t.begin(), t.end(), greater<ll>());
sort(q.begin(), q.end(), greater<ll>());
ll ender = 0; // 记录最终得分,初始为0
// 双指针策略:
ll tfirst = 0, tendd = n - 1; // 田忌最快和最慢的马的下标
ll qfirst = 0, qendd = n - 1; // 齐王最快和最慢的马的下标
// 开始比赛:只要还有马没有比完,就继续循环
while (tfirst <= tendd)
{
// 情况一:田忌当前最快的马 > 齐王当前最快的马
if (t[tfirst] > q[qfirst])
{
ender += 200; // 赢得本场比赛,加200分
tfirst++; // 田忌向右移动一位(下一匹最快的马)
qfirst++; // 齐王也向右移动一位
}
// 情况二:田忌当前最快的马 < 齐王当前最快的马
else if (t[tfirst] < q[qfirst])
{
ender -= 200; // 输掉本场比赛,扣200分
tendd--; // 田忌牺牲最慢的马(向左移动)
qfirst++; // 齐王使用当前最快的马去赢,所以向右移动
}
// 情况三:田忌当前最快的马 == 齐王当前最快的马(平局)【核心点】
else
{
// 先尝试用田忌最慢的马 vs 齐王最慢的马
if (t[tendd] > q[qendd])
{
ender += 200; // 如果能赢,则让这两匹慢马比赛
tendd--; // 田忌和齐王都去掉最慢的马
qendd--;
}
// 如果田忌最慢马无法战胜齐王最慢马
else
{
// 此时无论是否平局,都选择用田忌最慢马去对齐王最快的马(牺牲)
// 走到这个分支,当前田忌最慢马一定无法战胜齐王最快马:
// - 如果田忌最慢马 < 齐王最快马 → 输掉比赛,扣200分
if (t[tendd] < q[qfirst])
{
ender -= 200; // 如果输的话扣200分
}
// - 如果等于,则为平局,不加分也不扣分
tendd--; // 田忌最慢马已用,去掉
qfirst++; // 齐王当前最快马已用,去掉
}
}
}
// 输出最终得分
cout << ender << endl;
return 0;
}
四、常见实现误区与注意事项
在实现田忌赛马问题的过程中,一个常见的逻辑错误出现在处理
快马 == 快马 且 慢马 <= 慢马
的情况时
1.错误写法示例:
else {
ender -= 200; // 直接扣分
tendd--;
qfirst++;
}
这种写法的问题在于:无论田忌最慢马是否能和齐王最快马打平 或 打赢,都直接扣200分。这会导致以下一种合法情况被错误地当作失败处理:
情况 | 实际结果 | 是否应该扣分 |
---|---|---|
田忌慢马 == 齐王快马 | 平局 | ❌ 不应扣分 |
为了更好地说明这个问题,我们来看两组测试样例。
🔍 样例1:田忌慢马 < 齐王快马(应扣分)
输入:
2
3 1
4 2
排序后:
- 田忌:[3, 1]
- 齐王:[4, 2]
执行过程:
- 第一轮:3 < 4 → 牺牲慢马 1 vs 4 → 扣200分
- 第二轮:3 vs 2 → 赢 +200分
✅ 正确得分:0
❌ 若不加判断强制扣分,最终得分为 -200(错误)
🔍 样例2:田忌慢马 == 齐王快马(不应扣分)
输入:
2
3 2
3 1
排序后:
- 田忌:[3, 2]
- 齐王:[3, 1]
执行过程:
- 第一轮:快马相等,慢马 2 > 1 → +200分
- 第二轮:快马相等,慢马 == 快马 → 不加分也不扣分
✅ 正确得分:200
❌ 若不加判断强制扣分,最终得分为 0(错误)
2.正确做法:
只有当田忌慢马 确实小于 齐王快马时才应当扣分:
else
{
if (t[tendd] < q[qfirst])
ender -= 200; // 只有输的时候才扣分
tendd--;
qfirst++;
}
这样就能保证得分系统准确反映比赛结果,避免对平局做出惩罚性扣分
四、时间复杂度分析
操作 | 时间复杂度 |
---|---|
排序 | O(n log n) |
主循环 | O(n) |
总体 | O(n log n) |