1. 问题背景与核心挑战
公平分配问题是计算机算法中一类经典的实际应用问题。UVa 12259这道题目模拟了一个真实的生活场景:一群朋友需要共同分摊购买礼物的费用,但每个人有不同的经济承受能力。我们需要设计一个算法,在满足各种约束条件下,找到最公平的分配方案。
这个问题的核心挑战在于:
- 支付金额必须是整数(以分为单位)
- 每个人的支付不能超过其能力上限
- 公平性的定义是多层次的:首先最小化最大差距,然后依次比较次大差距
- 当存在多个最优解时,还需要考虑支付能力和原始顺序
2. 问题分析与数学建模
2.1 问题形式化定义
给定:
- 总金额 p(1 ≤ p ≤ 10^6)
- 人数 n(2 ≤ n ≤ 100)
- 每个人的最大支付能力 a_i(1 ≤ a_i ≤ 10^6)
要求找到支付方案 x_1, x_2, ..., x_n 满足:
- 0 ≤ x_i ≤ a_i(不超过个人能力)
- Σx_i = p(总金额等于礼物价格)
- 最小化差距向量 sorted([|x_i - p/n| for all i], reverse=True) 的字典序
- 当差距向量相同时,支付能力高者应支付更多
- 当支付能力也相同时,原始序号小者应支付更多
2.2 关键观察与性质
- 可行性检查:如果 Σa_i < p,直接判定为不可能
- 理想情况:如果所有人都能支付平均值 p/n,那是最公平的
- 约束处理:对于能力不足的人,只能支付到其上限
- 差距最小化:让有能力的人分担更多,使支付分布尽可能均匀
3. 算法设计与实现
3.1 二分搜索确定最小最大差距
核心思路是通过二分法找到最小的最大差距 D,使得存在支付方案满足所有 |x_i - p/n| ≤ D。
cpp复制bool canAchieveDiff(int maxDiff, int p, int n, const vector<Person>& people,
long long& minSum, long long& maxSum) {
minSum = 0, maxSum = 0;
double avg = (double)p / n;
for (const auto& person : people) {
int lower = max((int)ceil(avg - maxDiff), 0);
int upper = min((int)floor(avg + maxDiff), person.maxPay);
if (lower > upper) return false;
minSum += lower;
maxSum += upper;
}
return minSum <= p && p <= maxSum;
}
3.2 构造最优支付方案
找到最小 D 后,按以下步骤构造方案:
- 计算每个人的支付下限 L_i = max(ceil(p/n - D), 0)
- 计算支付上限 R_i = min(floor(p/n + D), a_i)
- 初始支付 x_i = L_i
- 分配剩余金额 remain = p - ΣL_i
cpp复制// 分配剩余金额
while (remain > 0) {
bool assigned = false;
// 优先在不增加最大差距的情况下分配
for (int i = 0; i < n && remain > 0; i++) {
if (people[i].curPay < upper[i]) {
people[i].curPay++;
remain--;
assigned = true;
}
}
// 如果还有剩余,按规则分配
if (!assigned && remain > 0) {
sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
if (a.curPay != b.curPay) return a.curPay < b.curPay;
if (a.maxPay != b.maxPay) return a.maxPay > b.maxPay;
return a.id < b.id;
});
for (auto& person : people) {
if (person.curPay < person.maxPay && remain > 0) {
person.curPay++;
remain--;
break;
}
}
}
}
3.3 完整算法流程
- 输入处理与可行性检查
- 对人员按支付能力降序、序号升序排序
- 二分搜索最小最大差距 D
- 计算每个人的支付区间 [L_i, R_i]
- 初始分配 L_i
- 分配剩余金额
- 按原始顺序输出结果
4. 复杂度分析与优化
4.1 时间复杂度
- 可行性检查:O(n)
- 二分搜索:O(log p) 次迭代
- 每次二分检查:O(n)
- 构造方案:最坏 O(n^2)
- 总复杂度:O(t * (n log p + n^2))
对于题目限制 t ≤ 100, n ≤ 100, p ≤ 10^6,完全可接受。
4.2 空间复杂度
- 存储人员信息:O(n)
- 辅助数组:O(n)
- 总空间:O(n)
5. 实现细节与注意事项
5.1 浮点数精度处理
在计算平均值和差距时,需要注意浮点数精度问题:
cpp复制double avg = (double)p / n; // 显式转换为double
int lower = max((int)ceil(avg - maxDiff), 0);
int upper = min((int)floor(avg + maxDiff), person.maxPay);
5.2 排序规则实现
自定义排序是本题的关键,需要正确处理多级比较:
cpp复制// 初始排序:能力降序,id升序
sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
if (a.maxPay != b.maxPay) return a.maxPay > b.maxPay;
return a.id < b.id;
});
// 分配剩余金额时的排序:当前支付升序,能力降序,id升序
sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
if (a.curPay != b.curPay) return a.curPay < b.curPay;
if (a.maxPay != b.maxPay) return a.maxPay > b.maxPay;
return a.id < b.id;
});
5.3 边界条件处理
需要特别注意以下边界情况:
- 所有人的能力刚好等于 p
- 某些人的能力为 0
- p = 0 的情况(虽然题目中 p ≥ 1)
- n = 2 的最小情况
6. 测试用例分析
让我们分析题目提供的样例,验证算法正确性:
6.1 样例1
输入:
code复制20 4
10 10 4 4
处理过程:
- 总能力 10+10+4+4=28 ≥ 20,可行
- 平均值 20/4=5
- 二分找到最小最大差距 D=1
- 支付区间:
- 前两人:[4,6](因为能力足够)
- 后两人:[4,4](能力限制)
- 初始分配:4,4,4,4(总和16)
- 剩余4分:
- 优先分配给前两人→6,6,4,4
6.2 样例2
输入:
code复制7 3
1 1 4
处理:
- 总能力1+1+4=6 <7,直接输出IMPOSSIBLE
6.3 样例3
输入:
code复制34 5
9 8 9 9 4
处理:
- 总能力9+8+9+9+4=39≥34,可行
- 平均值34/5=6.8
- 二分找到D≈1.2
- 支付区间:
- [6,8],[6,8],[6,8],[6,8],[4,8]
- 初始分配:6,6,6,6,4(总和28)
- 剩余6分:
- 优先给能力高者→8,7,8,7,4
7. 算法优化与变种
7.1 更高效的分配策略
在分配剩余金额时,可以计算每个人还能增加的最大金额,批量分配:
cpp复制for (auto& person : people) {
if (remain == 0) break;
int canAdd = min(upper[i] - person.curPay, remain);
person.curPay += canAdd;
remain -= canAdd;
}
7.2 处理大规模数据
如果n和p更大(如n=1e5, p=1e9),可以考虑:
- 使用优先队列维护可分配人员
- 批量分配金额,而非每次1分
- 并行计算支付区间
8. 常见错误与调试技巧
8.1 常见错误
- 浮点数精度问题导致错误计算支付区间
- 排序规则实现错误,导致分配顺序不符合题意
- 忘记处理IMPOSSIBLE的情况
- 剩余金额分配时陷入死循环
8.2 调试建议
- 打印中间变量:D值、支付区间、分配过程
- 对小样例手动计算验证
- 测试极端情况:p=1e6, n=100; p=1, n=2等
- 检查排序函数是否正确实现多级比较
9. 实际应用与扩展
这个算法可以应用于:
- 分摊费用计算(如团体购物、旅行开销)
- 资源分配(如服务器负载均衡)
- 任务分配(考虑不同工作能力)
可能的扩展:
- 引入权重(不同人对礼物有不同的重视程度)
- 多物品分配(不止一个礼物)
- 动态调整(中途有人加入或退出)
10. 总结与个人体会
解决这个问题的关键在于理解多层次优化目标和各种约束条件。通过将问题分解为:
- 可行性检查
- 二分搜索确定最小最大差距
- 构造性证明存在性
- 按规则分配剩余金额
我在实现过程中深刻体会到:
- 清晰的数学建模是算法设计的基础
- 二分搜索是解决最优化问题的强大工具
- 自定义排序需要仔细验证
- 边界条件测试不可或缺
这个算法不仅解决了题目要求,其思想也可以应用于其他公平分配问题。建议读者可以尝试实现这个算法,并思考如何扩展到更复杂的场景。