在有根和加权的树中,如何找到每个节点一定距离内的节点数?你只需要考虑下边缘,例如从根向下的节点。请记住每条边都有重量。
我可以在O(N^2)
时间使用每个节点的DFS并跟踪行进的距离来做到这一点,但是N >= 100000
它有点慢。我很确定你可以通过DP的未加权边缘轻松解决它,但是有人知道如何快速解决这个问题吗? (小于N^2
)
答案 0 :(得分:3)
通过使用以下观察结果,可以改善我之前对 O(nlog d)时间和O(n)空间的回答:
给定节点v上足够接近的节点数是每个子节点的足够接近节点数的总和,减去刚刚变得不足的节点数量 - 关闭。
让我们调用距离阈值m,以及两个相邻节点u和v d(u,v)之间边缘的距离。
对于每个节点v,我们将保持一个最初为0的计数c(v)。
对于任何节点v,请考虑从v的父节点到根节点的祖先链。调用此链中的第i个节点a(v,i)。注意,对于该链中的第一个节点的某个数i> = 0,并且没有其他节点,需要将v计数为足够接近。如果我们能够快速找到i,那么我们可以简单地递减 c(a(v,i + 1))(使它(可能进一步)低于0),这样当a的计数时(v,i + 1)的孩子在以后的传球中加入,v被正确排除在计算之外。如果我们在将节点v添加到c(v)之前为节点v的所有子节点计算完全准确的计数,则任何此类排除都会正确地“传播”到父计数。
棘手的部分是找到我有效率。调用从v到根s(v,j)的路径上的第一个j> = 0边的距离之和,并调用这些路径长度的所有深度(v)+1的列表,列在增加中订单,s(v)。我们想要做的是二进制搜索第一个条目的路径长度s(v)列表大于阈值m:这将在log(d)时间内找到i + 1。问题是构建s(v)。我们可以使用从v到root的运行总计轻松地构建它 - 但是每个节点需要O(d)时间,从而使任何时间改进无效。我们需要一种在恒定时间内从s(parent(v))构造s(v)的方法,但问题是当我们从一个节点v递归到它的子u时,路径长度“以错误的方式”增长:每一个路径长度x需要变为x + d(u,v),并且需要在开头添加新的路径长度0。这似乎需要O(d)更新,但一个技巧可以解决问题......
解决方案是在每个节点v计算从v到根的路径上所有边的总路径长度t(v)。这可以在每个节点的恒定时间内容易地完成:t(v)= t(父(v))+ d(v,父(v))。然后我们可以通过在-s(parent(v))的开头添加-t来形成s(v),并且在执行二进制搜索时,考虑每个元素s(v,j)来表示s(v,j)+ t (或等效地,二进制搜索m - t而不是m)。通过使节点v共享v的路径长度数组具有子u,并且s(u)被认为在s(v)之前开始一个存储器位置,可以在O(1)时间内在开始时插入-t。所有路径长度数组在大小为d + 1的单个内存缓冲区内“右对齐” - 具体而言,深度为k的节点将使其路径长度数组从缓冲区内的偏移量dk开始,以便为其后代节点提供前置空间条目。数组共享意味着兄弟节点将覆盖彼此的路径长度,但这不是问题:我们只需要s(v)中的值保持有效,而v和v的后代在预订DFS中处理。
通过这种方式,我们在O(1)时间内获得O(d)路径长度增加的效果。因此,在给定节点处找到i所需的总时间是O(1)(构建s(v))加上O(log d)(使用修改的二进制搜索找到i)= O(log d)。单个预订DFS传递用于查找和减少每个节点的适当祖先计数;后序DFS传递然后将子计数总计为父计数。这两个传递可以组合成一个遍历节点,在递归之前和之后执行操作。
答案 1 :(得分:2)
[编辑:请参阅我的其他答案,以获得更高效的O(nlog d)解决方案:)]
这是一个简单的O(nd)-time,O(n)空间算法,其中d是树中任何节点的最大深度。具有n个节点的完整树(其中每个节点具有相同数量的子节点的树)具有深度d = O(log n),因此这应该比基于O(n ^ 2)DFS的方法快得多。但是,如果每个节点的足够接近的后代的数量很小(即,如果DFS仅遍历少量级别),那么你的算法也不应该太糟糕。
对于任何节点v,请考虑从v的父节点到根节点的祖先链。注意,对于该链中的第一个节点的某个数i> = 0,并且没有其他节点,需要将v计数为足够接近。所以我们需要做的就是每个节点,向上爬向根直到总路径长度超过阈值距离m,在我们去的时候递增每个祖先的计数。有n个节点,每个节点最多有d个祖先,所以这个算法很简单O(nd)。