8

Can someone give me ideas for the following problem?

Given an array ar[] of length n, and some queries, each query is of the form a, b, c, find the number with the smallest index i such that the index lies in the range [c, n] and such that a < ar[i] < b. There are (n - 1) in total, c goes from 1 to n - 1. The expected complexity for each query should be about O(log n), and a precomputation of complexity at most O(n log n) is suitable. Intuitively, segment tree came up to my mind, but I couldn't think of a way of building it, nor what to keep in each node.

Peter O.
  • 32,158
  • 14
  • 82
  • 96
Chris
  • 26,544
  • 5
  • 58
  • 71
  • No, the array isn't sorted :) – Chris Dec 14 '11 at 12:15
  • I've deleted my answer until I've thought about the `c` constraint... – Oliver Charlesworth Dec 14 '11 at 12:27
  • @OliCharlesworth Okay, take your time :D – Chris Dec 14 '11 at 12:28
  • is it intended, that the index `i = 0` will never be returned (`0 < c < n`, instead of `0 <= c < n`)? Is the worst-case complexity for time or space or both? – moooeeeep Dec 21 '11 at 10:48
  • In each node of your segment tree you keep an ordered list of the numbers in the segment. Then you binary search for `c`. – Thomas Ahle Dec 22 '11 at 00:43
  • @ThomasAhle That's the answer I posted 2 hours ago. And like I said there, that seems to end up O((log n)^2) for queries, not quite O(log n). – user12861 Dec 22 '11 at 03:24
  • @user12861 Now that I think of it, we don't need a segment tree, because the `[c, n]` range always goes all the way to the right. Hence we only need to precompute all n ordered lists, which can be done in using an immutable tree/ordered-list structure in `O(nlogn)` time and memory. – Thomas Ahle Dec 22 '11 at 11:34
  • @ThomasAhle You're right, that sounds like a better approach. I'll try to code that and see if I can get it to work out. – user12861 Dec 22 '11 at 13:53
  • @ThomasAhle On further reflection, that technique won't work. Precomputing n ordered lists with lengths ranging from 1 to n will take O(n^2 log n) time, which is more than the segment tree method. Unless you have some clever technique, in which case I'd be interested in seeing it if you have the time and inclination to code it. – user12861 Dec 22 '11 at 14:06
  • @user12861: You store the ordered lists as binary search trees. First you create the right most. Because you use an immutable tree implementation, you can create a new tree with an added value in logn time and memory. – Thomas Ahle Dec 22 '11 at 15:15
  • @ThomasAhle It certainly sounds clever, but I can't make it work myself (can't figure out how to build the tree from the right and keep it balanced enough if the values in the original array are, say: 80, 60, 76, 88). If you can, I'd love to see some code, or a full explanation. I wish I could figure it out, because if it did work it sounds like it would actually be O(log n) for queries instead of O(log^2 n). – user12861 Dec 22 '11 at 19:09
  • @user12861 You are right, I should put my code where my mouth is :) I've sent in an answer below. Check it out and see what you think of it. – Thomas Ahle Dec 23 '11 at 01:24

7 Answers7

4

Ok, I thought I should implement the O(nlogn)/O(logn) solution I discussed with user12861.

The code works by building n trees, one for each c. Each tree shares most of its content with the smaller ones, with a maximum of logn new nodes. This way the total memory and time cost for preprocessing is limited to O(nlogn).

The actual trees are quite similar to interval trees. The nodes store the minimum and maximum value they span over, and the smallest index they contain. My implementation is however not self-balancing, but this can be added using your favorite heuristics.

import sys

class Node:
    pass
