【问题标题】:Algorithm to simplify a weighted directed graph of debts简化债务加权有向图的算法
【发布时间】:2013-03-30 20:31:14
【问题描述】:

我一直在使用我编写的一个小 Python 脚本来管理室友之间的债务。它有效,但缺少一些功能,其中之一是简化不必要的复杂债务结构。例如,如果下面的加权有向图代表一些人,箭头代表他们之间的债务(爱丽丝欠鲍勃 20 美元,查理欠查理 5 美元,鲍勃欠查理 10 美元等):

很明显,这张图应该简化为下图:

如果爱丽丝可以直接把 10 美元给查理,那么从爱丽丝到鲍勃再从鲍勃到查理的 10 美元是没有意义的。

因此,在一般情况下,目标是获取债务图并对其进行简化(即生成具有相同节点但不同边的新图),以便

  1. 没有一个节点有边指向它的内部和外部(没有无用的货币易手)
  2. 所有节点通过它们的“流量”与它们在原始图中的相同(在资金流向方面是相同的)。

“流”是指所有输入的值减去所有输出的值(对此有专门术语吗?我不是图论专家)。所以在上面的例子中,每个节点的流量值为:

  • 鲍勃:+10
  • 爱丽丝:-25
  • 查理:+15

您可以看到第一张和第二张图通过每个节点的流量相同,因此这是一个很好的解决方案。还有一些其他简单的情况,例如,任何循环都可以通过删除最低值的边并从所有其他边中减去它的值来简化。

这个:

应该简化成这样:

我无法想象没有人研究过这个问题;我只是不知道要搜索哪些术语来查找相关信息(同样,不是图论专家)。我一直在寻找几个小时无济于事,所以我的问题是:根据上面为任何加权有向图指定的条件,有什么算法可以产生简化(新图)?

【问题讨论】:

  • 整理的标准是什么。即使在您的最后一种情况下,丹尼尔也可以给鲍勃“3”,而爱丽丝给查理“8”和鲍勃“7”。
  • 哦,好点。我认为最佳算法也会最小化边的数量(你的建议),但我会满足于我在问题中指定的两个标准。
  • 从算法的角度来看这个问题很有趣,但你认为这在实践中可行吗?如果A欠B,B欠C,C为什么要从A那里拿钱? C借钱给B不知道A破产了...
  • 我们用它来管理因购买公共食品和支付账单而产生的债务,本身没有直接贷款。让 A 直接向 C 付款的想法是避免不必要的物流(A 必须找到 B 才能给钱,然后 B 必须找到 C,而不是 A 直接找到 C)。

标签: algorithm graph-theory graph-algorithm


【解决方案1】:

这是一篇详细研究这个问题的学术论文。接近尾声的第 8 节中还有一些不同算法的示例代码。

Verhoeff, T. (2004). Settling multiple debts efficiently : an invitation to computing science. Informatics in Education, 3(1), 105-126.

【讨论】:

  • 您是如何找到这篇论文的?我不可能自己找到它 - 它完全符合主题并提供了对问题的全面分析。谢谢!
  • 链接失效,请上传替代链接
  • 谷歌搜索标题和作者给了我这个链接research.tue.nl/en/publications/…,它现在正在工作
【解决方案2】:

简单算法

您可以在 O(n) 中找到期望获得或支付的金额。因此,您可以简单地创建两个列表,一个用于借方,另一个用于贷方,然后平衡两个列表的头部,直到它们为空。从你的第一个例子:

  • 初始状态:借:(A:25),贷:(B:15,C:10)
  • 第一笔交易,A:15 -> B:借:(A:10),贷:(C:10)
  • 第二笔交易,A:10 -> C:借:(),贷:()

事务定义了图表的边缘。对于涉及的 n 个人,最多会有 n-1 个交易 = 边。一开始,两个列表的总长度为 n。在每一步中,至少有一个列表(借方/贷方)会缩短一个,最后两个列表会同时消失。

问题在于,一般来说,此图表不必与原始图表相似,据我了解,这是一项要求。 (是吗?在某些情况下,最优解决方案包括添加新边。假设 A 欠 B 和 B 欠 C 相同数量的钱,A 应该直接支付 C,但这条边不在债务图中。)

更少的交易

如果目标只是构建一个等价图,您可以搜索债权人和借方名单(如上节所述)以找到完全匹配的情况,或者搜索贷方总和与一个 人(或相反)。寻找bin packing。对于其他情况,除了拆分流,您别无选择,但即使是上面的简单算法也会生成一个图,其边数最多比所涉及的人少一个。

