Skip to content

Commit

Permalink
feat: $1658
Browse files Browse the repository at this point in the history
  • Loading branch information
lucifer committed Dec 8, 2020
1 parent 9aaa7d4 commit edd547b
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 6 deletions.
97 changes: 97 additions & 0 deletions 91/binary-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,103 @@ function binarySearchRight(nums, target) {

```

### 寻找最左插入位置

上面我们讲了`寻找最左满足条件的值`。如果找不到,就返回 -1。那如果我想让你找不到不是返回 -1,而是应该插入的位置,使得插入之后列表仍然有序呢?

比如一个数组 nums: [1,3,4],target 是 2。我们应该将其插入(注意不是真的插入)的位置是索引 1 的位置,即 [1,**2**,3,4]。因此`寻找最左插入位置`应该返回 1,而`寻找最左满足条件` 应该返回-1。

另外如果有多个满足条件的值,我们返回最左侧的。 比如一个数组 nums: [1,2,2,2,3,4],target 是 2,我们应该插入的位置是 1。

不管是寻找最左插入位置还是后面的寻找最右插入位置,我们的更新指针代码都是一样的。即:

```
l = mid + 1
# or
r = mid
```

当然也有别的方式(比如 mid 不是向下取整,而是向上取整), 但是这样可以最小化记忆成本。

#### 思维框架

- 首先定义搜索区间为 [left, right],注意是左右都闭合,之后会用到这个点。

> 你可以定义别的搜索区间形式,不过后面的代码也相应要调整,感兴趣的可以试试别的搜索区间。
- 由于我们定义的搜索区间为 [left, right],因此当 left <= right 的时候,搜索区间都不为空。 但由于上面提到了更新条件有一个`r = mid`,因此如果结束条件是 left <= right 则会死循环。因此结束条件是 left < right。

> 有的人有疑问,这样设置结束条件会不会漏过正确的解,其实不会。举个例子容易明白一点。 比如对于区间 [4,4],其包含了一个元素 4,搜索区间不为空。如果我们的答案恰好是 4,会被错过么?不会,因为我们直接返回了 4。
- 循环体内,我们不断计算 mid ,并将 nums[mid] 与 目标值比对。
- 如果 nums[mid] 大于等于目标值, r 也可能是目标解,r - 1 可能会错过解,因此我们使用 r = mid。
- 如果 nums[mid] 小于目标值, mid 以及 mid 左侧都不可能是解,因此我们使用 l = mid + 1。
- 最后直接返回 l 或者 r 即可。(并且不需要像`最左满足条件的值`那样判断了)

#### 代码模板

##### Python

```py
def bisect_left(nums, x):
# 内置 api
bisect.bisect_left(nums, x)
# 手写
l, r = 0, len(nums) - 1
while l < r:
mid = (l + r) // 2
if nums[mid] < x:
l = mid + 1
else:
r = mid
# 由于 l 和 r 相等,因此返回谁都无所谓。
return l
```

其他语言暂时空缺,欢迎 [PR](https://github.com/azl397985856/leetcode-cheat/issues/4)

### 寻找最右插入位置

#### 思维框架

`寻找最左插入位置`类似。不同的地方在于:如果有多个满足条件的值,我们返回最右侧的。 比如一个数组 nums: [1,2,2,2,3,4],target 是 2,我们应该插入的位置是 4。

- 首先定义搜索区间为 [left, right],注意是左右都闭合,之后会用到这个点。

> 你可以定义别的搜索区间形式,不过后面的代码也相应要调整,感兴趣的可以试试别的搜索区间。
- 由于我们定义的搜索区间为 [left, right],因此当 left <= right 的时候,搜索区间都不为空。 但由于上面提到了更新条件有一个`r = mid`,因此如果结束条件是 left <= right 则会死循环。因此结束条件是 left < right。

> 有的人有疑问,这样设置结束条件会不会漏过正确的解,其实不会。举个例子容易明白一点。 比如对于区间 [4,4],其包含了一个元素 4,搜索区间不为空。如果我们的答案恰好是 4,会被错过么?不会,因为我们直接返回了 4。
- 循环体内,我们不断计算 mid ,并将 nums[mid] 与 目标值比对。
- 如果 nums[mid] 小于等于目标值, mid 以及 mid 左侧都不可能是解,因此我们使用 l = mid + 1。
- 如果 nums[mid] 大于目标值, r 也可能是目标解,r - 1 可能会错过解,因此我们使用 r = mid。
- 最后直接返回 l 或者 r 即可。(并且不需要像`最左满足条件的值`那样判断了)

#### 代码模板

##### Python

```py

def bisect_right(nums, x):
# 内置 api
bisect.bisect_right(nums, x)
# 手写
l, r = 0, len(nums) - 1
while l < r:
mid = (l + r) // 2
if nums[mid] > x:
r = mid
else:
l = mid + 1
# 由于 l 和 r 相等,因此返回谁都无所谓。
return l
```

其他语言暂时空缺,欢迎 [PR](https://github.com/azl397985856/leetcode-cheat/issues/4)

### 局部有序(先降后升或先升后降)

LeetCode 有原题 [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/)[81. 搜索旋转排序数组 II](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/), 我们直接拿过来讲解好了。
Expand Down
7 changes: 4 additions & 3 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
* [一文看懂《最大子序列和问题》](./selected/LSS.md)


* [第四章 - 高频考题(简单 73 题)](collections/easy.md)
* [第四章 - 高频考题](collections/easy.md)
* [面试题 17.12. BiNode](problems/binode-lcci.md)
* [0001. 两数之和](problems/1.two-sum.md)
* [0020. 有效的括号](problems/20.valid-parentheses.md)
Expand Down Expand Up @@ -87,7 +87,7 @@
* [1332. 删除回文子序列](problems/1332.remove-palindromic-subsequences.md)


* [第五章 - 高频考题(中等 118 题)](collections/medium.md)
* [第五章 - 高频考题](collections/medium.md)
* [0002. 两数相加](problems/2.add-two-numbers.md)
* [0003. 无重复字符的最长子串](problems/3.longest-substring-without-repeating-characters.md)
* [0005. 最长回文子串](problems/5.longest-palindromic-substring.md)
Expand Down Expand Up @@ -206,9 +206,10 @@
* [1381. 设计一个支持增量操作的栈](../problems/1381.design-a-stack-with-increment-operation.md) 91
* [1558. 得到目标数组的最少函数调用次数](../problems/1558.minimum-numbers-of-function-calls-to-make-target-array.md)
* [1631. 最小体力消耗路径](problems/1631.path-with-minimum-effort.md)
* [1658. 将 x 减到 0 的最小操作数](problems/1658.minimum-operations-to-reduce-x-to-zero.md)


* [第六章 - 高频考题(困难 32 题)](collections/hard.md)
* [第六章 - 高频考题](collections/hard.md)
* [0004. 寻找两个正序数组的中位数](problems/4.median-of-two-sorted-arrays.md)
* [0023. 合并K个升序链表](problems/23.merge-k-sorted-lists.md)
* [0025. K 个一组翻转链表](problems/25.reverse-nodes-in-k-groups.md)
Expand Down
1 change: 1 addition & 0 deletions collections/medium.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,4 @@
- [1381. 设计一个支持增量操作的栈](../problems/1381.design-a-stack-with-increment-operation.md) 91
- [1558. 得到目标数组的最少函数调用次数](../problems/1558.minimum-numbers-of-function-calls-to-make-target-array.md) 🆕
- [1631.path-with-minimum-effort](../problems/1631.path-with-minimum-effort.md) 🆕
- [1658. 将 x 减到 0 的最小操作数](../problems/1658.minimum-operations-to-reduce-x-to-zero.md) 🆕
163 changes: 163 additions & 0 deletions problems/1658.minimum-operations-to-reduce-x-to-zero.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# 题目地址(1658. 将 x 减到 0 的最小操作数)

https://leetcode-cn.com/problems/minimum-operations-to-reduce-x-to-zero

## 题目描述

```
给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。
如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1 。
 
示例 1:
输入:nums = [1,1,4,2,3], x = 5
输出:2
解释:最佳解决方案是移除后两个元素,将 x 减到 0 。
示例 2:
输入:nums = [5,6,7,8,9], x = 4
输出:-1
示例 3:
输入:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
 
提示:
1 <= nums.length <= 10^5
1 <= nums[i] <= 104
1 <= x <= 109
```

## 前置知识

-
- [滑动窗口](../thinkings/slide-window.md)

## 公司

- 暂无

##

### 思路

这里可以使用堆来解决。具体来说是我自己总结的**多路归并**题型。

> 关于这个算法套路,请期待后续的堆专题。
### 代码

代码支持:Python3

Python3 Code:

```py
class Solution:
def minOperations(self, nums: List[int], x: int) -> int:
# 看数据范围,这种方法铁定超时(指数复杂度)
h = [(0, 0, len(nums) - 1, x)]
while h:
moves,l,r,remain = heapq.heappop(h)
if remain == 0: return moves
if l + 1 < len(nums): heapq.heappush(h, (moves + 1, l + 1,r, remain-nums[l]))
if r > 0: heapq.heappush(h, (moves + 1, l,r-1, remain-nums[r]))
return -1

```

**复杂度分析**

- 时间复杂度:$O(2^moves)$,其中 moves 为题目答案。最坏情况 moves 和 N 同阶,也就是 $2^N$。
- 空间复杂度:$O(1)$。

由于题目数组长度最大可以达到 10^5, 这提示我们此方法必然超时。

我们必须考虑时间复杂度更加优秀的方式。

## 动态规划(记忆化递归)

### 思路

由上面的解法, 我们不难想到使用动态规划来解决。

枚举所有的 l,r,x 组合,并找到最小的,其中 l 表示 左指针, r 表示右指针,x 表示剩余的数字。这里为了书写简单我使用了记忆化递归。

### 代码

代码支持:Python3

Python3 Code:

> Python 的 @lru_cache 是缓存计算结果的数据结构, None 表示不限制容量。
```py
class Solution:
def minOperations(self, nums: List[int], x: int) -> int:
n = len(nums)

@lru_cache(None)
def dp(l, r, x):
if x == 0:
return 0
if x < 0 or r < 0 or l > len(nums) - 1:
return n + 1
return 1 + min(dp(l + 1, r, x - nums[l]), dp(l, r - 1, x - nums[r]))

ans = dp(0, len(nums) - 1, x)
return -1 if ans > n else ans
```

**复杂度分析**

- 时间复杂度:$O(N^2 * h)$,其中 N 为数组长度, h 为 x 的减少速度,最坏的情况可以达到三次方的复杂度。
- 空间复杂度:$O(N)$,其中 N 为数组长度,这里的空间指的是递归栈的开销。

这种复杂度仍然无法通过 10^5 规模,需要继续优化算法。

## 滑动窗口

### 思路

实际上,我们也可以逆向思考。即:我们剩下的数组一定是原数组的中间部分。

那是不是就是说,我们只要知道数据中子序和等于 sum(nums) - x 的长度。用 nums 的长度减去它就好了?

由于我们的目标是`最小操作数`,因此我们只要求**和为定值的最长子序列**,这是一个典型的[滑动窗口问题](../thinkings/slide-window.md)

### 代码

代码支持:Python3

Python3 Code:

```py
class Solution:
def minOperations(self, nums: List[int], x: int) -> int:
# 逆向求解,滑动窗口
i = 0
target = sum(nums) - x
win = 0
ans = len(nums)
if target == 0: return ans
for j in range(len(nums)):
win += nums[j]
while i < j and win > target:
win -= nums[i]
i += 1
if win == target:
ans = min(ans, len(nums) - (j - i + 1))
return -1 if ans == len(nums) else ans

```

**复杂度分析**

- 时间复杂度:$O(N)$,其中 N 为数组长度。
- 空间复杂度:$O(1)$。
25 changes: 24 additions & 1 deletion selected/LIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,32 @@ class Solution:
- 时间复杂度:$$O(N ^ 2)$$
- 空间复杂度:$$O(N)$$

## 优化

大家想看效率高的,其实也不难。 LIS 也可以用 **贪心 + 二分** 达到不错的效率。代码如下:

![](https://tva1.sinaimg.cn/large/0081Kckwly1gl6ajh887vj31zc0gmae6.jpg)

代码文字版如下:

```py
class Solution:
def lengthOfLIS(self, A: List[int]) -> int:
d = []
for a in A:
i = bisect.bisect_left(d, a)
if i < len(d):
d[i] = a
elif not d or d[-1] < a:
d.append(a)
return len(d)
```

## More

其他的我就不一一说了。比如 [673. 最长递增子序列的个数](https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/) (滴滴面试题)。 不就是求出最长序列,之后再循环比对一次就可以得出答案了么?
其他的我就不一一说了。

比如 [673. 最长递增子序列的个数](https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/) (滴滴面试题)。 不就是求出最长序列,之后再循环比对一次就可以得出答案了么?

[491. 递增子序列](https://leetcode-cn.com/problems/increasing-subsequences/) 由于需要找到所有的递增子序列,因此动态规划就不行了,妥妥回溯就行了,套一个模板就出来了。回溯的模板可以看我之前写的[回溯专题](https://github.com/azl397985856/leetcode/blob/master/problems/90.subsets-ii.md "回溯专题")

Expand Down
Loading

0 comments on commit edd547b

Please sign in to comment.