class Interval(Node):
    def __init__(self,left,val,i,right):
        self.i = i; self.val = val
        self.left = left; self.right = right
        self.mini = min(left.mini, i, right.mini)
        self.min = min(left.min, val, right.min)
        self.max = max(left.max, val, right.max)
    def add(self,val,i):
        # We do naive inserting here. In a real worst case logn solution,
        # self-balancing would take place here.
        if (val,i) < (self.val,self.i): return Interval(self.left.add(val,i), self.val, self.i, self.right)
        if (self.val,self.i) < (val,i): return Interval(self.left, self.val, self.i, self.right.add(val,i))
        return self
    def query(self,a,b):
        # If the entire interval is covered, return mini, this is what keeps us
        # at max 2 opened nodes per level in the tree.
        if a <= self.min and self.max <= b: return self.mini
        # Otherwise compose an answer from the subtrees.
        res = sys.maxint
        if a <= self.val <= b: res = min(res, self.i)
        if self.left.min <= b and self.left.max >= a: res = min(res, self.left.query(a,b))
        if self.right.min <= b and self.right.max >= a: res = min(res, self.right.query(a,b))
        return res
class Tip(Node):
    def __init__(self):
        # This is a typical null-pattern idiom. The self.values are set to
        # the min/max identities to ensure to interference with the upwards
        # value propagation.
        self.mini = sys.maxint
        self.min = sys.maxint
        self.max = -sys.maxint
    def add(self,val,i):
        return Interval(Tip(), val, i, Tip())
    def query(self,a,b):
        return sys.maxint

# The input data array
data = [0, 3, 1, 2, 0, 4, 9, 6, 1, 7]
N = len(data)

# Build the array of trees. O(nlogn)
ar = [None]*N + [Tip()]
for i in range(N-1, -1, -1):
    ar[i] = ar[i+1].add(data[i],i)

# The query function. O(logn)
def query(a,b,c):
    return ar[c].query(a,b)

# Test
print query(2, 7, 4) # = 5
Thomas Ahle
  • 30,774
  • 21
  • 92
  • 114
  • 1
    This does look like the solution to me. Very nice! Despite your hints in the comments I couldn't figure out how to either build or query the tree myself, so thanks for providing the code. One note, the code you provided is for a <= ar[i] <= b, whereas the question above asked for a < ar[i] < b. The algorithm is correct though, and that's what's important. This answer should be accepted as correct instead of mine. – user12861 Dec 23 '11 at 16:10
  • Of course the worst case performance without self-balancing is quite poor. O(n^2) setup and O(n) queries. I'm not too experienced with trees, so I don't know how to do self-balancing, but I'll trust it can be done if you say so. – user12861 Dec 23 '11 at 16:48
  • Self-balancing would take as much code as is above, so I thought it would take focus away from the interesting stuff. If you'd like to do a balanced version, I recommend looking into treaps. The key point in all balancing is rotations, which in this case, as well as for most tree based datastructures, don't change the semantics. – Thomas Ahle Dec 23 '11 at 23:18
1

Have you gone through this? I guess it will help you understand the structure and think of a way of building it.Rest can be set of comparisons.

samridhi
  • 500
  • 2
  • 10
  • I do know what a segment tree is. And I have already implemented some simple stuff using it. Can you be more specific please? Can you tell me what I should store in each node of the segment tree? – Chris Dec 14 '11 at 15:47
1

The first attempt that I made build a tree from the ranges.

You'd first find the root of a subtree which contained all the solutions to the query. This is pretty easy. However: that subtree contains some values outside of the solution set.

So to find the lowest number, you'd traverse the tree twice (from the root down the left side of the tree and again from the root down the right side). At each point in those traversals, you'd check to see if you'd found a solution lower than your previous solution.

When done, the lowest solution within that set would be the lowest.

Since checking each node's solutions required a binary search on the list of indexes within that subtree, you'd end up running at O(ln(n)^2) which is too slow.

The improvement on that comes from the fact that this would be easy if we were always looking for the first element in the array. Then you don't need to do a binary search, you just get the first element.

To get to that place, you end up building n trees.

tree[0] has all values in the initializaing array
tree[1] has all but the first.
tree[2] has all but the first and second.

So each of those trees just contains the single best solution.

So the search runs in O(ln(n)).

In the example code, took a liberty with building the tree by not implementing custom data structures.

You build an array of nodes, make a copy of that array and sort one of the copies. This runs in O(n ln (n)). The other copy is used to find the position in the sorted array so you can delete an element each time you build another tree.