编辑:感谢 j_random_hacker 指出如果有一组人的总债务与另一组人的信用相匹配,则可以使用少于 n-1 个边的解决方案:然后,该问题可以拆分为两个子问题,交易图的总成本为 n-2 条边。不幸的是,subset sum problem 是 NP 难的。

流量问题?

也许这也可以转换为min-cost flow problem。 如果您只想简化原始图表,请在其上构建一个流程,边缘容量是原始借方/贷方金额。借方用作流入节点(通过连接器节点为所有容量边缘等于其总债务的借方提供服务),债权人用作流出节点(具有类似的连接器节点)。

如果您想尽量减少交易数量,您会倾向于保留“大”交易并减少“小”交易。因此,每条边的成本可以建模为该边上流量的倒数。

【讨论】:

  • 我不认为这完全符合。他有周期和其他并发症。根据您的链接,它只是有向图。即,有生产/来源和消费者/汇。在这种情况下,我们有循环。但是,您可以通过找到一个循环中的最小值然后从所有节点中减去它来转换问题。
  • @artlessnoise:流算法可以处理循环,就像你建议的那样。
  • 嗯,我喜欢你的第一种方法;我不在乎最终图与初始图有多相似,只要它符合我上面的标准,添加新边就可以了(如您的示例中所示)。我会玩这个。我不确定最小成本分析会去哪里,因为我需要将网络转换为新网络,而不仅仅是分析单个网络。
  • 我不明白您在第一段中提出的内容如何在 O(n) 中实现;您需要查看所有边缘,所以这是 O(n+m),不是吗?
  • 我很确定它可以在 O(n) 中完成,其中 n 是边数。只需在边进行迭代时从节点流值中添加和减去(这是一个恒定时间操作)。
【解决方案3】:

其实我也遇到过和你一模一样的问题:)

我认为 krlmlr 的各种解决方案并不能完全解决问题。我会考虑如何准确地解决它(在最小边缘的意义上),但与此同时,一个实用的替代解决方案是发明一个新人,Steve

  1. 史蒂夫实际上不是一个人。 Steve 只是一个桶,上面贴着一张纸。
  2. 每个人都计算出他们欠的净额(或欠款,如果为负数),并在纸上写下他们的名字。
  3. 任何净头寸是他们欠钱的人都会在可能的情况下将净额的钱给史蒂夫,并划掉他们的名字。
  4. 每个净头寸是欠他们钱的人在看到史蒂夫有钱时从史蒂夫那里拿走这笔钱,并划掉他们的名字。

如果欠款的人无法一次全部还清,他们可以只给史蒂夫他们目前能够负担的金额,然后将这笔金额从欠款总额中扣除。同样,如果欠您的钱多于史蒂夫目前手头的钱,您可以拿走他目前拥有的所有钱,并从您名下的欠款总额中扣除。

如果每个人一开始都同意只向史蒂夫支付全部金额,那么每个净欠款人只进行一次存款,每个净欠款人只进行一次提款(尽管这可能需要对史蒂夫进行多次检查,看看他是否目前手头有足够的现金)。史蒂夫的好处是他总是在身边,而且从不忙于整理财务。不幸的是,他很容易上当受骗,所以 Alice、Bob 和 Charlie 需要已经相互信任才能不占他的便宜。

【讨论】:

  • 我的“简单”解决方案为所涉及的 n 人生成了一个最多具有 n-1 条边(=交易)的图。总是。恕我直言,只有在“较少交易”部分所示的条件下才有可能获得更好的解决方案。如果涉及史蒂夫,就像在您的解决方案中一样,事务图将包含 n 条边。
  • @krlmlr:是的,但我提出这只是一个简单实用的解决方案,而不是最佳解决方案。当存在债务总和为 x 的债务人子集和信用总和为 x 的债权人子集时,实际上可能有更好的解决方案(比 n-1 条边)——组中不必只有一个债务人,或者只是一个债权人。原因是这组(比如说,k)个债务人和债权人形成了一个子问题,它本身最多可以用 k-1 条边解决,留下 n-k 个人的问题最多可以用 n-k-1 条边解决,最多总共 n-2 个。
  • 有趣的想法。但接下来就是 NP-hard subset sum problem...
  • "Steve 只是一个水桶" 这实际上可以通过在你的厨房里放一个水桶(硬件)并完全跳过软件部分来完成。
  • 我正试图在与一群朋友旅行后合并债务,这是 IMO 最实用的现实世界解决方案。余额为负的每个人都支付旅行线索,而旅行线索支付的成员余额为正。行程线索就是这里的水桶。
猜你喜欢
  • 1970-01-01
  • 2014-04-24
  • 1970-01-01
  • 2016-07-13
  • 2012-05-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多