我应该使用哪种数据结构来进行区间最小值查询,需要使用O(n)的存储和O(log n)的查询时间?

22
我被算法课的以下作业问题难住了: 假设我们有一个包含 n 个值 x1,x2... xn 的序列,并且寻求快速响应形式为“给定 i 和 j,找到 x i ... x j 中的最小值”的重复查询。 设计一种数据结构,它使用 O(n)空间并在 O(log n)时间内回答查询。 首先,我不确定序列是指排序集还是未排序集-但由于没有另外说明,我将假定序列表示未排序。 因此,我意识到这显然必须涉及二叉树,如果我们谈论 O(log N) 查找时间。所以基本上,你有一个集合S,你将S中的每个元素插入二叉树中。问题是,问题基本上要我想出一种方法,在我得到一个范围索引的未排序集合时来回答查询,然后在 O(log N) 时间内确定该范围内的最小值。怎么可能呢?即使将集合中的每个数字插入树中,我能做的最好的就是在O(log N)时间内查找任何特定数字。这样无法让我在 S 的未排序数字范围内找到最小值。 有什么建议吗?

6
感谢您在提问之前说明正在做作业并尝试解决问题,我会为您进行翻译。欢迎来到 Stack Overflow! - JoshJordan
2
+1:非常好的作业问题。 - aib
这是来自斯基纳的书《算法设计手册》。 - Dimath
6个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
13

如果集合是有序的,你不需要一个树。在范围[i,j]中最小的元素将具有索引i。

因此,假设你的序列元素按它们在树叶处的索引顺序存储。是否可以在每个内部节点上存储任何其他信息(比如某种最小值和最大值),以便于查询?

如果可以,在树平衡的情况下,如果只查看从根到{i,j}两个元素的两条路径即可回答查询,则您将实现O(log N)的查找成本。由于带有N个叶子的平衡二叉树包含(2N-1)个总节点,因此您还将满足O(N)的存储限制。


更多细节:考虑计算范围[i,j]中的最小值。

在树的每个内部节点A中,保持其下方所有叶子的最小值。这可以在构建树时自下而上计算。

现在从叶子i开始。向上遍历树,并将i处的值或任何已知位于i右侧且位于j左侧的值作为候选最小值。停留在i和j的公共祖先的下面一个节点。

再次从叶子j开始。向上遍历树,再次将j处的值或任何已知位于j左侧且位于i右侧的值作为候选最小值。

然后,[i,j]的最小值是你计算出的两个值中的较小值。计算最大值类似。总存储要求为每个内部节点2个值加2个指针加每个叶子1个值,对于完整的树为N + 4(N-1)。

从叶子i到根的路径与如果您正在搜索叶子i时下降树的路径相同。