Of course it is possible to delete a node in a linked list in constant time, but since you only have a reference to the object, I suspect that the java implementation has to search the linked list for the item to be deleted, so building the trees probably takes O(n^2) in this example. Easy to fix though.

    public class ArraySearcher {

        public class SegmentNode implements Comparable<SegmentNode> {

            int min;
            int max;

            SegmentNode root;
            SegmentNode left;
            SegmentNode right;

            int lowIndex;

            public int compareTo(final SegmentNode obj)
            {
                return this.min - obj.min;

            }

            public boolean contains(final int n)
            {
                if ((n > this.min) && (n < this.max)) {
                    return true;
                } else {
                    return false;
                }
            }

            /**
             * Given the root of a tree, this method will find the center node. The
             * center node is defined as the first node in the tree who's left and
             * right nodes either contain solutions or are null.
             * 
             * If no node can be found which contains solution, this will return
             * null.
             * 
             * @param a
             * @param b
             * @return
             */
            public SegmentNode findCenter(final int a, final int b)
            {
                SegmentNode currentNode = this;

                while (true) {

                    /*
                     * first check to see if no solution can be found in this node.
                     * There is no solution if both nodes are
                     */
                    if ((a < currentNode.min && b < currentNode.min)
                            || (a > currentNode.max && b > currentNode.max)) {
                        return null;
                    }

                    /*
                     * Now check to see if this is the center.
                     */
                    if (((currentNode.left == null) || (a < currentNode.left.max))
                            && ((currentNode.right == null) || (b > currentNode.right.min))) {

                        // this is the center, return it.
                        return currentNode;

                    } else if ((currentNode.left == null)
                            || (a < currentNode.left.max)) {
                        // no solution on one side, is it in the left?
                        currentNode = currentNode.left;
                    } else {
                        currentNode = currentNode.right;
                    }
                }

            }

            public SegmentNode findLeft(final int seekMin)
            {
                /*
                 * keep going left until
                 */
                if ((this.min > seekMin)) {
                    return this;
                } else {
                    if (this.right == null) {
                        return null;
                    }
                    return this.right.findLeft(seekMin);
                }
            }

            /**
             * This method can be called on the center node. it traverses left
             * 
             * @param a
             * @param b
             * @return
             */
            public Integer findLeftmostLowestIndex(final int n)
            {
                if (null == this.left) {
                    return null;
                }

                int lowest = Integer.MAX_VALUE;
                SegmentNode current = this.left;

                // update the lowest with the right node if it is greater
                // than the current node.
                while (true) {

                    // collect the best value from the right and/or left
                    // if they are greater than N
                    if ((null != current.left) && (current.left.min > n)) {
                        lowest = current.left.lowIndex(lowest);
                    }
                    if ((null != current.right) && (current.right.min > n)) {
                        lowest = current.right.lowIndex(lowest);
                    }
                    if ((null == current.left) && (null == current.right)
                            && (current.max > n)) {
                        lowest = current.lowIndex(lowest);
                    }
                    // quit when we've gone as far left as we can
                    int seek = current.leftSeek(n);
                    if (seek == 0) {
                        break;
                    } else if (seek < 0) {
                        current = current.left;
                    } else {
                        current = current.right;
                    }

                }
                if (lowest == Integer.MAX_VALUE) {
                    return null;
                } else {
                    return new Integer(lowest);
                }

            }

            public SegmentNode findMatch(final int seekMin, final int seekMax)
            {
                if ((this.min > seekMin) && (this.max < seekMax)) {
                    return this;
                } else if ((this.min > seekMin) && (this.left != null)) {
                    return this.left.findMatch(seekMin, seekMax);
                } else {
                    if (this.right == null) {
                        return null;
                    }
                    return this.right.findMatch(seekMin, seekMax);
                }
            }

            public SegmentNode findMatchRight(final int seekMin, final int seekMax)
            {
                if ((this.min > seekMin) && (this.max < seekMax)) {
                    return this;
                } else if (this.max < seekMax) {
                    if (this.right == null) {
                        return null;
                    }
                    return this.right.findMatchRight(seekMin, seekMax);
                } else {
                    if (this.left == null) {
                        return null;
                    }
                    return this.left.findMatchRight(seekMin, seekMax);
                }
            }

            /**
             * Search for the first number in the tree which is lower than n.
             * 
             * @param n
             * @return
             */
            public Integer findRightmostLowestIndex(final int n)
            {
                if (null == this.left) {
                    return null;
                }

                int lowest = Integer.MAX_VALUE;
                SegmentNode current = this.right;

                // update the lowest with the right node if it is greater
                // than the current node.
                while (true) {

                    // collect the best value from the right and/or left //this.max
                    // < b
                    // if they are greater than N
                    if ((null != current.left) && (current.left.max < n)) {
                        lowest = current.left.lowIndex(lowest);
                    }
                    if ((null != current.right) && (current.right.max < n)) {
                        lowest = current.right.lowIndex(lowest);
                    }
                    if ((null == current.left) && (null == current.right)
                            && (current.min < n)) {
                        lowest = current.lowIndex(lowest);
                    }

                    // quit when we've gone as far left as we can
                    int seek = current.rightSeek(n);
                    if (seek == 0) {
                        break;
                    } else if (seek < 0) {
                        current = current.left;
                    } else {
                        current = current.right;
                    }

                }
                if (lowest == Integer.MAX_VALUE) {
                    return null;
                } else {
                    return new Integer(lowest);
                }
            }

            /**
             * 
             * @param seekMin
             * @param seekMax
             * @return
             */
            public SegmentNode findSegmentRoot(final int seekMin, final int seekMax)
            {

                return null;
            }

            public int leftSeek(final int n)
            {
                if ((null == this.left) && (null == this.right)) {
                    return 0;
                    // } else if ((null != this.left) && (this.left.max > n)) {
                } else if ((null != this.left) && ((n < this.left.max))
                        || (this.left.contains(n))) {
                    return -1;
                    // } else if ((null != this.right) && (this.right.min <= n)) {
                } else if ((null != this.right) && ((n >= this.right.min))) {
                    return +1;
                } else {
                    return 0;
                }
            }

            public int rightSeek(final int n)
            {
                if ((null == this.left) && (null == this.right)) {
                    return 0;
                } else if ((null != this.left) && (this.left.max >= n)) {
                    return -1;
                } else if ((null != this.right) && (this.right.min < n)) {
                    return +1;
                } else {
                    return 0;
                }
            }

            @Override
            public String toString()
            {
                StringBuilder value = new StringBuilder();
                if (null != this.left) {
                    value.append("{ { " + this.left.min + ", " + this.left.max
                            + "} }, ");
                } else {
                    value.append("{ " + this.min + " }, ");
                }

                if (null != this.right) {
                    value.append("{ { " + this.right.min + ", " + this.right.max
                            + "} }");
                } else {
                    value.append("{ " + this.max + " }, ");
                }
                return value.toString();
            }

            private int lowIndex(final int lowest)
            {
                if (lowest < this.lowIndex) {
                    return lowest;
                } else {
                    return this.lowIndex;
                }
            }
        }

        public static int bruteForceSearch(final int[] array, final int a,
                final int b, final int c)
        {
            // search from c onward

            /**
             * search for the first value of the array that falls between a and b
             * ignore everything before index of c
             */
            for (int i = c; i < array.length; i++) {
                if ((a < array[i]) && (array[i] < b)) {
                    return i;
                }
            }
            return -1;
        }

        SegmentNode[] trees;

        public ArraySearcher(final int[] array)
        {
            buildTree(array);
        }

        public void buildTree(final int[] array)
        {
            ArrayList<SegmentNode> mNodes = new ArrayList<SegmentNode>();
            for (int i = 0; i < array.length; i++) {
                SegmentNode mCurrentNode = new SegmentNode();
                mCurrentNode.lowIndex = i;
                mCurrentNode.min = array[i];
                mCurrentNode.max = array[i];
                mNodes.add(mCurrentNode);
            }
            ArrayList<SegmentNode> unsortedClone =
                    new ArrayList<SegmentNode>(mNodes);

            // n (ln (n) )
            Collections.sort(mNodes);

            LinkedList<SegmentNode> nodesList = new LinkedList<SegmentNode>(mNodes);

            this.trees = new SegmentNode[nodesList.size()];
            for (int i = 0; i < this.trees.length; i++) {
                this.trees[i] = merge(nodesList, 0, nodesList.size() - 1);

                // we remove the ith one at each iteration
                nodesList.remove(unsortedClone.get(i));
            }
        }

        /**
         * 
         * @param nodes
         * @param i
         * @param j
         * @return
         */
        public SegmentNode merge(final List<SegmentNode> nodes, final int i,
                final int j)
        {
            if (i > j) {
                throw new AssertionError();
            }
            SegmentNode left;
            SegmentNode right;
            int count = j - i;

            if (count == 1) {
                SegmentNode mParent = merge(nodes.get(i), nodes.get(i + 1));
                return mParent;
            } else if (count == 0) {
                return nodes.get(i);
            } else {
                int mid = (count / 2) + i;
                left = merge(nodes, i, mid);
                right = merge(nodes, mid + 1, j);
            }
            return merge(left, right);
        }

        /**
         * Build a parent segment from two other segments.
         * 
         * @param a
         * @param b
         * @return
         */
        public SegmentNode merge(final SegmentNode a, final SegmentNode b)
        {
            SegmentNode parent = new SegmentNode();
            parent.root = parent;
            parent.min = a.min;
            parent.left = a;
            parent.max = b.max;
            parent.right = b;

            if (a.lowIndex > b.lowIndex) {
                parent.lowIndex = b.lowIndex;
                b.root = parent;
            } else {
                parent.lowIndex = a.lowIndex;
                a.root = parent;
            }

            return parent;
        }

        /**
         * The work horse, find all the points with indexes greater than c that lie
         * between a and b.
         * 
         * @param a
         * @param b
         * @param c
         * @return
         */
        public Integer search(final int a, final int b, final int c)
        {
            if (c < this.trees.length) {
                SegmentNode root = this.trees[c];

                if ((a > root.max) || (b < root.min)) {
                    return null;
                }

                SegmentNode center = root.findCenter(a, b);
                if (null == center) {
                    return null;
                }

                // special case to deal with a node with no children.
                if ((center.left == null) && (center.right == null)) {
                    if ((a < center.min) && (b > center.max)) {
                        return new Integer(center.lowIndex);
                    } else {
                        return null;
                    }
                }

                Integer right = center.findRightmostLowestIndex(b);
                Integer left = center.findLeftmostLowestIndex(a);

                if ((null == right) && (null == left)) {
                    return null;
                } else if (null == right) {
                    return left;
                } else if (null == left) {
                    return right;
                } else if (right.compareTo(left) > 0) {
                    return left;
                } else {
                    return right;
                }

            } else {
                return null;
            }
        }
    }

