1. 题目背景与需求解析
UVa 119 "Greedy Gift Givers" 是国际大学生程序设计竞赛(ICPC)和各类编程比赛中常见的模拟类练习题。这道题目源自真实生活中的礼物赠送场景,考察选手对基础数据结构的运用和边界条件的处理能力。
题目描述了一群人互相赠送礼物的过程,要求根据每个人的初始金额、赠送对象和金额分配情况,计算出最终每个人的净收入或支出。这类问题在金融结算、社交网络分析等领域都有实际应用价值。
关键提示:题目虽然逻辑简单,但隐藏了许多新手容易忽略的细节陷阱,比如零钱分配、同名处理等边界情况。
2. 数据结构设计与算法选择
2.1 输入输出分析
输入格式通常为:
N(人数) 姓名1 姓名2 ... 姓名N 后续N组数据: 赠送者姓名 初始金额 赠送人数 接收者1 接收者2 ... 接收者M输出要求显示每个人的净变化:
姓名1 净变化 姓名2 净变化 ... 姓名N 净变化2.2 核心数据结构
推荐使用两种数据结构组合:
- 哈希表/字典:存储姓名到账户余额的映射
- C++:
unordered_map<string, int> - Python:
dict[str, int]
- C++:
- 动态数组:保存原始姓名顺序
- 用于保证输出顺序与输入一致
// C++示例结构 unordered_map<string, int> balance; vector<string> names;2.3 算法流程
- 读取所有参与者姓名,初始化余额为0
- 处理每个赠送者:
- 读取初始金额和赠送人数
- 计算每人分得金额(注意整数除法)
- 赠送者账户扣除总支出
- 每个接收者账户增加相应金额
- 按原始顺序输出结果
3. 关键实现细节与陷阱
3.1 零钱处理问题
题目要求"no fractional money",意味着必须使用整数除法。常见错误包括:
# 错误做法(产生浮点数) each = amount / num_recipients # 正确做法(整数除法) each = amount // num_recipients重要细节:当赠送人数为0时,应跳过分配步骤,避免除零错误
3.2 赠送金额计算
赠送者实际支出应为each * num_recipients,而不是初始金额。因为:
- 初始金额可能无法被人数整除
- 余数应保留在赠送者账户中
if num_recipients > 0: each = amount // num_recipients balance[giver] -= each * num_recipients # 不是 -= amount3.3 输入输出规范
- 每组数据后需要输出空行(包括最后一组)
- 姓名区分大小写
- 初始金额可能是0
- 赠送人数可能为0(即不赠送任何人)
4. 完整代码实现与解析
4.1 Python实现
def solve(): first_case = True while True: try: n = int(input()) except EOFError: break if not first_case: print() first_case = False names = input().split() balance = {name: 0 for name in names} for _ in range(n): giver = input().strip() amount, m = map(int, input().split()) recipients = input().split() if m > 0 else [] if m == 0: continue each = amount // m balance[giver] -= each * m for recipient in recipients: balance[recipient] += each for name in names: print(f"{name} {balance[name]}") if __name__ == "__main__": solve()4.2 C++实现
#include <iostream> #include <vector> #include <unordered_map> using namespace std; void solve() { int n; bool first_case = true; while (cin >> n) { if (!first_case) cout << endl; first_case = false; vector<string> names(n); unordered_map<string, int> balance; for (int i = 0; i < n; ++i) { cin >> names[i]; balance[names[i]] = 0; } for (int i = 0; i < n; ++i) { string giver; int amount, m; cin >> giver >> amount >> m; if (m == 0) continue; int each = amount / m; balance[giver] -= each * m; for (int j = 0; j < m; ++j) { string recipient; cin >> recipient; balance[recipient] += each; } } for (const auto& name : names) { cout << name << " " << balance[name] << endl; } } } int main() { solve(); return 0; }5. 测试用例与调试技巧
5.1 典型测试用例
基础案例:
5 dave laura owen vick amr dave 200 3 laura owen vick owen 500 1 dave amr 150 2 vick owen laura 0 2 amr vick vick 0 0边界案例:
3 a b c a 100 0 b 50 1 a c 0 3 a b c5.2 调试技巧
- 使用小规模数据手动验证
- 打印中间变量:
- 每次分配前后的账户余额
- 计算出的each值
- 特别注意:
- 除零情况
- 最后是否多输出空行
- 余额计算是否使用整数除法
6. 算法复杂度分析
时间复杂度:O(N + M)
- N:人数
- M:所有赠送操作中的接收者总数
空间复杂度:O(N)
- 存储姓名和余额的映射
在实际竞赛中,该算法可以轻松处理题目给出的约束条件(通常N≤10)。对于大规模数据(如N≤1e5),可能需要更高效的IO处理方式。
7. 实际应用扩展
虽然题目看似简单,但其核心逻辑可以扩展到:
- 社交网络中的资金流转分析
- 团体活动费用分摊计算
- 区块链交易模拟
- 财务系统中的往来账目核对
我在实际开发中曾用类似逻辑处理过电商平台的优惠券分发系统,关键点同样在于正确处理整数分配和边界情况。一个经验是:当涉及金额分配时,务必使用专门的十进制数据类型(如Python的Decimal),避免浮点数精度问题。