1. 项目背景与问题定义
P1080 [NOIP 2012 提高组] 国王游戏是一道经典的算法竞赛题目,源自全国青少年信息学奥林匹克联赛(NOIP)提高组的真题。这道题目考察了参赛者对贪心算法、大数运算和数学推导的综合运用能力。
题目描述了一个有趣的场景:国王和他的大臣们玩一个特殊的游戏。每位大臣左右手上各写有一个正整数,国王指定一个排列顺序让大臣们排成一列。每位大臣获得的奖励是他前面所有人左手上的数的乘积除以他自己右手上的数。我们需要找到一种排列顺序,使得获得最多奖励的大臣获得的奖励尽可能少。
这个问题的实际意义在于资源分配的公平性优化。类似场景可以延伸到:
- 任务调度中的负载均衡
- 生产线工序安排
- 分布式计算中的任务分配
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法思路解析
2.1 贪心策略的直觉
初看这个问题,可能会想到以下几种排列方式:
- 按左手数字升序排列
- 按右手数字升序排列
- 按左右手数字之和排序
- 按左右手数字乘积排序
经过简单验证可以发现,前三种策略都不能保证得到最优解。正确的解法是采用第四种策略:按照左右手数字的乘积升序排列。
2.2 严格的数学证明
设相邻的两个大臣i和j,i在j前面。设他们前面所有人的左手乘积为S。
当前排列的这两个大臣的最大值为:
max(S/r_i, S×l_i/r_j)
如果交换i和j的位置,则最大值为:
max(S/r_j, S×l_j/r_i)
为了使得交换后的结果不会更差,需要满足:
max(S/r_i, S×l_i/r_j) ≤ max(S/r_j, S×l_j/r_i)
可以推导出当l_i×r_i ≤ l_j×r_j时,原排列不会比交换后的排列更差。因此,按照l×r升序排列是最优的。
3. 实现细节与优化
3.1 大数处理技巧
由于题目中n可以达到1000,每个l和r可以达到10000,最终的乘积可能达到10000^1000量级,远超普通数据类型的表示范围。因此需要实现高精度运算:
cpp复制struct BigInt {
vector<int> digits;
BigInt(int x = 0) {
while(x) {
digits.push_back(x % 10);
x /= 10;
}
}
BigInt& operator*=(int x) {
int carry = 0;
for(int &d : digits) {
int prod = d * x + carry;
d = prod % 10;
carry = prod / 10;
}
while(carry) {
digits.push_back(carry % 10);
carry /= 10;
}
return *this;
}
BigInt operator/(int x) const {
BigInt res;
res.digits.resize(digits.size());
int remainder = 0;
for(int i = digits.size()-1; i >= 0; i--) {
int curr = remainder * 10 + digits[i];
res.digits[i] = curr / x;
remainder = curr % x;
}
while(res.digits.size() > 1 && res.digits.back() == 0) {
res.digits.pop_back();
}
return res;
}
bool operator<(const BigInt& other) const {
if(digits.size() != other.digits.size()) {
return digits.size() < other.digits.size();
}
for(int i = digits.size()-1; i >= 0; i--) {
if(digits[i] != other.digits[i]) {
return digits[i] < other.digits[i];
}
}
return false;
}
};
3.2 完整算法实现
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Minister {
int l, r;
bool operator<(const Minister& other) const {
return l * r < other.l * other.r;
}
};
int main() {
int n;
cin >> n;
vector<Minister> ministers(n);
for(int i = 0; i < n; i++) {
cin >> ministers[i].l >> ministers[i].r;
}
sort(ministers.begin(), ministers.end());
BigInt product(1);
BigInt max_reward(0);
for(int i = 0; i < n; i++) {
if(i > 0) {
BigInt reward = product / ministers[i].r;
if(max_reward < reward) {
max_reward = reward;
}
}
product *= ministers[i].l;
}
// 输出max_reward
for(int i = max_reward.digits.size()-1; i >= 0; i--) {
cout << max_reward.digits[i];
}
cout << endl;
return 0;
}
4. 常见问题与调试技巧
4.1 边界条件处理
- 当n=1时,结果应该是0,因为没有其他大臣在前
- 当所有l或r为1时,乘积不会溢出32位整数,但仍需用高精度
- 当有大臣的r=1时,除法结果等于被除数本身
4.2 性能优化
- 高精度乘法的优化:可以使用更大的基数(如10000)来减少运算次数
- 高精度除法的优化:可以预先估计商的位数,避免不必要的计算
- 排序前可以过滤掉l=0或r=0的异常情况(根据题目描述应该不会出现)
4.3 调试技巧
- 先用小规模数据测试排序是否正确
- 单独测试高精度乘法和除法
- 对于n=2的情况手动计算验证
- 输出中间结果检查乘积是否正确累积
5. 算法扩展与应用
这个问题的贪心策略可以推广到类似的资源分配问题中,只要满足:
- 目标是最小化某种最大值
- 交换相邻元素的影响可以局部评估
例如在任务调度中,如果有n个任务,每个任务有处理时间t和权重w,希望最小化最大完成时间乘以权重,可以使用类似的策略按t/w排序。
另一个应用是在投资组合优化中,当需要平衡多个投资项目的风险和收益时,可以借鉴这种排序思想。