by the way, I'm still using tests like this to verify it against the brute force approach:

    static void testBoth(final int[] array, final ArraySearcher searcher,
            final int a, final int b, final int c)
    {
        System.out.println("ArraySearcherTest.testBoth(array, mSearcher, " + a
                + ", " + b + ", " + c + ");");
        int expected = ArraySearcher.bruteForceSearch(array, a, b, c);
        Integer calcObj = searcher.search(a, b, c);
        int calcInt = -1;
        if (null != calcObj) {
            calcInt = calcObj.intValue();
        }
        assertEquals(expected, calcInt);

    }

    @Test
    public void randomizedProblemTester()
    {
        for (int i = 0; i < 100; i++) {
            // build arrays from 5 to 20 elements long
            int[] array = new int[TestUtils.randomInt(5, 20)];
            System.out.print("int[] array = {");
            for (int j = 0; j < array.length; j++) {
                // with values from 0 to 100
                array[j] = TestUtils.randomInt(0, 100);
                System.out.print(array[j] + ", ");
            }
            System.out.println("};");

            ArraySearcher mSearcher = new ArraySearcher(array);
            for (int j = 0; j < array.length; j++) {
                int a = TestUtils.randomInt(0, 100);
                int b = TestUtils.randomInt(a, 100);
                int c = TestUtils.randomInt(0, 20);
                ArraySearcherTest.testBoth(array, mSearcher, a, b, c);
            }

        }


   }
