1. 问题背景与理解
国王游戏是一道经典的贪心算法题目,源自NOIP 2012提高组。题目描述了一个有趣的场景:国王和大臣们各自在左右手上写下整数,通过特定的排列顺序,使得获得最多金币的大臣所获金币尽可能少。
这个问题的核心在于理解金币计算方式:每位大臣获得的金币数等于前面所有人左手数字的乘积除以自己右手数字的向下取整。我们需要找到一种排列顺序,使得所有大臣中获得金币最多的那个人的金币数最小化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 贪心策略的推导
2.1 基本思路分析
要解决这个问题,我们需要确定大臣的排列顺序。直观上,我们希望让可能获得大量金币的大臣尽量往后排,因为他们的金币计算会涉及前面所有人的左手乘积。
考虑两个相邻大臣i和j的交换对结果的影响。设前面所有人的左手乘积为S,则:
- 原顺序i在前:i得S/b_i,j得(S×a_i)/b_j
- 交换后j在前:j得S/b_j,i得(S×a_j)/b_i
我们需要比较max(S/b_i, (S×a_i)/b_j)和max(S/b_j, (S×a_j)/b_i),选择较小的那个。
2.2 关键比较条件
通过数学推导,我们可以得出:当a_i×b_i < a_j×b_j时,应该让i排在j前面。这意味着我们应该按照a×b的值从小到大排序大臣。
这个结论的推导过程:
- 假设原顺序更优,则max(S/b_i, (S×a_i)/b_j) ≤ max(S/b_j, (S×a_j)/b_i)
- 由于S/b_i ≤ (S×a_j)/b_i(因为a_j ≥1),所以只需比较(S×a_i)/b_j和(S×a_j)/b_i
- 化简得a_i×b_i ≤ a_j×b_j
3. 高精度处理的必要性
3.1 数据范围分析
题目中n可达1000,a和b可达10000。在最坏情况下,左手乘积可能达到10000^1000,这远远超出了普通数据类型的表示范围(如long long最大约10^18)。
3.2 高精度实现方案
我们需要实现高精度整数的以下操作:
- 高精度×低精度
- 高精度÷低精度
- 高精度比较
在C++中,我们可以用vector
4. 完整代码解析
4.1 数据结构定义
cpp复制struct Hand {
int left;
int right;
};
class BigInt {
private:
vector<int> digits; // 倒序存储数字
public:
// 赋值运算符重载
BigInt& operator=(int num) {
digits.clear();
if (!num) {
digits.push_back(0);
return *this;
}
while (num) {
digits.push_back(num % 10);
num /= 10;
}
return *this;
}
// 其他成员函数...
};
4.2 高精度乘法实现
cpp复制BigInt operator*(int num) {
BigInt res;
int carry = 0;
for (int i = 0; i < this->digits.size() || carry; i++) {
if (i < this->digits.size())
carry += this->digits[i] * num;
res.digits.push_back(carry % 10);
carry /= 10;
}
return res;
}
4.3 高精度除法实现
cpp复制BigInt operator/(int num) {
BigInt res;
res.digits.resize(this->digits.size());
int remainder = 0;
for (int i = this->digits.size() - 1; i >= 0; i--) {
remainder = remainder * 10 + this->digits[i];
res.digits[i] = remainder / num;
remainder %= num;
}
// 去除前导零
while (res.digits.size() > 1 && res.digits.back() == 0)
res.digits.pop_back();
return res;
}
4.4 主算法流程
cpp复制int main() {
// 输入处理
cin >> n;
lineup.resize(n + 1);
for(int i = 0; i <= n; i++) {
cin >> lineup[i].left >> lineup[i].right;
}
// 按a*b排序(国王不参与排序)
sort(lineup.begin() + 1, lineup.end(), [](const Hand &h1, const Hand &h2) {
return (h1.left * h1.right) < (h2.left * h2.right);
});
// 计算最大金币
BigInt product = lineup[0].left;
BigInt ans, tmp;
for (int i = 1; i <= n; i++) {
tmp = product / lineup[i].right;
if (ans < tmp)
ans = tmp;
product = product * lineup[i].left;
}
cout << ans;
return 0;
}
5. 算法正确性证明
5.1 贪心选择性质
我们需要证明按照a_i×b_i排序得到的解是最优的。假设存在一个最优解不满足我们的排序规则,那么必然存在相邻的两个大臣i和j,其中a_i×b_i > a_j×b_j但i排在j前面。
通过交换这两个大臣,可以证明不会得到更差的结果:
- 交换前最大值可能是(S×a_i)/b_j
- 交换后最大值可能是(S×a_j)/b_i
- 由于a_i×b_i > a_j×b_j ⇒ (S×a_i)/b_j > (S×a_j)/b_i
- 因此交换后最大值不会更大,可能更小
5.2 最优子结构性质
在已经排好前k个大臣后,剩下的n-k个大臣的子问题也需要按照相同的规则排序。这说明问题具有最优子结构性质。
6. 时间复杂度分析
- 排序时间复杂度:O(n log n)
- 高精度乘法:每次O(d),其中d是数字位数
- 高精度除法:每次O(d)
- 总共n次乘法和除法操作
最坏情况下,数字位数d可能达到O(n log a_max) ≈ 40000位(1000×log10(10000)),因此总时间复杂度约为O(n^2 log a_max)。
7. 优化与注意事项
7.1 高精度运算优化
- 可以采用更高效的乘法算法(如Karatsuba算法)
- 使用更大的进制(如10000进制)减少运算次数
- 预先分配足够的空间避免频繁内存分配
7.2 边界情况处理
- 当n=0时,直接输出0
- 处理左手或右手为0的情况(题目保证0 < a,b < 10000)
- 乘积为0时的特殊处理
7.3 实际编码技巧
- 高精度类应该实现完整的运算符重载
- 可以添加输出格式化功能
- 添加调试输出便于检查中间结果
8. 扩展与变种
8.1 问题变种
- 求最大金币的最小值改为求金币的某种函数最优
- 改变金币计算方式(如加法替代乘法)
- 引入更复杂的约束条件
8.2 实际应用
这类问题在实际中出现在:
- 任务调度优化
- 资源分配问题
- 排队论中的最优排序
9. 常见错误与调试
9.1 典型错误
- 忘记处理高精度导致溢出
- 排序规则实现错误
- 前导零处理不当
- 国王参与排序
9.2 调试建议
- 用小数据测试排序是否正确
- 单独测试高精度类
- 输出中间乘积检查计算过程
- 比较不同排列的结果验证贪心策略
10. 总结与个人体会
这道题目很好地结合了贪心算法和高精度处理两个重要知识点。在实际编码中,我发现以下几点特别重要:
- 高精度类的实现要全面考虑各种运算和比较
- 贪心策略的证明需要严谨的数学推导
- 边界情况的处理往往决定程序的鲁棒性
- 算法效率在实际比赛中至关重要,需要合理估计时间复杂度
对于类似的题目,我建议先在小规模数据上验证算法正确性,再逐步扩展到大规模数据。同时,高精度运算的实现要尽可能模块化,便于调试和重用。
