6

Given an array which contains N different integers, find the longest subsequence which satisfies:

  1. the start element of the subsequence is the smallest of the subsequence.
  2. the end element of the subsequence is the largest of the subsequence.

Eg: 8,1,9,4,7. The answer is 1,4,7.

2,6,5,4,9,8. The answer is 2,6,5,4,9 or 2,6,5,4,8.

Here is a O(N^2) algorithm:

  • Let X be the array of numbers.
  • Iterate over X. Suppose we are at index i. Let Y be the array where Y[j] is the number of elements in (j, i] which are smaller than X[j]. Let z be the number of elements in [j, i] which are smaller than X[i]. If X[j] is smaller than X[i], we can get a subsequence of length z-Y[j] which satisfies the constrains.
  • Set z to 1. Loop j from i-1 down to 0.

    if X[j] < X[i]: z++; ans = max(ans, z - Y[j]); else Y[j]++;

Can we do better? I think there should be an O(NlogN) algorithm to find the max length.

Amos
  • 3,238
  • 4
  • 19
  • 41
  • [Homework](http://meta.stackexchange.com/q/10811/133817) should be marked as such, and shouldn't ask for the solution, but instead be about problems with your existing solution. – outis Apr 19 '14 at 21:38

1 Answers1

1

Let me redo the explanation of this O(n log n) algorithm.

Interpret the elements of the input sequence as points in 2D, where the x-coordinate is the index, and the y-coordinate is the value. We're looking for the rectangle containing the most input points, subject to the constraint that the lower left corner and the upper right corner be input points. Under the usual component-wise partial order, the lower left corner of the optimum rectangle is minimal, and the upper right corner is maximal.

Make two linear sweeps to find the minimal and maximal points. Create an integer-valued segment tree keyed by the former, with operations that (i) accept an interval of keys and increment/decrement the associated values and that (ii) compute the maximum value. The algorithm is to iterate left to right through the maximal points, using the segment tree to track how many input points lie between (with respect to the partial order) each minimal point and the current maximal point.

Both minimal points and maximal points go down as we move left to right. Suppose, then, that we're moving from a maximal point (x, y) to the next maximal point (x', y'). We have x < x' and y' < y. How do the values in the segment tree change? Since x < x', the points with x-coordinate in ]x, x'] do not belong to rectangles with upper right (x, y) but may belong to rectangles with upper right (x', y'). Conversely, since y' < y, the points with y-coordinate in ]y', y] may belong to rectangles with upper right (x, y) but do not belong to rectangles with upper right (x', y'). All other points are unaffected.

----+                   empty
    |
----+---------+ (x, y)
      removed |
--------------+-------+ (x', y')
              | added |
              |       +----+
              |       |    |

We go through the possibly affected points one by one, updating the segment tree. The points are given sorted by x; if we make a copy and sort by y during initialization, then we can enumerate the possibly affected points efficiently. Note that, over time, the x-intervals are pairwise disjoint, as are the y-intervals, so we can afford to spend logarithmic time on each possibly affected point. Given a point (x'', y'') such that x'' in ]x, x'] (note that y'' <= y' in this case), we need to increment the segment tree at the minimal points whose x-coordinate lies in ]inf, x''] and whose y-coordinate lies in ]inf, y'']. This may not look one-dimensional, but in fact, the ordering on x-coordinates and ordering on y-coordinates are opposite for minimal points, so this set of keys is an interval. Similarly, given a point (x''', y''') such that y''' in ]y', y] (note that x''' <= x in this case), we need to decrement the values at an interval of keys.

Here's the "magic" segment tree data structure in Java.

public class SegmentTree {
    private int n;
    private int m;
    private int[] deltaValue;
    private int[] deltaMax;

    private static int nextHighestPowerOfTwoMinusOne(int n) {
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return n;
    }

    public SegmentTree(int n) {
        this.n = n;
        m = nextHighestPowerOfTwoMinusOne(n) + 1;
        deltaValue = new int[m];
        deltaMax = new int[m];
    }

    private static int parent(int i) {
        int lob = i & -i;
        return (i | (lob << 1)) - lob;
    }

    private static int leftChild(int i) {
        int lob = i & -i;
        return i - (lob >>> 1);
    }

    private static int rightChild(int i) {
        int lob = i & -i;
        return i + (lob >>> 1);
    }

    public int get(int i) {
        if (i < 0 || i > n) {
            throw new IllegalArgumentException();
        }
        if (i == 0) {
            return 0;
        }
        int sum = 0;
        do {
            sum += deltaValue[i];
            i = parent(i);
        } while (i < m);
        return sum;
    }

    private int root() {
        return m >>> 1;
    }

    private int getMax(int i) {
        return deltaMax[i] + deltaValue[i];
    }

    public void addToSuffix(int i, int delta) {
        if (i < 1 || i > n + 1) {
            throw new IllegalArgumentException();
        }
        if (i == n + 1) {
            return;
        }
        int j = root();
        outer:
        while (true) {
            while (j < i) {
                int k = rightChild(j);
                if (k == j) {
                    break outer;
                }
                j = k;
            }
            deltaValue[j] += delta;
            do {
                int k = leftChild(j);
                if (k == j) {
                    break outer;
                }
                j = k;
            } while (j >= i);
            deltaValue[j] -= delta;
        }
        while (true) {
            j = parent(j);
            if (j >= m) {
                break;
            }
            deltaMax[j] =
                Math.max(0,
                         Math.max(getMax(leftChild(j)),
                                  getMax(rightChild(j))));
        }
    }

    public int maximum() {
        return getMax(root());
    }
}
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • I think you've supposed the subarray is continuous which actually may not be. – Amos Apr 09 '14 at 04:29
  • Thank you, David. The _starters_ and _enders_ are obvious. But it's hard to understand your definition of _candidates_ and the manner to choose them. A pseudocode would help a lot. – Amos Apr 09 '14 at 06:53
  • Thanks again! It looks like an decent answer. So the essential idea is to build a magic segment tree that each `candidates` only needs to do a single operation on it. But I don't understand how to implement this tree and I have a small doubt that the overall complexity is asymptotic to O(n log n). – Amos Apr 09 '14 at 15:35
  • @amos It's not magic. For just bulk adds in a segment tree, each segment has an associated quantity delta_value. The value of a point is the sum of delta_values of all of the segments in which it lies. Now, for each segment s, maintain a field delta_max, namely, the maximum value of a point in s minus the sum of delta_values for s and its ancestors. Then, the equation for a segment s with two children s1 and s2 is delta_max(s) = max(delta_max(s1) + delta_value(s1), delta_max(s2) + delta_value(s2)). The necessary updates to delta_max are O(log n) if you defer them until the end of a bulk add. – David Eisenstat Apr 09 '14 at 15:44
  • I spend a morning reviewing segment trees and I still don't get the idea you use to design the tree. Is there some relevant topics about this usage of segment tree? Or could you make some plots explain it in more detail? This would be very helpful. Thanks! – Amos Apr 10 '14 at 07:12