Thomas Ahle
  • 30,774
  • 21
  • 92
  • 114
Mason Bryant
  • 1,372
  • 14
  • 23
  • Something's wrong with findLowestIndex as it's written. It only returns either the passed in value or null. Looks easily fixable, just change that final return null to something better I think. Works on your examples because they all return the passed in third parameter you started with. Still thinking about efficiency considerations as I'm not very familiar with segment trees, but looks promising anyway. – user12861 Dec 20 '11 at 04:31
  • Efficiency and technique look right to me. Nice! Needs a fair bit of unit testing to prove this code is right though, I haven't checked it super thoroughly. Better to work on the explanation I'd say, that's more important than the code anyway. – user12861 Dec 20 '11 at 04:41
  • Fist of all, thanks for the answer! I have went through your code, I couldn't get ***every*** part of it, I would really extremely appreciate the explanation, which I'm waiting for eagerly. Thanks again! :) – Chris Dec 20 '11 at 08:54
  • Ech... you're right (in your comment on the edit). This doesn't solve it when the solution spans multiple trees. Hope you can solve that, I'm very interested to see if there is a solution. – user12861 Dec 20 '11 at 19:28
  • 1
    The new technique sounds like it should work well as far as O(log n) queries, but I'm not convinced that the setup is O(n log n). Sure, the sort is O(n log n), but then you are building n trees, and I think building each tree takes O(n log n), so I think your setup time works out to O(n^2 log n). – user12861 Dec 23 '11 at 13:45
  • @Thomas Ahle Hrm, yes you are correct. I'm not sure there is a clean solution to that problem. Seems like ye olde trade off between memory, set up time, and run time. – Mason Bryant Jan 03 '12 at 18:01
  • @Mason: See my answer, it builds the trees in nearly the normal way, but uses a functional programming style trick to save the intermediate trees without wasting memory. – Thomas Ahle Jan 03 '12 at 20:41