C#代码用于搜索:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace RangeSearch
{
    public class RangeSearch
    {
        int[] tree;
        int N;

        int LeafLocation(int leafNumber) { return leafNumber + N - 1; }
        int LeafValue(int leafNumber) { return tree[ LeafLocation(leafNumber)]; }
        int LeftChild(int x) { return 2*x + 1; }
        int RightChild(int x) { return 2*x + 2; }
        int Parent(int x) { return (x-1)/2; }
        bool IsPowerOf2(int x) { while (x > 0) { if (x == 1) return true; if ((x & 1) == 1 ) return false; x = x >> 1; } return false; }
        bool IsAncestorOf( int x, int y ) { if( x>y ) return false;  return x==y || IsAncestorOf(LeftChild(x), y) || IsAncestorOf(RightChild(x),y); } // note: violating time bound for legibility, can fix by storing min/max descendant index at each node

        public RangeSearch(params int[] vals)
        {
            if (!IsPowerOf2(vals.Length))
                throw new ArgumentException("this implementation restricted to N being power of 2");
            N = vals.Length;
            tree = new int[2 * N - 1];
            // the right half of the array contains the leaves
            vals.CopyTo(tree, N - 1);
            // the left half of the array contains the interior nodes, each of which holds the minimum of all its children
            for (int i = N - 2; i >= 0; i--)
                tree[i] = Math.Min(tree[LeftChild(i)], tree[RightChild(i)]);           
        }

        public int FindMin(int a, int b)
        {
            if( a>b )
                throw new ArgumentException( "FindMin expects a range [a,b] with a<=b" );
            int x = Walk( a, true, b);
            int y = Walk( b, false, a);
            return Math.Min(x, y);
        }

        int Walk( int leafNumber, bool leftSide, int otherLeafNumber )
        {
            int minSoFar =  LeafValue(leafNumber);
            int leafLocation = LeafLocation(leafNumber);
            int otherLeafLocation = LeafLocation(otherLeafNumber);
            int parent = Parent(leafLocation);
            bool cameFromLeft = (leafLocation == LeftChild(parent));
            return Walk2(minSoFar, parent, cameFromLeft, leftSide, otherLeafLocation);
        }
        int Walk2(int minSoFar, int node, bool cameFromLeft, bool leftSide, int otherLeafLocation)
        {
            if (IsAncestorOf(node, otherLeafLocation))
                return minSoFar;
            if (leftSide)
                minSoFar = !cameFromLeft ? minSoFar : Math.Min(minSoFar, tree[RightChild(node)]);
            else
                minSoFar = cameFromLeft ? minSoFar : Math.Min(minSoFar, tree[LeftChild(node)]);
            return Walk2(minSoFar, Parent(node), node == LeftChild(Parent(node)), leftSide, otherLeafLocation);
        }
    }
}

用于测试的C#代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace RangeSearch
{
    class Program
    {
        static void Main(string[] args)
        {
            RangeSearch rngA = new RangeSearch(9, 3, 7, 1);
            System.Diagnostics.Trace.Assert(3 == rngA.FindMin(0, 2) );
            System.Diagnostics.Trace.Assert(1 == rngA.FindMin(0, 3));
            System.Diagnostics.Trace.Assert(1 == rngA.FindMin(1, 3));

            RangeSearch rngB = new RangeSearch(1, 7, 3, 9);
            System.Diagnostics.Trace.Assert(3 == rngB.FindMin(1, 3));
            System.Diagnostics.Trace.Assert(1 == rngB.FindMin(0, 3));
            System.Diagnostics.Trace.Assert(1 == rngB.FindMin(0, 2));

            RangeSearch rngC = new RangeSearch(17, 21, 77, 70, 58, 79, 79, 89);
            System.Diagnostics.Trace.Assert(21 == rngC.FindMin(1, 7));

            RangeSearch rngD = new RangeSearch(94, 78, 88, 72, 95, 97, 89, 83);
            System.Diagnostics.Trace.Assert(72 == rngD.FindMin(1, 6));

            RangeSearch rngE = new RangeSearch(0, 66, 6, 43, 34, 34, 63, 49);
            System.Diagnostics.Trace.Assert(34 == rngE.FindMin(3, 4));

            Random rnd = new Random();
            for (int i = 0; i < 1000000; i++)
            {
                int[] tmp = new int[64];
                for (int j = 0; j < tmp.Length; j++)
                    tmp[j] = rnd.Next(0, 100);
                int a = rnd.Next(0, tmp.Length);
                int b = rnd.Next(a, tmp.Length);
                RangeSearch rng = new RangeSearch(tmp);
                System.Diagnostics.Trace.Assert(Min(tmp, a, b) == rng.FindMin(a, b));
            }
        }

        static int Min(int[] ar, int a, int b)
        {
            int x = ar[a];
            for (int i = a + 1; i <= b; i++)
                x = Math.Min(x, ar[i]);
            return x;
        }

    }
}

