家谱算法

时间:2011-05-23 19:43:30

标签: algorithm graph tree family-tree

我正在努力为一个介绍级别的CS课程安排一个问题集,并提出一个问题,从表面上看,似乎很简单:

  

您将获得一份包含父母姓名,出生日期和死亡日期的人员名单。你有兴趣找出谁在他们一生中的某个时刻是父母,祖父母,曾祖父母等等。设计一个算法,用这个信息作为一个整数来标记每个人(0表示这个人从来没有过孩子,1表示该人是父母,2表示该人是祖父母等。)

为简单起见,您可以假设族图是DAG,其无向版本是树。

这里有趣的挑战是你不能只看树的形状来确定这些信息。例如,我有8位曾祖父母,但由于我出生时没有一个人活着,所以在他们的一生中,没有一个人是伟大的曾祖父母。

我能解决这个问题的最佳算法是在时间O(n 2 )中运行,其中n是人数。这个想法很简单 - 从每个人开始一个DFS,找到在该人死亡日期之前出生的家谱中最远的后代。但是,我很确定这不是问题的最佳解决方案。例如,如果图形只是两个父母及其n个孩子,那么问题可以在O(n)中平凡地解决。我希望的是一些算法要么胜过O(n 2 ),要么运行时参数化在图形的形状上,这使得它对于广泛的图形具有快速降级到O的速度( n 2 )在最坏的情况下。

9 个答案:

答案 0 :(得分:11)

更新:这不是我提出的最佳解决方案,但我已经离开了,因为有太多与之相关的评论。

你有一系列事件(出生/死亡),父母身份(没有后代,父母,祖父母等)和生活状态(活着,死亡)。

我会将数据存储在具有以下字段的结构中:

mother
father
generations
is_alive
may_have_living_ancestor

按日期对事件进行排序,然后为每个事件选择以下两个逻辑课程之一:

Birth:
    Create new person with a mother, father, 0 generations, who is alive and may
        have a living ancestor.
    For each parent:
        If generations increased, then recursively increase generations for
            all living ancestors whose generations increased.  While doing that,
            set the may_have_living_ancestor flag to false for anyone for whom it is
            discovered that they have no living ancestors.  (You only iterate into
            a person's ancestors if you increased their generations, and if they
            still could have living ancestors.)

Death:
    Emit the person's name and generations.
    Set their is_alive flag to false.

最糟糕的情况是O(n*n)如果每个人都有很多活着的祖先。但是,一般来说,您的排序预处理步骤为O(n log(n)),然后您为O(n * avg no of living ancestors),这意味着大多数人群的总时间往往为O(n log(n))。 (由于@Alexey Kukanov的修正,我没有正确计算排序前提。)

答案 1 :(得分:7)

今天早上我想到了这一点,然后发现@Alexey Kukanov有类似的想法。但我的更加充实并且有更多的优化,所以无论如何我都会发布它。

此算法为O(n * (1 + generations)),适用于任何数据集。对于实际数据,这是O(n)

  1. 遍历所有记录并生成表示人员的对象,其中包括出生日期,父母的链接,子项链接以及其他几个未初始化的字段。 (自我和祖先之间的最后一次死亡时间,以及他们有0,1,2,......幸存下来的一系列日期。)
  2. 通过所有人并以递归方式查找并存储上次死亡的时间。如果再次呼叫此人,请返回已记录的记录。对于每个人,您可以遇到该人(需要计算它),并且可以在您第一次计算时为每个父母再生成2个呼叫。这使得O(n)总共可以初始化此数据。
  3. 通过所有人并递归地生成他们第一次添加一代的记录。这些记录只需要在该人或其最后一位祖先去世时达到最大值。你有0代时计算O(1)。然后,对于对孩子的每次递归调用,您需要执行O(generations)工作以将该孩子的数据合并到您的家中。每个人在数据结构中遇到他们时都会被呼叫,并且可以从每个父母呼叫一次O(n)次呼叫和总费用O(n * (generations + 1))
  4. 通过所有人,弄清楚他们死后有多少代人活着。如果使用线性扫描实现,则再次O(n * (generations + 1))
  5. 所有这些操作的总和为O(n * (generations + 1))

    对于实际数据集,这将是O(n),具有相当小的常数。

答案 2 :(得分:5)