1

I've modified Mason Bryant's technique to something that works. The problems were more bugs in FindLowestIndex and a more major bug searching the tree (it can return multiple results).

Despite doing that work, it still doesn't actually solve the problem. O(n log n) time setting up is easy enough, but using this technique I'm only able to get O((log n)^2) query time. I wonder if you have a link to the original problem in case there are more clarifications there? Or I wonder if the problem is really solvable. Or maybe O((log n)^2) is "about" O(log n) as the problem requests, it's less than O(n) anyway.

The technique is to store our array in a typical segment tree, but along with the usual segment information we also store, in order, all the indexes of the elements under each node. This extra storage only takes an extra O(n log n) time/space if you add it up properly (n items stored at each of log n levels), so it doesn't hurt our setup time in any way that matters. Then we query the tree to find the minimum set of nodes that are contained by our range of (a, b). This query takes about the same time as a typical segment tree query (O(log n)) and finds at most about 2*log n matching segments. As we query, we find the lowest index in each matching segment that matches our constraint c. We can use a binary search to find this index since we kept the indices in order, so it takes O(log n) time worst case for each matching node. When we add it all up appropriately, the total time is O((log n)^2).

Let me know whichever steps you'd like clarified.

C# Code:

void Test() {
  DateTime start;
  TimeSpan at = new TimeSpan(), bt = new TimeSpan();

  Random rg = new Random();
  for(int i = 0; i < 20; i++) {
    // build arrays from 5 to 10000 elements long, random values between 0 and 100
    // Break even time for queries is around 10000 elements in array for the values and queries used
    int[] array = (from n in Enumerable.Range(1, rg.Next(5, 10000)) select rg.Next(0, 100)).ToArray<int>();

    // Setup should be O(n log n) time/space
    ArraySearcher Searcher = new ArraySearcher(array);

    // Test each array a number of times equal to its length, with random values for a, b, c
    for(int j = 0; j < array.Length; j++) {
      int a = rg.Next(-1, 101), b = rg.Next(a, 102), c = rg.Next(0, array.Length);
      start = DateTime.Now;
      int expected = BruteSearch(array, a, b, c);
      at += DateTime.Now - start;
      // Search should be O(log n) time, but in reality is O((log n)^2) :(
      start = DateTime.Now;
      int got = Searcher.QuickSearch(a, b, c);
      bt += DateTime.Now - start;
      System.Diagnostics.Debug.Assert(got == expected);
    }
  }
  MessageBox.Show(at.ToString() + ", " + bt.ToString());
}

