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)。然而,一般来说,你有排序预处理步骤,即
我想到今天早上,然后发现@alexey kukanov也有类似的想法。但我的更丰满,有更多的优化,所以我无论如何都会贴出来。
该算法是
所有这些操作的总和是
对于实际的数据集,这将是具有相当小常量的
我的建议:
- 此外,在问题陈述中描述的值中,每个个人记录将有两个字段:子计数器和动态增长向量(以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(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,例如一个展开树,那么您就可以实现上述限制。
希望上面的算法和分析至少足够清晰,可以遵循。如果您需要任何澄清,请发表评论。
创建按
对于每个人,定义一个
为每个人定义另一个字段,称为
挑战在于确定
在这个过程中,如果我们找到了一个没有活祖先的祖先,现在已经死了,那么我们可以将
我们最近在我们的一个项目中实现了关系模块,其中我们拥有数据库中的所有内容,是的,我认为算法是最好的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)算法,它借助一个合适的树按时间顺序扫描事件。
你真的不应该布置你自己解决不了的作业。