我的建议:

  • 除问题陈述中描述的值外,每个个人记录还有两个字段:子计数器和动态增长的向量(在C ++ / STL意义上),它将在每个人的后代中保留最早的生日。 / LI>
  • 使用哈希表来存储数据,人名是关键。构建它的时间是线性的(假设一个好的散列函数,映射已经为插入和查找分摊了恒定的时间)。
  • 为每个人,检测并保存孩子的数量。它也是在线性时间内完成的:对于每个个人记录,找到其父母的记录并增加他们的计数器。此步骤可与前一步骤结合使用:如果未找到父级记录,则会创建并添加该记录,而在输入中找到详细信息(日期等)时将添加。
  • 遍历地图,并将没有子项的所有个人记录的引用放入队列中。仍然是O(N)
  • 从队列中取出的每个元素:
    • 为父母双方将此人的生日添加到descendant_birthday[0](如果需要,可以增长该向量)。如果已设置此字段,则仅在新日期较早时更改此字段。
    • 对于当前记录矢量中可用的所有descendant_birthday[i]日期,请按照上述相同规则更新父母记录中的descendant_birthday[i+1]
    • 减少父母的子女柜台;如果它达到0,则将相应的父记录添加到队列中。
    • 此步骤的成本为O(C*N),其中C是给定输入的“族深度”的最大值(即最长descendant_birthday向量的大小)。对于实际数据,它可以被一些合理的常数限制而没有正确性损失(正如其他人已经指出的那样),因此不依赖于N.
  • 再次遍历地图,并使用i仍然早于死亡日期的最大descendant_birthday[i]“标记每个人”;还O(C*N)

因此,对于真实数据,可以在线性时间内找到问题的解决方案。虽然对于@ btilly评论中提出的人为设计数据,C可能很大,甚至在退化情况下也可能是N的顺序。它可以通过在矢量大小上设置上限或通过@btilly解决方案的第2步扩展算法来解决。

如果输入数据中的父子关系是通过名称提供的(如问题陈述中所述),则哈希表是解决方案的关键部分。如果没有哈希,则需要O(N log N)来构建关系图。大多数其他建议的解决方案似乎都假设关系图已经存在。

答案 3 :(得分:3)

创建按birth_date排序的人员列表。创建另一个人员列表,按death_date排序。您可以按时间逻辑旅行,从这些列表中弹出人员,以便在事件发生时获取事件列表。

对于每个人,定义一个is_alive字段。这对每个人来说都是假的。随着人们的出生和死亡,相应地更新此记录。

为每个人定义另一个字段,称为has_a_living_ancestor,首先为每个人初始化为FALSE。出生时,x.has_a_living_ancestor将设置为x.mother.is_alive || x.mother.has_a_living_ancestor || x.father.is_alive || x.father.has_a_living_ancestor。因此,对于大多数人(但不是每个人),这将在出生时设置为TRUE。

挑战在于确定has_a_living_ancestor可以设置为FALSE的情况。每次一个人出生时,我们都会通过祖先进行DFS,但只有那些ancestor.has_a_living_ancestor || ancestor.is_alive为真的祖先。

在那个DFS期间,如果我们找到一个没有活着的祖先的祖先,现在已经死了,那么我们可以将has_a_living_ancestor设置为FALSE。我认为,这确实意味着有时候has_a_living_ancestor会过时,但希望很快就会被抓住。

答案 4 :(得分:3)

以下是一个O(n log n)算法,适用于每个子节点最多只有一个父节点的图形(编辑:此算法不扩展到具有O(n log n)性能的双父案例) 。值得注意的是,我相信可以通过额外的工作将性能提高到O(n log(最大级别标签))。

一个父案例:

对于每个节点x,以相反的拓扑顺序,创建一个二进制搜索树T_x,它在出生日期和从x中删除的世代数中严格增加。 (T_x包含以x为根的祖先图的子图中的第一个出生的孩子c1,以及该子图中下一个最早出生的孩子c2,使得c2的“大祖父母级别”严格地大于c1的级别,以及这个子图中的下一个最早出生的子c3使得c3的级别严格大于c2的级别等。)为了创建T_x,我们合并先前构造的树T_w,其中w是x的子项(它们之前是构造的,因为我们正在以反向拓扑顺序迭代。)

如果我们小心我们如何执行合并,我们可以证明这种合并的总成本是整个祖先图的O(n log n)。关键的想法是要注意,在每次合并之后,每个级别的最多一个节点在合并树中存活。我们将每个树T_w与h(w)log n的电位相关联,其中h(w)等于从w到叶子的最长路径的长度。

当我们合并子树T_w以创建T_x时,我们“销毁”所有树T_w,释放它们存储的所有潜力以用于构建树T_x;然后我们用(log n)(h(x))势创建一个新的树T_x。因此,我们的目标是花费最多O((log n)(sum_w(h(w)) - h(x)+常数))时间从树T_w创建T_x,以便合并的摊销成本将是只有O(log n)。这可以通过选择树T_w使得h(w)最大作为T_x的起始点然后修改T_w以创建T_x来实现。在对T_x做出这样的选择之后,我们将每个其他树一个接一个地合并到T_x中,其算法类似于合并两个二叉搜索树的标准算法。

