关于图:族树算法

Family Tree Algorithm

我正在为入门级的CS课程准备一个问题集,并提出了一个表面上看起来非常简单的问题:

You are given a list of people with the names of their parents, their birth dates, and their death dates. You are interested in finding out who, at some point in their lifetime, was a parent, a grandparent, a great-grandparent, etc. Devise an algorithm to label each person with this information as an integer (0 means the person never had a child, 1 means that the person was a parent, 2 means that the person was a grandparent, etc.)

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

这里有趣的挑战是,你不能仅仅通过观察树的形状来确定这些信息。例如,我有8个曾祖父母,但由于我出生时他们都没有活着,所以在他们的一生中,他们都不是曾祖父母。

对于这个问题,我能想到的最好的算法运行在时间o(n2)中,其中n是人数。这个想法很简单——从每个人身上开始一个DFS,找到在这个人死亡之前出生的家族树上最远的后代。不过,我很肯定这不是解决这个问题的最佳方案。例如,如果图只是两个父母和他们的n个孩子,那么这个问题可以在o(n)中解决。我希望得到的是一种比O(n2)更好的算法,或者它的运行时间参数化在图的形状上,这样在最坏的情况下,宽图可以很快地降级为O(n2)。


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

您有一组事件(出生/死亡)、父母状态(没有后代、父母、祖父母等)和生命状态(活着、死去)。

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

1
2
3
4
5
mother
father
generations
is_alive
may_have_living_ancestor

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

如果每个人都有很多活着的祖先,那么最坏的情况就是江户十一世(0)。然而,一般来说,你有排序预处理步骤,即O(n log(n)),然后你是O(n * avg no of living ancestors),这意味着在大多数人群中总时间趋向于O(n log(n))。(由于@alexey kukanov的更正,我没有正确计算排序的prestep。)


我想到今天早上,然后发现@alexey kukanov也有类似的想法。但我的更丰满,有更多的优化,所以我无论如何都会贴出来。

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

  • 运行所有记录并生成表示人员的对象,这些对象包括出生日期、指向父级的链接、指向子级的链接以及多个未初始化的字段。(自我和祖先最后一次死亡的时间,以及他们拥有0,1,2,…幸存的几代。)
  • 遍历所有人,递归地查找和存储最后一次死亡的时间。如果你再打电话给那个人,请把记录退回。对于每个人,您可以遇到该人(需要计算它),并且可以在第一次计算它时再给每个家长生成两个调用。这就提供了初始化此数据所需的全部O(n)工作。
  • 遍历所有人,递归地生成他们第一次添加一代的记录。这些记录只需要达到该人或其最后一个祖先死亡的最大时间。当你有0代人的时候,计算的是O(1)。然后,对于每个递归调用的子级,您需要执行O(generations)工作,将该子级的数据合并到您的数据中。当您在数据结构中遇到每个人时都会被调用,并且对于O(n)调用和total expense O(n * (generations + 1))调用,可以从每个父级调用一次。
  • 仔细检查所有的人,找出他们死后有多少代人还活着。如果用线性扫描来实现,这也是O(n * (generations + 1))
  • 所有这些操作的总和是O(n * (generations + 1))

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


    我的建议:

    • 此外,在问题陈述中描述的值中,每个个人记录将有两个字段:子计数器和动态增长向量(以C++ +STL感测),这将在每一代人的后代中保持最早的生日。
    • 使用哈希表存储数据,人名是键。构建它的时间是线性的(假设一个好的散列函数,映射已经为插入和查找分配了恒定的时间)。
    • 对于每个人,检测并保存孩子的数量。它也在线性时间内完成:对于每个个人记录,查找其父母的记录并增加其计数器。此步骤可以与上一步结合:如果找不到父级的记录,则创建并添加该记录,而在输入中找到详细信息(日期等)时将添加该记录。
    • 遍历地图,并将对所有没有子项的个人记录的引用放入队列中。仍然是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来解决。

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


    以下是一个O(n log n)算法,适用于每个子级最多有一个父级的图形(编辑:此算法不扩展到具有O(n log n)性能的两个父级情况)。值得注意的是,我相信通过额外的工作,性能可以提高到O(n log(max level label))。

    单亲病例:

    对于每个节点x,按照相反的拓扑顺序,创建一个二进制搜索树t_x,它严格地增加了出生日期和从x中删除的代的数量。(t_x在根于x的祖先图的子图中包含第一个出生的子图c1,以及下一个最早出生的ch。子图中的ild c2,使c2的"曾祖父母级别"严格大于c1的级别,以及子图中的下一个最早出生的孩子c3,使c3的级别严格大于c2的级别,等等),为了创建t_x,我们合并了以前构造的树t_w,其中w是X的一个子级(它们以前是构造的,因为我们是按逆拓扑顺序迭代的)。

    如果我们对如何执行合并非常小心,我们可以证明这样的合并的总成本是整个祖先图的O(n log n)。关键思想是要注意,在每次合并之后,在合并的树中,每个级别的至多有一个节点存在。我们将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)+constant))时间从树t_w创建t_x,以便合并的摊余成本仅为o(logn)。这可以通过选择树t_w使h(w)最大作为t_x的起始点,然后修改t_w以创建t_x来实现。在对t_x进行这样的选择之后,我们将其他树逐个合并到t_x中,使用类似于Mergi标准算法的算法。两个二进制搜索树。

    本质上,合并是通过在t_w中对每个节点y进行迭代,在出生日期前搜索y的前一个z,然后如果从x中删除的级别多于z,则将y插入t_x;然后,如果将z插入t_x,则搜索最低级的t_x中严格的节点。y大于z的水平,并拼接中间节点以保持t_x严格按照出生日期和水平排序的不变量。对于t_w中的每个节点,它的开销为o(log n),t_w中最多有o(h(w))个节点,因此合并所有树的总开销为o(logn)(sum_w(h(w)),将除子w'之外的所有子w相加,使h(w)最大。

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

    就这样。我们简单地注意到,初始电位为0,最终电位为正,因此摊余界限之和是整个树上所有合并总成本的上限。一旦我们创建了bst t_x,我们就可以找到每个节点x的标签,通过二进制搜索t_x中在x以0(log n)的代价死亡之前出生的最新元素。

    要改进绑定到O(n log(max level label)),可以惰性地合并树,只在需要时合并树的前几个元素,为当前节点提供解决方案。如果使用一个利用引用位置的BST,例如一个展开树,那么您就可以实现上述限制。

    希望上面的算法和分析至少足够清晰,可以遵循。如果您需要任何澄清,请发表评论。


    创建按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。所以,对于大多数人(但不是每个人),这在出生时都会被设置为真。

    挑战在于确定has_a_living_ancestor可以设置为假的情况。每一个人出生时,我们都要通过祖先来完成一个使命,但只有那些以东记(11)所说的祖先才是真的。

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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
    }

    这种图形看起来像双向图形。但在本例中,首先构建所有人员的列表,然后可以构建列表关系,并在每个节点之间建立fromrelations和torelations。那么,你所要做的就是,对于每个人,你只需要导航类型(儿子,女儿)的关系。既然你有约会,你就可以计算所有的事情。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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();
    }


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

    由于日期必须严格增加,我们可以使用二进制搜索(或整洁的数据结构)来查找O(log n)时间内最遥远的后代。

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

    我仍然需要弄清楚最好的案例是如何工作的,并试图更好地识别最坏的案例(看看是否有解决方法)


    这是我的刺:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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();

    • 有一个相对简单的O(n logn)算法,它借助一个合适的树按时间顺序扫描事件。

    • 你真的不应该布置你自己解决不了的作业。