4

I'm reading this editorial here: https://www.geeksforgeeks.org/given-an-array-arr-find-the-maximum-j-i-such-that-arrj-arri/ and I cannot understand the explanation for how the O(n) solution works. The paragraph describing it seems to contradict the code. I've gone through a sample array and manually make sure this seems to work but it does not seem intuitive to me at all.

Would someone more experienced with solving programming puzzles be willing to explain how and why this works, or explain what is wrong with it?

Thank you.

(Below is text from the link above:)

Problem:

Given an array arr[], find the maximum j – i such that arr[j] > arr[i] Given an array arr[], find the maximum j – i such that arr[j] > arr[i].

Examples :

Input: {34, 8, 10, 3, 2, 80, 30, 33, 1}
Output: 6  (j = 7, i = 1)

Input: {9, 2, 3, 4, 5, 6, 7, 8, 18, 0}
Output: 8 ( j = 8, i = 0)

Method 2 (Efficient)

To solve this problem, we need to get two optimum indexes of arr[]: left index i and right index j. For an element arr[i], we do not need to consider arr[i] for left index if there is an element smaller than arr[i] on left side of arr[i]. Similarly, if there is a greater element on right side of arr[j] then we do not need to consider this j for right index. So we construct two auxiliary arrays LMin[] and RMax[] such that LMin[i] holds the smallest element on left side of arr[i] including arr[i], and RMax[j] holds the greatest element on right side of arr[j] including arr[j]. After constructing these two auxiliary arrays, we traverse both of these arrays from left to right. While traversing LMin[] and RMa[] if we see that LMin[i] is greater than RMax[j], then we must move ahead in LMin[] (or do i++) because all elements on left of LMin[i] are greater than or equal to LMin[i]. Otherwise we must move ahead in RMax[j] to look for a greater j – i value.

גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61

2 Answers2

3

It works. The code author just took a confusing shortcut.

As the editorial points out, given indices i1 < i2 with arr[i1] ≤ arr[i2], there's no point in considering i = i2. We can always do better by setting i = i1 instead, since for all j,

  1. j - i1 > j - i2, and
  2. if arr[j] > arr[i2], then the fact that arr[i2] ≥ arr[i1] implies that arr[j] > arr[i1].

Similarly, given indices j1 < j2 with arr[j1] ≤ arr[j2], there's no point in considering j = j1.

Let's examine the first sample input and eliminate all of these suboptimal candidates.

34  8  10  3  2  80  30  33  1
34  8      3  2              1  // candidate values of arr[i]
                 80      33  1  // candidate values of arr[j]

Observe that both subsequences decrease. This is a straightforward consequence of the conditions above for being a candidate.

Searching for the best i and j now involves a programming contest cliché. Let me organize the possibilities into a table. Note that the algorithm does not construct this table explicitly; it's just a visual aid.

    80  33  1
-------------
34   √
 8   √   √
 3   √   √
 2   √   √
 1   √   √

The checkmark () means that arr[i] < arr[j]. Going down means increasing i and decreasing arr[i]. Going left means decreasing j and increasing arr[j]. Hence, wherever there is a checkmark, all squares below or to the left or some combination also have checkmarks. On the other hand, given a square with a checkmark that is below/to the left of another square with a checkmark, the latter square is necessarily a better solution, because i is less or j is greater or both. Therefore we are keenly interested in the boundary between checkmarks and no checkmarks, because that's where the optimal solution lies.

The algorithm for tracing the boundary is simple. Start in the upper left square. If the current square has a checkmark, go right. Otherwise, go down. I hope that you can get a sense of how this process visits the first checkmark in each column in time O(#rows + #columns). Here's another possible table.

    33  8  7  3
---------------
34
 8   √
 3   √  √  √
 2   √  √  √  √
 1   √  √  √  √

To get to the implementation, observe that we're doing the same search but with some of the rows/columns duplicated to fill the empty space left by the non-candidates, saving us the trouble of minding the correspondence between indices. It's basically the same idea.

David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • I do not understand how this explains the O(n) solution CryptoCommander is asking for? It is (to my knowledge) not a matrix... – Aldert Jan 26 '19 at 15:08
  • @Aldert It doesn't actually construct the matrix, but that's the easiest way to show what's going on. – David Eisenstat Jan 26 '19 at 15:14
0

It's the magic of monotonic functions and the kind of insight one can get by visualising a problem's solution space and how those values align themselves. Let's plot one of the examples in the page you linked to; array indexes on the x-axis, LMin and RMax on the y-axis:

  {34, 8,10, 3, 2,80,30,33, 1}
    0  1  2  3  4  5  6  7  8

80  r  r  r  r  r  r
 .
34  l
33                    r  r
 .                       ^
30
 .
10
 . 
 8     l  l
 .     ^
 3           l
 2              l  l  l  l
 1                         lr
    0  1  2  3  4  5  6  7  8

An r value is not (necessarily) the value associated with the array index; rather, it's an indication of how far we can extend an interval to the right, guaranteeing no r on the right would be greater (meaning there might be a larger interval we'd miss). Similarly, an l is an indication that we are on the lowest left-ward value, and since we first explore moving along the rs, we are guaranteed not to have missed an earlier l for any interval in our search. Clearly, we iterate from left to right at most along all rs and all ls, so O(n).

גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61