基本上,合并是通过迭代T_w中的每个节点y,按出生日期搜索y的前任z,然后如果从x中移除更多级别,则将y插入T_x;然后,如果z被插入到T_x中,我们在T_x中搜索严格大于z的级别的最低级别的节点,并拼接出中间节点以保持T_x严格按出生日期和级别排序的不变量。这花费了T_w中每个节点的O(log n),并且T_w中最多有O(h(w))个节点,因此合并所有树的总成本是O((log n)(sum_w(h(w) ))),对所有孩子进行总结,除了孩子w'使得h(w')是最大的。

我们将与T_x的每个元素相关联的级别存储在树中每个节点的辅助字段中。我们需要这个值,这样我们就可以在构造T_x后找出x的实际级别。 (作为技术细节,我们实际上将每个节点的级别与其父级的级别存储在T_x中,以便我们可以快速递增树中所有节点的值。这是标准的BST技巧。)

就是这样。我们只是注意到初始潜力为0且最终潜力为正,因此摊销边界的总和是整个树中所有合并的总成本的上限。一旦我们通过二进制搜索T_x中的最新元素来创建BST T_x,我们就会找到每个节点的标签x.x_x是在x死于成本O(log n)之前出生的。

要改进对O(n log(最大级别标签))的绑定,您可以懒惰地合并树,只需根据需要合并树的前几个元素,以便为当前节点提供解决方案。如果您使用利用引用局部性的BST,例如展开树,那么您可以实现上述限制。

希望上述算法和分析至少足够明确。如果您需要任何澄清,请发表评论。

答案 5 :(得分:2)

我有一种预感,即为每个人获取一个映射(生成 - >该生成的第一个后代的生成日期)会有所帮助。

由于日期必须严格增加,我们可以使用二元搜索(或整齐的数据结构)在O(log n)时间内找到最远的生活后代。

问题是合并这些列表(至少天真地)是O(代数),所以在最坏的情况下这可能是O(n ^ 2)(考虑A和B是C和D的父,谁是E和F的父母......)。

我仍然需要弄清楚最佳案例是如何运作的,并尝试更好地识别最坏的情况(并查看是否有针对他们的解决方法)

答案 6 :(得分:2)

我们最近在我们的一个项目中实现了关系模块,其中我们拥有数据库中的所有内容,是的,我认为算法最好2nO(m)(m是最大分支因子)。我将操作乘以两次到N,因为在第一轮我们创建关系图,在第二轮我们访问每个人。我们在每两个节点之间存储了双向关系。在导航时,我们只使用一个方向来旅行。但是我们有两组操作,一组只遍历子项,另一组只遍历父项。

Person{
  String Name;

  // all relations where
  // this is FromPerson
  Relation[] FromRelations; 

  // all relations where
  // this is ToPerson
  Relation[] ToRelations;

  DateTime birthDate;
  DateTime? deathDate;
}

Relation
{
  Person FromPerson;
  Person ToPerson;
  RelationType Type;
}

enum RelationType
{
  Father,
  Son,
  Daughter,
  Mother
}

这种看起来像双向图。但在这种情况下,首先构建所有Person的列表,然后您可以构建列表关系并在每个节点之间设置FromRelations和ToRelations。那么你所要做的就是,对于每个人,你只需要只导航类型(儿子,女​​儿)的ToRelations。由于你有约会,你可以计算一切。

我没有时间检查代码的正确性,但这会让您了解如何执行此操作。

void LabelPerson(Person p){
   int n = GetLevelOfChildren(p, p.birthDate, p.deathDate);
   // label based on n...
}

int GetLevelOfChildren(Person p, DateTime bd, DateTime? ed){
   List<int> depths = new List<int>();
   foreach(Relation r in p.ToRelations.Where(
             x=>x.Type == Son || x.Type == Daughter))
   {
      Person child = r.ToPerson;
      if(ed!=null && child.birthDate <= ed.Value){
         depths.Add( 1 + GetLevelOfChildren( child, bd, ed));
      }else
      {
         depths.Add( 1 + GetLevelOfChildren( child, bd, ed));
      }
   }
   if(depths.Count==0)
      return 0;
   return depths.Max();
}

答案 7 :(得分:0)

这是我的刺:

class Person
{
    Person [] Parents;
    string Name;
    DateTime DOB;
    DateTime DOD;
    int Generations = 0;

    void Increase(Datetime dob, int generations)
    {
        // current person is alive when caller was born
        if (dob < DOD)
            Generations = Math.Max(Generations, generations)
        foreach (Person p in Parents)
            p.Increase(dob, generations + 1);
    }

    void Calculate()
    {
        foreach (Person p in Parents)
            p.Increase(DOB, 1);
    }
}

// run for everyone
Person [] people = InitializeList(); // create objects from information
foreach (Person p in people)
    p.Calculate();

答案 8 :(得分:-2)

  • 有一个相对简单的O(n log n)算法,可以在合适的top tree的帮助下按时间顺序扫描事件。

  • 你真的不应该分配你自己无法解决的作业。