3
这不会违反O(N)存储要求。拥有N个元素的平衡二叉树包含2N-1个节点。如果每个节点具有固定数量的数据,则总存储量为O(N)。 - Eric
2
这个回答让我感到不舒服。你能提供一些伪代码,更恰当地演示如何在O(log N)时间内获取范围的最小值吗?我看到你的方法非常类似于笛卡尔树方法,但我认为你高估了查找的效率。 - Welbog
1
现在我想起来了,你所描述的是一种具有O(N log N)存储的线段树。请参考:http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=lowestCommonAncestor#Segment_Trees - hobodave
@Eric:我觉得你和我对“左”和“右”的定义是相反的,但即使它适用于集合9 3 7 1,如果我将其简单地倒转为1 7 3 9,它也会崩溃,因为现在你对左右的想法没有考虑到这一点。至少在我在纸上运行你的算法时是如此。 - Welbog
@Eric:我被说服了。+1 为你的努力。花了我一些时间才看懂它是如何工作的。 - Welbog
显示剩余8条评论

8

好的,我认为我已经为您做了一个良好的开端,并在此过程中学到了新知识。

我建议您查看维基百科上关于笛卡尔树的条目。我不想告诉您更多的内容,以免替您完成作业,但是您似乎是个聪明的人,我相信您能够理解它。

顺便感谢您帮助我学习一种新的数据结构!


2
不幸的是,我认为笛卡尔树不能在最坏情况下给你O(log N)的搜索时间,因为它们不是平衡的。 - comingstorm
@comingstorm:你说得对。然而,如果我们假设输入集是随机的,它将平均为O(log N)。只要OP在作业中证明了他的理解,我相信他会获得满分。 - Welbog
我作为TA评估过算法考试答案。我会给予部分分数。如果它没有解决所述的问题,但通过解决一个(仅有的)稍微简单一些的问题来显示理解,那么我会给予部分分数。如果没有解释的话,我会给予较少的分数:因为这看起来只是一个疏忽。 - Jonas Kölker

1

3
我明白,起初我也是这么想的。但问题本身毫无意义,因为如果集合已排序,那么在任何子序列中找到最小元素都是一个O(1)的操作。对于给定的索引i和j,最小元素总是位于索引i处。 - Channel72
6
排序和有序并不是同一件事情,@Channel72。 - Welbog
1
同Welbog所说:sorted和ordered不是同一个意思。Ordered只是表示项目以固定顺序出现 - 并不意味着它们已排序。有序的例子:{3, 1, 7, 2}显然没有排序。我认为作业描述的意思是给你一个未排序数字数组的输入。 - Mark S
为什么我们需要像稀疏表这样的高级算法来解决RMQ问题?我们为什么不只是对集合进行排序,然后查询就可以在常数时间内完成呢? - Alcott
@Alcott:如果输入数组是[9 1 7 3],我查询的是(1, 2)---代表子序列[1 7]---那么你的答案应该是1。如果你存储的数组是[1 3 7 9],我查询的是(1, 2),你是如何得到1的呢? - Jonas Kölker
我想这个链接可能有用,可以解释有序集合和排序集合之间的区别,https://dev59.com/wnNA5IYBdhLWcg3wH6AW - nybon

1

0
你考虑过区间树吗? 看了一下维基百科的条目,它似乎非常符合你所需求的。 http://en.wikipedia.org/wiki/Interval_tree 编辑: 是的,看起来区间树并不适用于这种情况...

不,看完那个结构的描述后,它根本不适合这种情况。那个结构以排序的方式存储间隔,而这里的数据是未排序但有序的。在这种情况下,间隔没有意义。除非你有想要创建间隔的花哨方法? - Welbog
仍然,这个答案不值得-3。+1 - aib

0

一些提示:

假设您以某种方式存储了长度为1、2、4、8等的所有对齐子数组的最小值? 您可以通过查看其中多少个最小值来返回正确答案? 如何存储它们以便您可以有效地检索它们?

(*例如,存储min(x0...3)和min(x4...x7),但不是min(x1...x4))


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,