我是一些家谱软件(用C++和Qt编写)的开发人员。直到我的一位客户给我邮寄了一份错误报告,我才发现问题。问题是,客户有两个孩子和自己的女儿,因此,由于错误,他无法使用我的软件。

这些错误是我对正在处理的族图的各种断言和不变量的结果(例如,在走完一个循环后,程序声明X不能既是Y的父亲又是Y的祖父)。

如何在不删除所有数据断言的情况下解决这些错误?


撇开潜在的法律影响不谈,你显然需要将家谱上的“节点”视为前任,而不是假设节点可以是唯一的人。

让树节点包含一个人以及继任者,然后可以在树的更深处创建另一个节点,该节点包含同一个人和不同的继任者。


我想,你有一些价值,可以唯一地识别一个人,你可以根据这个人进行检查。

这是一个棘手的问题。假设您希望将结构保持为树,我建议如下:

假设这样:A和自己的女儿有孩子。

A在节目中以A和B的身份加入节目。一旦扮演了父亲的角色,我们就称之为男朋友。

添加一个is_same_for_out()函数,该函数告诉程序的输出生成部分,所有内部指向B的链接在数据表示时都应该指向a。

这将为用户带来一些额外的工作,但我想IT将相对容易实现和维护。

在此基础上,您可以进行代码同步A和B,以避免不一致。

这种解决方案肯定不是完美的,但却是第一种方法。


我讨厌评论这种糟糕的情况,但最简单的方法是在你的图中创建一个虚拟顶点,作为回到乱伦父亲的代理。


放松你的断言。

而不是改变规则,这可能对99.9%的客户在输入数据时发现错误非常有帮助。

相反,将其从错误“无法添加关系”更改为警告“无论如何添加”。