int BruteSearch(int[] array, int a, int b, int c) {
  for(int i = c; i < array.Length; i++)
    if(a < array[i] && array[i] < b)
      return i;
  return -1;
}

class ArraySearcher {

  SegmentNode Root;
  List<SegmentNode> Nodes;

  public ArraySearcher(int[] array) {
    Nodes = array.Select((value, index) => new SegmentNode(value, value, new List<int> { index }, null, null)).ToList<SegmentNode>();
    // Sorting will take us O(n log n)
    Nodes.Sort();
    // Creating a normal segment tree takes O(n log n)
    // In addition, in this tree each node stores all the indices below it in order
    // There are a total of n of these indices at each tree level, kept in order as we go at no extra time cost
    // There are log n levels to the tree
    // So O(n log n) extra time and space is spent making our lists, which doesn't hurt our big O notation compared to just sorting
    this.Root = SegmentNode.Merge(Nodes, 0, Nodes.Count - 1);
  }

  public int QuickSearch(int a, int b, int c) {
    return this.Root.FindLowestIndex(a, b, c);
  }

  class SegmentNode : IComparable {

    public int Min, Max;
    public List<int> Indices;
    public SegmentNode Left, Right;

    public SegmentNode(int Min, int Max, List<int> Indices, SegmentNode Left, SegmentNode Right) {
      this.Min = Min;
      this.Max = Max;
      this.Indices = Indices;
      this.Left = Left;
      this.Right = Right;
    }

    int IComparable.CompareTo(object other) {
      return this.Min - ((SegmentNode)other).Min;
    }

    public static SegmentNode Merge(List<SegmentNode> Nodes, int Start, int End) {
      int Count = End - Start;
      if(Start == End)
        return Nodes[Start];
      if(End - Start == 1)
        return SegmentNode.Merge(Nodes[Start], Nodes[End]);
      return SegmentNode.Merge(SegmentNode.Merge(Nodes, Start, Start + Count/2), SegmentNode.Merge(Nodes, Start + Count/2 + 1, End));
    }

    public static SegmentNode Merge(SegmentNode Left, SegmentNode Right) {
      int LeftCounter = 0, RightCounter = 0;
      List<int> NewIndices = new List<int>();
      while(LeftCounter < Left.Indices.Count || RightCounter < Right.Indices.Count) {
        if(LeftCounter < Left.Indices.Count && (RightCounter == Right.Indices.Count || Left.Indices[LeftCounter] < Right.Indices[RightCounter]))
          NewIndices.Add(Left.Indices[LeftCounter++]);
        else
          NewIndices.Add(Right.Indices[RightCounter++]);
      }
      return new SegmentNode(Left.Min, Right.Max, NewIndices, Left, Right);
    }

    public int FindLowestIndex(int SeekMin, int SeekMax, int c) {
      // This will find at most O(log n) matching segments, always less than 2 from each level of the tree
      // Each matching segment is binary searched in at worst O(log n) time
      // Total does indeed add up to O((log n)^2) if you do it right
      if(SeekMin < this.Min && SeekMax > this.Max)
        return FindLowestIndex(this.Indices, c);
      if(SeekMax <= this.Min || SeekMin >= this.Max)
        return -1;
      int LeftMatch = this.Left.FindLowestIndex(SeekMin, SeekMax, c);
      int RightMatch = this.Right.FindLowestIndex(SeekMin, SeekMax, c);
      if(LeftMatch == -1)
        return RightMatch;
      if(RightMatch == -1)
        return LeftMatch;
      return LeftMatch < RightMatch ? LeftMatch : RightMatch;
    }

