第一次看到蓝桥杯这道"三国游戏"题目时,我盯着题目描述足足发了五分钟呆。题目大意是说有三个国家X、Y、Z,每个国家有n个武将,每个武将有三个属性值分别代表对三个国家的忠诚度。我们需要选择一个国家作为己方,通过挑选武将使得己方武将的总忠诚度大于另外两国武将的总和。这听起来就像是在玩一款策略游戏,需要做出最优的武将选择。
我尝试用具体例子来理解:假设有三个武将,属性分别是(100,80,90)、(90,110,85)、(95,95,120)。如果选择X国作为己方,那么第一个武将的X国忠诚度100需要大于Y+Z的80+90=170吗?显然100>170不成立。这说明我的第一理解有误——实际上题目要求的是所有选中武将的X忠诚度总和,要大于这些武将的Y和Z忠诚度的总和。
这个理解上的弯路让我意识到,在算法竞赛中,准确理解题意往往比急着写代码更重要。于是我把问题重新表述为:从n个三元组中选出尽可能多的三元组,使得这些三元组在某个维度上的和,大于另外两个维度的和。
面对这个问题,我的第一反应是:能不能用贪心算法?贪心算法通常适用于"局部最优能导致全局最优"的场景。在这个问题中,我们需要选择尽可能多的武将,那么很自然地想到应该优先选择"性价比"最高的武将。
具体来说,对于选择X国的情况,我们需要考虑每个武将的a_i(X国忠诚度)与b_i+c_i(Y+Z忠诚度)的差值。这个差值越大,说明选择这个武将对我们越有利。于是直觉告诉我,应该按照a_i-(b_i+c_i)从大到小的顺序选择武将,直到无法满足总和条件为止。
为了验证这个直觉,我设计了一个小测试用例:
按照贪心策略,我们会先选差值最大的武将1和3,它们的a_i和是11,b_i+c_i和是9,确实满足11>9。如果再加选武将2,总和就变成15>14,仍然满足但数量更多。这个例子验证了贪心策略的有效性。
虽然例子验证了贪心策略,但在算法竞赛中,没有证明的贪心都是耍流氓。我们需要用更严谨的方法证明这个贪心策略的正确性。这里我采用了邻项交换法,这是证明贪心算法正确性的常用技巧。
假设我们有两个武将i和j,其中a_i-(b_i+c_i) > a_j-(b_j+c_j)。我们需要证明在任何最优解中,如果同时包含这两个武将,i一定排在j前面。
反证法:假设存在一个最优解,j排在i前面。那么我们可以交换i和j的位置:
考虑交换前后总和的变化。由于a_i-(b_i+c_i) > a_j-(b_j+c_j),即(a_i-b_i-c_i) > (a_j-b_j-c_j),这意味着交换后的总和会更大。因此,任何不按这个顺序排列的解都可以通过邻项交换得到更优解,这与假设矛盾。这就证明了我们的贪心策略是正确的。
理解了算法原理后,代码实现就相对直接了。以下是完整的C++实现,我添加了详细注释:
cpp复制#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
// 计算选择某个国家能得到的最大武将数
int calculate(vector<int>& a, vector<int>& b, vector<int>& c) {
vector<int> diff(n);
// 计算每个武将的a-(b+c)差值
for(int i = 0; i < n; i++) {
diff[i] = a[i] - (b[i] + c[i]);
}
// 按差值从大到小排序
sort(diff.begin(), diff.end(), greater<int>());
int sum = 0, count = 0;
for(int i = 0; i < n; i++) {
// 尝试加入当前武将
if(sum + diff[i] > 0) {
sum += diff[i];
count++;
} else {
break; // 无法满足条件时停止
}
}
return count;
}
void solve() {
cin >> n;
vector<int> a(n), b(n), c(n);
// 输入数据
for(int i = 0; i < n; i++) cin >> a[i];
for(int i = 0; i < n; i++) cin >> b[i];
for(int i = 0; i < n; i++) cin >> c[i];
// 分别计算三个国家作为己方的情况
int ans = max({
calculate(a, b, c), // X国
calculate(b, a, c), // Y国
calculate(c, a, b) // Z国
});
cout << (ans ? ans : -1) << endl;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
int t = 1;
while(t--) solve();
}
在实际编码时,有几个优化点值得注意:
greater<int>()进行降序排序,比默认升序排序后再逆序遍历更直观max({...})语法简洁地比较多个值ios::sync_with_stdio(0)加速在实现这个算法时,我踩过几个坑,这里分享给大家避免重复犯错:
错误1:错误理解题意
最初我误以为是要每个被选中的武将单独满足a_i > b_i + c_i,导致完全错误的解法。这种理解错误在竞赛中很常见,解决方法是仔细阅读题目,特别是用样例验证自己的理解。
错误2:忽略边界条件
当所有武将的差值都为负时,理论上应该返回0,但题目要求返回-1。这种特殊情况的处理容易被忽略。我的经验是:永远要考虑空集、全负、全等等边界情况。
错误3:排序方向错误
贪心策略依赖于正确的排序方向。有次我忘记指定降序排序,导致结果完全错误。现在我会在排序代码后立即添加断言或打印语句验证排序结果。
调试这类问题时,我通常会:
让我们分析这个算法的时间和空间复杂度:
时间复杂度:
空间复杂度:
虽然这个复杂度已经足够好,但还有优化空间:
在实际比赛中,O(n log n)的解法通常已经足够,更重要的还是保证代码的正确性和可读性。
通过这道题,我们可以总结出贪心算法解题的一般模式:
贪心算法在以下场景特别有效:
但要注意,贪心算法并不总是适用。当问题具有最优子结构性质时,可能需要动态规划。区分这两者的关键是要看局部最优是否能保证全局最优。
为了巩固贪心算法的应用能力,我推荐练习以下类似题目:
对于想进一步提高的同学,可以尝试这些变种问题:
在刷题过程中,我建议养成以下习惯:
这道三国游戏题目很好地展示了贪心算法的思考过程:从问题理解到直觉构建,从严格证明到代码实现。在实际比赛中,遇到类似问题时,我会先花时间验证贪心策略的正确性,而不是急于编码。有时候,多花5分钟思考可以节省30分钟的调试时间。