这就是为什么像“Go”这样的语言没有断言的原因之一。它们被用来处理那些你可能没有想到的案例,所有这些都太频繁了。你只应该断言不可能,而不仅仅是不可能。做后者会给断言带来坏名声。每次你输入assert(,走开十分钟,认真思考一下。

在你特别令人不安的案例中,在罕见但可能的情况下,这样的断言是假的,这是可以想象的,也是令人震惊的。因此,在你的应用程序中处理它,如果只是说“这个软件不是为处理你所呈现的场景而设计的”。

断言你的曾、曾、曾祖父是你的父亲是不可能的,这是合理的做法。

如果我是为一家被雇佣来测试你的软件的测试公司工作的话,我当然会提出这种情况。为什么?每一个年轻而聪明的“用户”都会做同样的事情,并对由此产生的“错误报告”津津乐道。


看来你(和/或你的公司)对家谱应该是什么有着根本的误解。

让我澄清一下,我也在一家公司工作,该公司的产品组合中有一个家谱(作为其产品之一),我们一直在解决类似的问题。

在我们的案例中,我假设你的案例也是如此,问题来自于GEDCOM格式,它对家庭应该是什么非常有见解。然而,这种格式包含了一些关于家庭树真实外观的严重误解。

GEDCOM有很多问题,例如同性关系不相容、乱伦等。现实生活中发生的事情比你想象的要多(尤其是当追溯到1700-1800年时)。

我们已经将我们的家谱模型化为现实世界中发生的事情:事件(例如,出生、婚礼、订婚、结婚、死亡、收养等)。我们没有对这些事情施加任何限制,但逻辑上不可能的事情除外(例如,一个人不能成为自己的父母,关系需要两个人,等等)

缺乏验证为我们提供了一个更“真实”、更简单、更灵活的解决方案。

对于这个具体的案例,我建议删除这些断言,因为它们并不普遍适用。

为了显示问题(可能会出现),我建议根据需要多次绘制同一节点,通过选择其中一个副本点亮所有副本来暗示重复。


你应该关注真正为你的软件创造价值的东西。为一个消费者提供服务所花费的时间是否值得许可证的价格?可能不会。

我建议您向这位客户道歉,告诉他他的情况超出了您的软件范围,并向他退款。


这是家谱的问题:它们不是树。它们是有向无环图或DAG。如果我正确理解人类生殖生物学的原理,就不会有任何周期。

据我所知,即使是基督徒也接受表兄弟之间的婚姻(以及孩子),这将把家谱变成家庭DAG。

这个故事的寓意是:选择正确的数据结构。


你的家谱应该使用定向关系。这样你就不会有循环了。


对一个愚蠢问题的另一个假装严肃的回答:

真正的答案是,使用适当的数据结构。人类谱系不能用没有循环的纯树来完全表达。你应该使用某种图表。此外,在进一步讨论之前,请与人类学家交谈,因为在其他许多地方,试图建立家谱模型可能会犯类似的错误,即使是在最简单的“西方父权一夫一妻制婚姻”的情况下

即使我们想忽略这里所讨论的当地禁忌关系,也有很多完全合法和完全意想不到的方法将循环引入家谱。

例如:http://en.wikipedia.org/wiki/Cousin_marriage

基本上,近亲结婚不仅是普遍的和意料之中的,也是人类从数千个小家庭群体发展到全球60亿人口的原因。它不能以任何其他方式工作。

在家谱、家族和血统方面,真的很少有普遍性。几乎任何关于姨妈可以是谁,谁可以嫁给谁,或者孩子如何合法继承的规范的严格假设,都可能被世界或历史上的某个地方的某些例外所打乱。


你应该把Atreides家族(现代的Dune,或古代的Oedipus Rex)作为一个测试案例。通过使用经过净化的数据作为测试用例,您不会发现错误。


所以,我在家谱软件上做了一些工作。我认为你要解决的问题是你需要能够在树上行走而不陷入无限循环——换句话说,树需要是非循环的。

然而,你似乎在断言一个人和他们的祖先之间只有一条路。这将保证没有周期,但过于严格。从生物学上讲,后代是一个有向无环图(DAG)。你的情况当然是一个退化的情况,但这种情况在更大的树上总是发生。

例如,如果你看看你在第n代的祖先,如果没有重叠,那么你在公元1000年的祖先会比活着的人多。所以,必须有重叠。

然而,您也会得到无效的循环,只是坏数据。如果您正在遍历树,那么必须处理循环。您可以在每个单独的算法中或在加载时执行此操作。我是负重做的。

在树中找到真正的循环可以通过几种方式完成。错误的方法是标记给定个体的每个祖先,当遍历时,如果你要走到的下一个人已经被标记,那么就切断链接。这将切断潜在的准确关系。正确的做法是从每个个体开始,并用通向该个体的路径标记每个祖先。如果新路径包含当前路径作为子路径,那么它是一个循环,应该中断。您可以将路径存储为vector<bool>(MFMF、MFFFMF等),这使得比较和存储速度非常快。

还有一些其他方法可以检测循环,例如发送两个迭代器,看看它们是否与子集测试冲突,但我最终使用了本地存储方法。

还需要注意的是,您不需要实际切断链接,只需将其从正常链接更改为“弱”链接,而某些算法不会遵循该链接。在选择将哪个链接标记为弱链接时,您也需要小心;有时,你可以通过查看出生日期信息来找出应该打破这个周期的地方,但通常你什么都搞不清楚,因为缺少了太多数据。


一些答案显示了保持断言/不变量的方法,但这似乎是滥用断言/不变量。断言是为了确保应该是真的东西是真的,不变量是为了确保不应该改变的东西不会改变。

你在这里断言的是乱伦关系是不存在的。显然它们确实存在,因此您的断言是无效的。您可以解决这个断言,但真正的错误在于断言本身。应删除断言。


最重要的是避免产生问题,所以我认为你应该使用直接关系来避免循环。

正如@markmywords所说,#包括“fritzl.h”。

最后,我不得不说重新检查您的数据结构。也许那边出了问题(也许双向链接列表解决了您的问题)。


不要删除所有断言,您仍然应该检查诸如某人是自己的父母或其他不可能的情况,并提出错误。如果不太可能的话,可能会发出警告,这样用户仍然可以检测到常见的输入错误,但如果一切都正确的话,它会起作用。

我会将数据存储在一个向量中,每个人都有一个永久整数,并将父母和孩子存储在个人对象中,其中所述int是向量的索引。这在几代人之间会很快(但在名字搜索等方面会很慢)。对象将按创建时的顺序排列。


复制父项(或使用符号链接/引用)。

例如,如果您使用的是分层数据库:

$ #each person node has two nodes representing its parents.
$ mkdir Family
$ mkdir Family/Son
$ mkdir Family/Son/Daughter
$ mkdir Family/Son/Father
$ mkdir Family/Son/Daughter/Father
$ ln -s Family/Son/Daughter/Father Family/Son/Father
$ mkdir Family/Son/Daughter/Wife
$ tree Family
Family
└── Son
    ├── Daughter
    │   ├── Father
    │   └── Wife
    └── Father -> Family/Son/Daughter/Father

4 directories, 1 file

家谱数据是循环的,不适合于非循环图,所以如果你有针对循环的断言,你应该删除它们。

在不创建自定义视图的情况下在视图中处理此问题的方法是将循环父对象视为“幽灵”父对象。换句话说,当一个人同时是同一个人的父亲和祖父时,祖父节点正常显示,但父亲节点被渲染为“幽灵”节点,该节点具有简单的标签(如“看见祖父”)并指向祖父。

为了进行计算,您可能需要改进处理循环图的逻辑,以便在存在循环的情况下不会多次访问节点。


断言不能生存于现实

通常,断言在与真实世界数据的接触中无法生存。这是软件工程过程中的一部分,决定您要处理哪些数据,哪些数据超出了范围。

循环族图

关于家族“树”(事实上,它是完整的图表,包括循环),有一个很好的轶事:

我娶了一个寡妇,她有一个成年的女儿。我的父亲经常来看我们,他爱上了我的继女并娶了她。结果,我的父亲成了我的儿子,我的女儿成了我母亲。一段时间后,我给了我妻子一个儿子,他是我父亲和我叔叔的兄弟。我父亲的妻子(也是我的女儿和母亲)有一个儿子。结果,我得到了同一个人的一个哥哥和一个孙子。我的妻子现在是我的祖母,因为她是我母亲的母亲。所以我是我妻子的丈夫,同时也是我妻子的继孙。换句话说,我是我自己的爷爷。

当你考虑到代孕或“模糊的父亲身份”时,事情变得更加奇怪。

如何应对

将周期定义为超出范围

你可以决定你的软件不应该处理这种罕见的情况。如果出现这种情况,用户应使用不同的产品。这使得处理更常见的情况更加健壮,因为您可以保留更多断言和更简单的数据模型。

在这种情况下,为您的软件添加一些良好的导入和导出功能,以便用户可以在必要时轻松迁移到其他产品。

允许手动关系

您可以允许用户添加手动关系。这些关系不是“一流公民”,即软件按原样对待它们,不检查它们,也不在主数据模型中处理它们。

然后,用户可以手动处理罕见的情况。您的数据模型仍然非常简单,您的断言将继续存在。

小心手动关系。有一种诱惑是让它们完全可配置,从而创建一个完全可配置的数据模型。这是行不通的:你的软件无法扩展,你会发现奇怪的bug,最终用户界面将变得不可用。这种反模式被称为“软编码”,《每日WTF》中有很多这样的例子。

使数据模型更灵活,跳过断言,测试不变量

最后的办法是使数据模型更灵活。您必须跳过几乎所有的断言,并将数据模型建立在一个完整的图表上。正如上面的例子所示,你很容易成为自己的祖父,因此你甚至可以拥有自行车。

在这种情况下,您应该广泛测试您的软件。您必须跳过几乎所有断言,因此很有可能会出现其他错误。

使用测试数据生成器检查异常测试用例。有Haskell、Erlang或C的快速检查库。对于Java/Scala,有ScalaCheck和Nyaya。一个测试思路是模拟一个随机种群,让它随机杂交,然后让你的软件先导入然后导出结果。期望是,输出中的所有连接也在输入中,反之亦然。

属性保持不变的情况称为不变量。在这种情况下,不变量是模拟人口中个体之间的一组“浪漫关系”。尝试找到尽可能多的不变量,并用随机生成的数据测试它们。不变量可以是函数,例如:

即使你增加了更多的“浪漫关系”,叔叔仍然是叔叔每个孩子都有父母有两代人的人口至少有一个祖父母

也可以是技术性的:

您的软件不会在多达100亿成员的图形上崩溃(无论有多少互连)软件的缩放比例为O(节点数)和O(边数^2)您的软件可以保存和重新加载多达100亿成员的每个家庭图

通过运行模拟测试,您将发现许多奇怪的角落案例。修复它们需要很多时间。此外,您将失去很多优化,您的软件将运行得慢得多。你必须决定它是否值得,这是否在你的软件范围内。