    int FindLowestIndex(List<int> Indices, int c) {
      int left = 0, right = Indices.Count - 1, mid = left + (right - left) / 2;
      while(left <= right) {
        if(Indices[mid] == c)
          return c;
        if(Indices[mid] > c)
          right = mid - 1;
        else
          left = mid + 1;
        mid = left + (right - left) / 2;
      }
      if(mid >= Indices.Count)
        return -1;
      // no exact match, so return the next highest.
      return Indices[mid];
    }
  }
}
Thomas Ahle
  • 30,774
  • 21
  • 92
  • 114
user12861
  • 2,358
  • 4
  • 23
  • 41
  • I think I get it. And I think `O(log ^ 2 N)` complexity is fine (hopefully). But there's something I can't get, why are you storing a list of all the indexes? Isn't it enough to store the beginning and end of each segment? (Sorry if I sound dumb :D). – Chris Dec 22 '11 at 17:32
  • The beginning and end of each segment let us know when we have values that fit our constraint `a < ar[i] < b`. Then we still need to satisfy the constraint that `i >= c`, and we need to do that in `O(log n)` time to get `O(log^2 n)` time total. If we searched the whole tree for elements satisfying that it would take `O(n)` time, too slow. So instead we have each node keep an ordered list of indices that it contains, so we can binary search that list in `O(log n)` time. I hope that helps. And I hope it's right! – user12861 Dec 22 '11 at 18:40
0

Perhaps I'm missing something. Doesn't this satisfy your requirements?

for (int i = c; i < n; i++)
{
    if (a < ar[i] && ar[i] < b)
        return i;
}
Fantius
  • 3,806
  • 5
  • 32
  • 49
  • That would be O(n) though, O(log n) seems to imply a binary search so I think something's missing from the question. – Chris Chilvers Dec 19 '11 at 15:51
  • Yep, that's what I missed. I just skipped right over the log part :) – Fantius Dec 19 '11 at 15:52
  • @ChrisChilvers O(log n) can also imply some kind of segment tree. There actually ***is*** something I didn't mention in the question, I'll update immediately. – Chris Dec 19 '11 at 15:54
  • @Abody97, Descending a binary tree can be considered a binary search. – Fantius Dec 19 '11 at 17:55
0

Build an array of indexes and sort them. That is, given the array [20, 0, 10, 15, 5], you would create an initial array [0, 1, 2, 3, 4]. Now you sort the array of indexes to reflect the items being in sorted order. The sorted index would be [1, 4, 2, 3, 0]. You end up with two arrays:

original = [20, 0, 10, 15, 5]
tag = [1, 4, 2, 3, 0]

You can binary search the original array through the tag array. That is, your comparison function compares original[tag[x]] and original[tag[y]]. That solves the "where is the index" problem. You can then use binary search on the segment tag[c] ... tag[n].

Seems like that should work. You'll need a stable sort so that equal numbers maintain their relative order.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • So basically, you are compressing the numbers. Since each index reflects the order of the original integer in the sorted array, the segment `tag[c]...tag[n]` isn't sorted, so how are you going to perform binary search on it? Maybe I got something wrong, please explain if possible. – Chris Dec 19 '11 at 16:01
  • @Abody97: Hmmm, you're right. Why did this work in my head? Let me ponder. – Jim Mischel Dec 19 '11 at 16:06
0

If i am reading this correctly there are n-1 queries with only c changing, In that case why dont you solve the queries backwards? first take the last query since it involves the last element of array check if the element falls beetween a and b if so store the result in a array ans[n-1]=n-1 else lets put ans[n-1]=-1, for any next query j from n-2 to 0

if a[j] is not between a and b 
     ans[j] = ans[j+1] 
else 
     ans[j] = j

this all can be done in O(n).

FUD
  • 5,114
  • 7
  • 39
  • 61