65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中通过 RMQ 约简解决 LCA

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (12投票s)

2011年1月2日

CPOL

3分钟阅读

viewsIcon

34035

downloadIcon

320

在树中查找最近公共祖先。

引言

昨天我花了一些时间寻找一种算法来找到给定树中的最近公共祖先。 LCA 是两个子节点共享的第一个节点。

lca1.png

在上图中,3 和 6 的最近公共祖先是 1。节点 0 也是一个公共祖先,但不是最近的公共祖先。

最近公共祖先在社交网络、计算机网络、编译器中的公共子表达式消除等方面有很多应用。我的需求是在表达式树中找到公共祖先。 我在互联网上找不到任何 C# 的相关内容。 我决定根据 Robert Tarjan、Omer Berkman 和 Uzi Vishkin 的工作编写一个。

我的图中的一个节点有一个值和一组子节点。 我们没有对父节点的引用。 在此图中“识别”节点的唯一方法是通过引用。

/// <summary>
/// A tree node.
/// </summary>
/// <typeparam name="T">The type of the value associated with this node.</typeparam>
public interface ITreeNode<T>
{
    /// <summary>
    /// Gets or sets the value.
    /// </summary>
    /// <value>The value.</value>
    T Value { get; set; }

    /// <summary>
    /// Gets the children.
    /// </summary>
    /// <value>The children.</value>
    IEnumerable<ITreeNode<T>> Children { get; }
}
示例树中的一个节点。

Using the Code

我们将创建一个具有单个方法的辅助类。 此方法接受两个节点并返回它们最常见的父节点。

IGraphNode<T> FindCommonParent(ITreeNode<T> x, ITreeNode<T> y) 

Berkman 和 Vishkin 的算法非常简单。 我们首先做一些预处理。 我们使用欧拉路径访问每个节点并将此存储在数组中。 可以使用深度优先遍历找到欧拉路径。 我们从根开始,记录从根到第一个子节点的路径。 如果该子节点也有一个子节点,那么我们将记录到该子节点的路径,一直到图的底部。 一旦我们到达底部,我们将从子节点返回父节点记录路径。 如果父节点有另一个子节点,我们将执行相同的过程,直到我们访问了图中的每个节点并返回到根节点。

lca4.png

上图显示了欧拉路径和结果数组。
private void PreProcess()
{
    // Eulerian path visit of graph 
    Stack<ProcessingState> lastNodeStack = new Stack<ProcessingState>();
    ProcessingState current = new ProcessingState(_rootNode);
    ITreeNode<T> next;
    lastNodeStack.Push(current);

    NodeIndex nodeIndex;
    int valueIndex;
    while (lastNodeStack.Count != 0)
    {
        current = lastNodeStack.Pop();
        if (!_indexLookup.TryGetValue(current.Value, out nodeIndex))
        {
            valueIndex = _nodes.Count;
            _nodes.Add(current.Value);
            _indexLookup[current.Value] = new NodeIndex(_values.Count, valueIndex);
        }
        else
        {
            valueIndex = nodeIndex.LookupIndex;
        }
        _values.Add(valueIndex);

        // If there is a next then push the current value on to the stack along with 
        // the current value.
        if (current.Next(out next))
        {
            lastNodeStack.Push(current);
            lastNodeStack.Push(new ProcessingState(next));
        }
    }
    _nodes.TrimExcess();
    _values.TrimExcess();
}
用于创建欧拉路径数组的代码。

预处理完成后,我们可以开始使用范围最小值查询来计算 LCA。 范围最小值查询的工作方式与听起来完全一样。 对于数组中给定的范围,我们需要找到最小值。 我们的范围由节点第一次被访问的时间定义。 我们进行哈希表查找以获取给定节点的信息,然后使用简单的 FOR 循环来查找给定范围内的最小值。 不包括哈希表查找,我们可以说最坏情况下的时间复杂度是 O(n),其中 n 是图中的节点数。

lca3.png

在给定的范围内,1 是最小值,也是最近的公共祖先。 范围由首次遇到两个节点的位置定义。
public ITreeNode<T> FindCommonParent(ITreeNode<T> x, ITreeNode<T> y)
        {
            // Find the first time the nodes were visited during preprocessing.
            NodeIndex nodeIndex;
            int indexX, indexY;
            if (!_indexLookup.TryGetValue(x, out nodeIndex))
            {
                throw new ArgumentException("The x node was not found in the graph.");
            }
            indexX = nodeIndex.FirstVisit;
            if (!_indexLookup.TryGetValue(y, out nodeIndex))
            {
                throw new ArgumentException("The y node was not found in the graph.");
            }
            indexY = nodeIndex.FirstVisit;

            // Adjust so X is less than Y
            int temp;
            if (indexY < indexX)
            {
                temp = indexX;
                indexX = indexY;
                indexY = temp;
            }

            // Find the lowest value.
            temp = int.MaxValue;
            for (int i = indexX; i < indexY; i++)
            {
                if (_values[i] < temp)
                {
                    temp = _values[i];
                }
            }
            return _nodes[temp];
        } 

关注点

此解决方案存在一个小问题。 如果我们正在计算节点 X、Y 的 LCA,并且 Y 是 X 的后代,那么此解决方案将返回 X。 对于我的要求,这不是问题,但在某些情况下可能想要返回 X 的直接父级。

这是我第一次尝试,我不是算法专家。 这个问题有很多更优化的解决方案。 这个解决方案的代码很少,而且非常容易理解。 如果您有任何反馈,请告诉我。

© . All rights reserved.