Skip to content

Latest commit

 

History

History
218 lines (175 loc) · 7.14 KB

1574.shortest-subarray-to-be-removed-to-make-array-sorted.md

File metadata and controls

218 lines (175 loc) · 7.14 KB

Test Cases

Edge / Corner Cases

  • The array is already sorted. (Important to check this condition)
Input: [1, 2, 3]
Output: 0
  • The array is in descending order.
Input: [3, 2, 1]
Output: 2

Prework

For a given array, we can breakdown into three parts:

[X X X X] [Y Y Y Y] [Z Z Z Z]
 0     i             j     n-1

To make the whole array sorted by removing Y part (may be empty), we have to satisfy the following conditions:

  1. [0:i] is non-decreasing.
  2. [j:n-1] is non-decreasing.
  3. arr[i] <= arr[j].

So the first steps is to identify the longest non-decreasing subarrays from the beginning and the end of the array. (for the conditions 1. and 2. above)

For the example input [1, 2, 3, 9, 4, 2, 3, 5], we can identify the two non-decreasing subarrays from the beginning and the end of the array:

[1, 2, 3, 9, 4, 2, 3, 5]
 ^^^^^^^^^^
          L

[1, 2, 3, 9, 4, 2, 3, 5]
                ^^^^^^^
                R

After identifying the longest non-decreasing subarray from the beginning and the end, there are 3 options to consider as the result:

  1. We remove the left part: [X, X, X, X, X, 2, 3, 5]
  2. We remove the right part: [1, 2, 3, 9, X, X, X, X]
  3. We remove the middle part and merge the two non-decreasing subarrays into one: [1, 2, 3, 9, 2, 3, 5]

We can initialize our result as the minimum of the two options 1 and 2, then we consider the third option: to merge the two subarrays into one, but it is not guaranteed that the merged array is sorted.

fun findLengthOfShortestSubarray(arr: IntArray): Int {
    var n = arr.size
    var left = 0

    // Find the longest non-decreasing subarray from the beginning
    while (left + 1 < n && arr[left] <= arr[left + 1]) left++
    
    // All are non-decreasing (it's necessary to check this condition)
    if (left == n - 1) return 0

    // Find the longest non-decreasing subarray from the end
    var right = n - 1
    while (0 <= right - 1 && arr[right - 1] <= arr[right]) right--

    // Remove left part or right part
    var result = minOf(n - left - 1, right)

    // TODO: Not finished, see below...
}

Next question is how to delete the minimum number of elements to make the merged array sorted?

1, 2, 3, 9, 2, 3, 5
     <-- L  R -->

How to move the pointers L and R?

Very nice illustration to explain how to move the pointers L and R: https://leetcode.cn/problems/shortest-subarray-to-be-removed-to-make-array-sorted/solutions/2189149/dong-hua-yi-xie-jiu-cuo-liang-chong-xie-iijwz/

Two Pointers + Binary Search

Since the R..n-1 is non-decreasing (sorted), so we can iterate all possible l in 0..L, then binary search the first r in R..n-1 that satisfies arr[l] <= arr[r] (keep sorted after merge):

[0, ... L], ..., [R, ... n-1]
l ->              r ->
0  1  2  3  4  5  6  7  // index
1, 2, 3, 9, 2, 3, 5     // value
         L  R           // the original non-decreasing subarrays
|--------|  |--------|
l           r           // l = 0 -> r = 4, to remove is 3 (2, 3, 9)
   l        r           // l = 1 -> r = 4, to remove is 2 (3, 9)
      l        r        // l = 2 -> r = 5, to remove is 1 (9, 2)
         l           r  // r is out of range, to remove is 3 (2, 3, 5)
fun findLengthOfShortestSubarray(arr: IntArray): Int {
    // ... see above

    for (l in 0..left) {
        // right is monotonic increasing, so we don't start from original right index
        right = search(arr, arr[l], right)
        result = minOf(result, right - l - 1)
    }
    return result
}

private fun search(arr: IntArray, target: Int, startIndex: Int): Int {
    var left = startIndex
    var right = arr.size - 1
    while (left <= right) {
        val middle = left + (right - left) / 2
        if (target <= arr[middle]) {
            right = middle - 1
        } else {
            left = middle + 1
        }
    }
    return left
}
  • Time Complexity: O(n log n)
  • Space Complexity: O(1)

Two Pointers

We can optimize the above solution based on the same idea (to find the first r that satisfies arr[l] <= arr[r]) by using two pointers approach.

Because 0..L and R..n-1 are both non-decreasing, as we find the first r that satisfies arr[l] <= arr[r] in the first iteration, then we can start from the previous r to find the next r that satisfies arr[l] <= arr[r], we don't need to start from R again.

1, 3, 8, 9, 2, 3, 5, 6, 7, 8
         L  R         
|--------|  |--------------|
l           r1                  // first iteration
   l         ->r2               // second iteration, r starts from the previous r
      l        r2 -------> r3   // third iteration
         l                 r3 -> 

As we move l and r in the same direction, rather than starting from R again, we can find the next r that satisfies arr[l] <= arr[r] in O(n) time complexity.

因为i的移动必须保证 [0:i] 单调增,所以 arr[i] 会变大,所以 arr[j] 必然也要增大以保证 arr[j]>=arr[i]。我们还发现 [j:n-1] 也是单调增的区间,所以我们必然会将 j 右移。这样我们会找到下一个合适的j,使得此时的 {i,j} 成为一组合适的解。

同理的分析,我们每向右移动一次i指针,必然也必须向右移动 j 指针以寻找合适的 {i,j}。这就是一个双指针的模式。所以我们用 o(n) 的时间复杂度就可以探索到所有合适的 {i,j}.

fun findLengthOfShortestSubarray(arr: IntArray): Int {
    // ... see above

    var l = 0
    var r = right
    while (l <= left && r < n) {
        if (arr[l] <= arr[r]) {
            result = minOf(result, r - l - 1)
            l++
            // We don't update r here, r should be the previous r
        } else {
            r++
        }
    }
    return result
}
  • Time Complexity: O(n)
  • Space Complexity: O(1)

WA

[1,2,3,10,0,7,8,9] should return 2, but my code returns 4.

fun findLengthOfShortestSubarray(arr: IntArray): Int {
    var n = arr.size
    var left = 0
    while (left + 1 < n && arr[left] <= arr[left + 1]) left++
    
    // All are non-decreasing
    if (left == n - 1) return 0

    var right = n - 1
    while (0 <= right - 1 && arr[right - 1] <= arr[right]) right--

    var result = Int.MAX_VALUE
    val originalRight = right

    /**
    0  1  2   3  4  5  6  7  8
    1, 2, 3, 10, 4, 2, 3, 5
              L     R

    Fix L, search the first R that satisfies arr[L] <= arr[R]
    1, 2, 3, 10, 4, 2, 3, 5
              L              R
        */
    while (right < n && arr[left] > arr[right]) right++
    result = minOf(result, right - left - 1)

    /**     
    0  1  2   3  4  5  6  7  8
    1, 2, 3, 10, 4, 2, 3, 5
              L     R

    Fix R, search the first L that satisfies arr[L] <= arr[R]
    1, 2, 3, 10, 4, 2, 3, 5
       L            R
        */
    right = originalRight
    while (left >= 0 && arr[left] > arr[right]) left--
    result = minOf(result, right - left - 1)

    return if (result == Int.MAX_VALUE) 0 else result
}