From f400a3c85cc021781f1f88988e74c9e9a33fb7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BD=20Xu=C3=A2n=20Sang?= Date: Sun, 30 Aug 2026 07:29:43 +0700 Subject: [PATCH] add 2091 solution --- .../description.md | 55 ++++ .../solution.md | 235 ++++++++++++++++++ README.md | 5 +- SUMMARY.md | 1 + _sidebar.md | 1 + 5 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 Medium/2091.Removing-Minimum-and-Maximum-From-Array/description.md create mode 100644 Medium/2091.Removing-Minimum-and-Maximum-From-Array/solution.md diff --git a/Medium/2091.Removing-Minimum-and-Maximum-From-Array/description.md b/Medium/2091.Removing-Minimum-and-Maximum-From-Array/description.md new file mode 100644 index 0000000..2fee760 --- /dev/null +++ b/Medium/2091.Removing-Minimum-and-Maximum-From-Array/description.md @@ -0,0 +1,55 @@ +# 2091. Removing Minimum and Maximum From Array + +You are given a **0-indexed** array of **distinct** integers `nums`. + +There is an element in `nums` that has the **lowest** value and an element that +has the **highest** value. We call them the **minimum** and **maximum** +respectively. Your goal is to remove **both** these elements from the array. + +A **deletion** is defined as either removing an element from the **front** of the +array or removing an element from the **back** of the array. + +Return the **minimum** number of deletions it would take to remove **both** the +minimum and maximum element from the array. + +## Example 1 + +```text +Input: nums = [2,10,7,5,4,1,8,6] +Output: 5 +Explanation: +The minimum element in the array is nums[5], which is 1. +The maximum element in the array is nums[1], which is 10. +We can remove both the minimum and maximum by removing 2 elements from the front +and 3 elements from the back. +This results in 2 + 3 = 5 deletions, which is the minimum number possible. +``` + +## Example 2 + +```text +Input: nums = [0,-4,19,1,8,-2,-3,5] +Output: 3 +Explanation: +The minimum element in the array is nums[1], which is -4. +The maximum element in the array is nums[2], which is 19. +We can remove both the minimum and maximum by removing 3 elements from the front. +This results in only 3 deletions, which is the minimum number possible. +``` + +## Example 3 + +```text +Input: nums = [101] +Output: 1 +Explanation: +There is only one element in the array, which makes it both the minimum and +maximum element. +We can remove it with 1 deletion. +``` + +## Constraints + +- `1 <= nums.length <= 10^5` +- `-10^5 <= nums[i] <= 10^5` +- The integers in `nums` are **distinct**. diff --git a/Medium/2091.Removing-Minimum-and-Maximum-From-Array/solution.md b/Medium/2091.Removing-Minimum-and-Maximum-From-Array/solution.md new file mode 100644 index 0000000..91dbb20 --- /dev/null +++ b/Medium/2091.Removing-Minimum-and-Maximum-From-Array/solution.md @@ -0,0 +1,235 @@ +# Intuition + +Deletions only ever happen at the two ends, so whatever survives is always a +contiguous middle stretch of the original array. That means the *values* in `nums` +are irrelevant beyond one fact: **where the minimum and the maximum sit**. Once +those two positions are known, the array might as well be a row of `n` boxes with +two of them marked. + +Removing both marked boxes from the ends can only be done in three shapes, and the +answer is simply the cheapest of the three. + +# Approach: Three Cases on Two Positions + +Find the index of the minimum and the index of the maximum, then sort them into + +- `left` — the earlier of the two positions, +- `right` — the later one. + +Every valid deletion plan must reach *both*, and there are only three ways to do +that. + +| plan | what it deletes | cost | +| --- | --- | --- | +| **From the front only** | everything up to and including `right` | `right + 1` | +| **From the back only** | everything from `left` to the end | `n - left` | +| **From both ends** | the prefix through `left`, and the suffix from `right` | `(left + 1) + (n - right)` | + +The answer is the minimum of the three. + +## Why those three exhaust the possibilities + +Deleting `f` elements from the front and `b` from the back removes exactly the +index ranges `[0, f)` and `[n - b, n)`. To remove both marked positions, each of +`left` and `right` must fall into one of those ranges. Since `left ≤ right`: + +- If both fall in the **front** range, the front must extend past `right`, giving + `f ≥ right + 1`. Cheapest is the first row. +- If both fall in the **back** range, the back must extend before `left`, giving + `b ≥ n - left`. Cheapest is the second row. +- Otherwise they are **split**: the only workable split is `left` from the front + and `right` from the back, because `left ≤ right` — taking `right` from the + front while `left` came from the back would mean the two ranges overlap and the + whole array is deleted anyway. That gives `f ≥ left + 1` and `b ≥ n - right`, + the third row. + +No other assignment exists, so the minimum over these three is the true optimum. + +## Why the minimum and maximum are found without sorting + +Both indices come from one linear scan. The Go and Rust versions carry +`maxIndex` and `minIndex` and update each when a strictly better value appears; +the Python version calls `nums.index(min(nums))` and `nums.index(max(nums))`, +which is three passes but still linear. + +Because the problem guarantees **distinct** integers, `nums.index(...)` is +unambiguous — there is exactly one position holding the minimum, and one holding +the maximum. With duplicates allowed, `index` would return the first occurrence, +which is in fact still what you want here, but the guarantee makes it moot. + +## The `n <= 2` early return is redundant + +Go and Rust return `n` immediately when `n <= 2`; Python has no such guard. Both +are correct, because the general formula already produces the right answer at +those sizes: + +- **`n = 1`** — one element is both the minimum and the maximum, so + `left = right = 0`. The three costs are `1`, `1`, and `2`, and the minimum is + `1`. +- **`n = 2`** — the two marks land on indices `0` and `1`, so `left = 0`, + `right = 1`. The costs are `2`, `2`, and `2`, and the answer is `2`. + +Removing the guard from the Rust version and re-running the full test corpus +produced identical answers on every case, confirming it is a shortcut rather than +a correctness fix. + +## No underflow in the unsigned arithmetic + +Rust computes with `usize`, where subtracting past zero panics in debug builds and +wraps in release. Both subtractions are safe: + +- `n - left` — since `left ≤ n - 1`, the result is at least `1`. +- `left + 1 + n - right` — evaluated left to right, so the running total reaches + `left + 1 + n` before anything is subtracted, and that is `≥ right` because + `right ≤ n - 1`. + +The whole corpus was run through a debug build, where an underflow would abort, +and none occurred. + +# Worked examples + +## `nums = [2,10,7,5,4,1,8,6]` → `5` + +Minimum `1` sits at index `5`; maximum `10` sits at index `1`. So `left = 1` and +`right = 5`, with `n = 8`. + +| plan | cost | detail | +| --- | --- | --- | +| front only | `5 + 1 = 6` | delete indices `0..5` | +| back only | `8 - 1 = 7` | delete indices `1..7` | +| both ends | `2 + 3 = 5` | front through index `1`, back from index `5` | + +The answer is `5`, matching the statement's "2 from the front and 3 from the +back". + +## `nums = [0,-4,19,1,8,-2,-3,5]` → `3` + +Minimum `-4` at index `1`, maximum `19` at index `2`, so `left = 1`, `right = 2`. + +| plan | cost | +| --- | --- | +| front only | `2 + 1 = 3` | +| back only | `8 - 1 = 7` | +| both ends | `2 + 6 = 8` | + +Here the two marks sit next to each other near the front, so sweeping in from one +side beats splitting the work — the opposite of the previous example. + +## `nums = [1,2,3,4,5]` → `2` + +Minimum at index `0`, maximum at index `4` — the two extremes are already at the +two ends, so `left = 0` and `right = 4`. + +| plan | cost | +| --- | --- | +| front only | `5` | +| back only | `5` | +| both ends | `1 + 1 = 2` | + +This is the case where the split plan wins by the widest margin: one deletion from +each end. + +## `nums = [101]` → `1` + +A single element is both the minimum and the maximum, so `left = right = 0` and +the costs are `1`, `1`, `2`. One deletion suffices. + +# Complexity + +- Time complexity: $$O(n)$$ — one pass to locate both extremes, then constant + work. Python makes three passes (`min`, `max`, and two `index` scans), which is + still $$O(n)$$. +- Space complexity: $$O(1)$$ — two indices and three candidate costs. + +Sorting to find the extremes would cost $$O(n \log n)$$ and also destroy the +positional information the whole approach depends on. + +# Code + +## Go + +```go +func minimumDeletions(nums []int) int { + n := len(nums) + if n <= 2 { + return n + } + maxIndex, minIndex := 0, 0 + for i, num := range nums { + if nums[maxIndex] < num { + maxIndex = i + } + if nums[minIndex] > num { + minIndex = i + } + } + left, right := min(maxIndex, minIndex), max(maxIndex, minIndex) + + return min(right + 1, n - left, left + 1 + n - right) +} +``` + +The builtin `min` and `max` need **Go 1.21**, where they became generic over +ordered types. `min` is variadic, so the three candidate costs go in one call. + +## Rust + +```rust +impl Solution { + pub fn minimum_deletions(nums: Vec) -> i32 { + let n = nums.len(); + if n <= 2 { + return n as i32; + } + let (mut max_index, mut min_index) = (0, 0); + for i in 0..n { + if nums[max_index] < nums[i] { + max_index = i; + } + if nums[min_index] > nums[i] { + min_index = i; + } + } + let (left, right) = ((max_index).min(min_index), max_index.max(min_index)); + (right + 1).min(n - left).min(left + 1 + n - right) as i32 + } +} +``` + +`Ord::min` chains rather than taking a list, so the three-way minimum is written +as two calls. The single `as i32` at the end converts the final `usize`, keeping +all the intermediate arithmetic in the unsigned domain analysed above. + +## Python + +```python +class Solution: + def minimumDeletions(self, nums: List[int]) -> int: + min_index = nums.index(min(nums)) + max_index = nums.index(max(nums)) + left, right = min(max_index, min_index), max(max_index, min_index) + n = len(nums) + return min(right + 1, n - left, left + 1 + n - right) +``` + +Note that `min` is doing two different jobs here: `min(nums)` finds the smallest +*value*, while `min(right + 1, ...)` picks the cheapest *plan*. Python's `min` is +variadic like Go's, so the final line mirrors the Go one exactly. + +# Test cases + +| `nums` | `left`, `right` | costs (front, back, both) | answer | +| --- | --- | --- | --- | +| `[2,10,7,5,4,1,8,6]` | `1`, `5` | `6`, `7`, `5` | `5` | +| `[0,-4,19,1,8,-2,-3,5]` | `1`, `2` | `3`, `7`, `8` | `3` | +| `[101]` | `0`, `0` | `1`, `1`, `2` | `1` | +| `[1,2,3,4,5]` | `0`, `4` | `5`, `5`, `2` | `2` | +| `[5,9]` | `0`, `1` | `2`, `2`, `2` | `2` | +| `[9,5]` | `0`, `1` | `2`, `2`, `2` | `2` | + +All three implementations were checked against a brute force that tries every +`(front, back)` deletion split and keeps the cheapest one that removes both +extremes. The corpus was **8916** cases: the three examples, every permutation of +distinct values for `n` from 1 to 7, and 3000 random arrays of distinct integers. +Go, Rust and Python agreed on every case, with zero mismatches against the +reference. diff --git a/README.md b/README.md index 1bf680d..4e0b223 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Easy/350.Intersection-of-Two-Arrays-II/ ## Solutions index -Total: **206** problems with at least one solution file. +Total: **207** problems with at least one solution file. Solution links use variant names when multiple approaches or languages exist (`main` = `solution.md`, others = `solution-.md`). @@ -81,7 +81,7 @@ Solution links use variant names when multiple approaches or languages exist (`m | 3731. Find Missing Elements | [Link](https://leetcode.com/problems/find-missing-elements/) | [main](Easy/3731.Find-Missing-Elements/solution.md) | | 3754. Concatenate Non-Zero Digits and Multiply by Sum I | [Link](https://leetcode.com/problems/concatenate-non-zero-digits-and-multiply-by-sum-i/) | [main](Easy/3754.Concatenate-Non-Zero-Digits-and-Multiply-by-Sum-I/solution.md) | -### Medium (119) +### Medium (120) | Problem | LeetCode | Solution | | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -161,6 +161,7 @@ Solution links use variant names when multiple approaches or languages exist (`m | 2028. Find Missing Observations | [Link](https://leetcode.com/problems/find-missing-observations/) | [main](Medium/2028.Find-Missing-Observations/solution.md) | | 2058. Find the Minimum and Maximum Number of Nodes Between Critical Points | [Link](https://leetcode.com/problems/find-the-minimum-and-maximum-number-of-nodes-between-critical-points/) | [main](Medium/2058.Find-the-Minimum-and-Maximum-Number-of-Nodes-Between-Critical-Points/solution.md) | | 2070. Most Beautiful Item for Each Query | [Link](https://leetcode.com/problems/most-beautiful-item-for-each-query/) | [main](Medium/2070.Most-Beautiful-Item-for-Each-Query/solution.md) | +| 2091. Removing Minimum and Maximum From Array | [Link](https://leetcode.com/problems/removing-minimum-and-maximum-from-array/) | [main](Medium/2091.Removing-Minimum-and-Maximum-From-Array/solution.md) | | 2096. Step By Step Directions From A Binary Tree Node To Another | [Link](https://leetcode.com/problems/step-by-step-directions-from-a-binary-tree-node-to-another/) | [go](Medium/2096.Step-By-Step-Directions-From-A-Binary-Tree-Node-To-Another/solution-go.md) | | 2109. Adding Spaces To A String | [Link](https://leetcode.com/problems/adding-spaces-to-a-string/) | [main](Medium/2109.Adding-Spaces-To-A-String/solution.md) | | 2161. Partition Array According to Given Pivot | [Link](https://leetcode.com/problems/partition-array-according-to-given-pivot/) | [main](Medium/2161.Partition-Array-According-to-Given-Pivot/solution.md) | diff --git a/SUMMARY.md b/SUMMARY.md index e77b143..068a13a 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -147,6 +147,7 @@ * [2028. Find Missing Observations](Medium/2028.Find-Missing-Observations/solution.md) * [2058. Find the Minimum and Maximum Number of Nodes Between Critical Points](Medium/2058.Find-the-Minimum-and-Maximum-Number-of-Nodes-Between-Critical-Points/solution.md) * [2070. Most Beautiful Item for Each Query](Medium/2070.Most-Beautiful-Item-for-Each-Query/solution.md) +* [2091. Removing Minimum and Maximum From Array](Medium/2091.Removing-Minimum-and-Maximum-From-Array/solution.md) * 2096. Step By Step Directions From A Binary Tree Node To Another * [go](Medium/2096.Step-By-Step-Directions-From-A-Binary-Tree-Node-To-Another/solution-go.md) * [2109. Adding Spaces To A String](Medium/2109.Adding-Spaces-To-A-String/solution.md) diff --git a/_sidebar.md b/_sidebar.md index fb0ae3f..b13e846 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -142,6 +142,7 @@ - [2028. Find Missing Observations](Medium/2028.Find-Missing-Observations/solution.md) - [2058. Find the Minimum and Maximum Number of Nodes Between Critical Points](Medium/2058.Find-the-Minimum-and-Maximum-Number-of-Nodes-Between-Critical-Points/solution.md) - [2070. Most Beautiful Item for Each Query](Medium/2070.Most-Beautiful-Item-for-Each-Query/solution.md) + - [2091. Removing Minimum and Maximum From Array](Medium/2091.Removing-Minimum-and-Maximum-From-Array/solution.md) - 2096. Step By Step Directions From A Binary Tree Node To Another - [go](Medium/2096.Step-By-Step-Directions-From-A-Binary-Tree-Node-To-Another/solution-go.md) - [2109. Adding Spaces To A String](Medium/2109.Adding-Spaces-To-A-String/solution.md)