关于C++:家庭树软件中的循环

Cycles in family tree software

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

这些错误是我关于正在处理的族图的各种断言和不变量的结果(例如,在遍历一个循环之后,程序声明x不能既是y的父也不是y的祖父)。

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


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

让我澄清一下,我也在一家公司工作,该公司(作为其产品之一)的投资组合中有一个家族树,我们一直在与类似的问题作斗争。

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

GEDCOM存在着许多问题,如同性关系不相容、乱伦等。这在现实生活中发生的次数比你想象的要多(尤其是回到1700-1800年的时候)。

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

缺乏验证给了我们一个更"真实的世界",更简单和更灵活的解决方案。

对于这个特定的案例,我建议删除断言,因为它们不具有普遍性。

对于显示问题(将出现的问题),我建议根据需要多次绘制相同的节点,在选择其中一个节点时,通过点亮所有副本来提示复制。


放松你的断言。

而不是通过更改规则,这对99.9%的客户在输入数据时遇到错误非常有帮助。

相反,将其从错误"can't add relationship"更改为警告"add anyway"。


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

据我所知,甚至基督徒也接受表兄弟之间的婚姻(因此也接受子女),这将把家族树变成家族之剑。

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


我猜你有一个值,它唯一地标识了一个人,你可以根据这个人进行检查。

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

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

A将自己添加到程序中,称为AB。一旦成为父亲,我们就称之为男朋友。

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

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

在此基础上,您可以处理代码同步AB,以避免不一致。

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


你应该关注什么才是你的软件真正的价值所在。花在为一个消费者工作上的时间是否值许可证的价格?可能不会。

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


你应该建立阿特瑞德家族(现代,沙丘,或古代,俄狄浦斯雷克斯)作为测试案例。通过将清理过的数据用作测试用例,您不会发现错误。


这就是为什么像"go"这样的语言没有断言的原因之一。它们被用来处理你可能没有想到的案件,太频繁了。你只应该断言不可能,而不仅仅是不可能。后一种做法会让断言名声扫地。每次你输入assert(时,都要走开十分钟,好好想想。

在你特别令人不安的情况下,这种断言在罕见但可能的情况下是伪造的,这是可以想象的,也是令人震惊的。因此,在你的应用程序中处理它,如果只是说"这个软件不是为处理你提出的场景而设计的"。

断言你的曾祖父、曾祖父是你的父亲是不可能的,这是一件合理的事情。

如果我是为一家被雇佣来测试你的软件的测试公司工作的话,我当然会提出这种情况。为什么?每一个幼稚但聪明的"用户"都会做同样的事情,并喜欢由此产生的"错误报告"。


我不喜欢评论这样一个混乱的情况,但最简单的方法不是重新获得你所有的不变量是在你的图中创建一个幻影顶点,作为一个代理返回乱伦的父亲。


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

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

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

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

在树中找到真正的循环可以用几种方法完成。错误的方法是标记来自给定个体的每个祖先,当遍历时,如果要进入下一个个体的人已经被标记,那么就切断链接。这将切断潜在的精确关系。正确的方法是从每个个体开始,并用指向该个体的路径标记每个祖先。如果新路径包含当前路径作为子路径,那么它是一个循环,应该被破坏。您可以将路径存储为vector(mfmf、mffmf等),这使得比较和存储非常快。

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

另外请注意,您不需要实际切断链接,只需将其从普通链接更改为"弱"链接,这不是您的一些算法所遵循的。在选择哪一个链接将标记为弱链接时,您还需要小心;有时,您可以通过查看生日信息来确定应该在何处破坏循环,但通常情况下,由于丢失了这么多数据,您无法找到任何内容。


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

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

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

例如:http://en.wikipedia.org/wiki/coun-marriage

从根本上说,表亲婚姻不仅是常见的,也是人们期望的,它是人类从数千个小家庭群体走向全球60亿人口的原因。它不能以任何其他方式工作。

当涉及到系谱、家庭和血统时,确实很少有普适性。几乎任何关于规范的严格假设,暗示谁是姑姑,谁可以嫁给谁,或者为了继承遗产而如何使孩子合法化,都会因世界或历史上的某个地方的某些例外而感到不安。


撇开潜在的法律含义不谈,您显然需要将家族树上的"节点"视为前辈,而不是假定节点可以是唯一的人。

让树节点包括一个人和继承人-然后您可以让另一个节点更深入树,其中包含具有不同继承人的同一个人。


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

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


你的家谱应该使用有向关系。这样你就不用骑自行车了。


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

在视图中处理此问题而不创建自定义视图的方法是将循环父级视为"重影"父级。换言之,当一个人同时是同一个人的父亲和祖父时,则通常显示祖父节点,但父亲节点渲染为具有简单标签(如"see grandor")并指向祖父的"幽灵"节点。

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


断言不存在于现实中

通常,断言在与现实数据的接触中不存在。这是软件工程过程中需要决定的一部分,需要处理哪些数据,哪些数据超出了范围。

循环族图

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

I married a widow who had a grown daughter. My father, who often visited us, fell in love with my step-daughter and married her. As a result, my father became my son, and my daughter became my mother. Some time later, I gave my wife a son, who was the brother of my father, and my uncle. My father's wife (who is also my daughter and my mother) got a son. As a result, I got a brother and a grandson in the same person. My wife is now my grandmother, because she is my mother's mother. So I am the husband of my wife, and at the same time the step-grandson of my wife. In other words, I'm my own grandpa.

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

如何处理这个问题将周期定义为超出范围

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

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

允许手动关系

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

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

小心处理手动关系。有一种诱惑,使它们完全可配置,从而创建一个完全可配置的数据模型。这不起作用:你的软件不会扩展,你会得到奇怪的错误,最终用户界面将变得不可用。这种反模式被称为"软编码","每日WTF"中充满了这样的例子。

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

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

在这种情况下,您应该广泛地测试您的软件。您必须跳过几乎所有的断言,因此有很好的机会产生额外的错误。

使用测试数据生成器检查异常的测试用例。对于Haskell,Erlang或C,有快速检查库。对于Java/Scala,有ScalaCheck和Nyay.一个测试的想法是模拟一个随机的群体,让它随机地混合,然后让您的软件首先导入,然后导出结果。期望是,输出中的所有连接也在输入和副韵文中。

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

  • 叔叔还是叔叔,即使你加上更多的"浪漫关系"
  • 每个孩子都有父母
  • 有两代人的人口至少有一个祖父母。

或者它们可以是技术性的:

  • 您的软件不会在多达100亿成员的图形上崩溃(无论有多少互联)
  • 您的软件按o(节点数)和o(边数^2)缩放
  • 您的软件可以保存和重新加载每个族的图形,最多100亿个成员

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


最重要的是对avoid creating a problem,所以我认为你应该使用直接关系来避免循环。

正如@markmywords所说,包括"fritzl.h"。

最后我要说的是recheck your data structure。也许那边出了问题(也许双向链表解决了你的问题)。


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

我会将数据存储在一个向量中,每个人都有一个永久整数,并将父对象和子对象存储在person对象中,其中所述int是向量的索引。这在几代人之间是相当快的(但是对于像名字搜索这样的事情来说是很慢的)。对象将按照创建的顺序排列。


复制父级(或使用symlink/reference)。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ #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