Skip to content

Commit

Permalink
二分法的二度理解
Browse files Browse the repository at this point in the history
  • Loading branch information
yk committed Feb 19, 2024
1 parent 31db1b7 commit 51883fc
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 33 deletions.
53 changes: 30 additions & 23 deletions content/posts/algorithm/data structure/数组-前缀和数组.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ series: [data structure, trick]
categories: [algorithm]
---

## 概念
### 概念

应用场景:在原始数组不会被修改的情况下,**快速、频繁查询某个区间的累加和**

Expand Down Expand Up @@ -215,40 +215,47 @@ var findMaxLength = function (nums) {

### [lc.528 按权重随机选择](https://leetcode.cn/problems/random-pick-with-weight/)

第一次碰见道题大概率是没读懂它到底是个啥意思的 😂,看了题解后,豁然开朗(怎么每次都是这种感觉,f\*\*k!)

首先就是前缀和 `[0...arr.length-1]` 是总的前缀和,把这个总前缀和看成是一把尺子,那么每个数字的权重可以看成是在这把尺子上占用的长度。由此,可以得到一个重要的特点:**每个长度区间的右边界是当前数字 i 的前缀和,左边界是上一个区间的前缀和右边界 + 1**

例如 w=[3,1,2,4]时,权重之和 total=10,那么我们按照 [1,3],[4,4],[5,6],[7,10][1,10] 进行划分,使得它们的长度恰好依次为 3,1,2,4。

```JavaScript
/**
* @param {number[]} w
*/
var Solution = function (w) {
// 前缀和,然后随机数,看看落在前缀和的哪个区间,这样就保证了源数据的随机性带上了权重,当然需要配合二叉搜索左边界
this.presum = new Array(w.length + 1)
this.presum[0] = 0
for (let i = 1; i < this.presum.length; ++i) {
this.presum[i] = this.presum[i - 1] + w[i - 1]
this.preSum = [0]
for(let i = 0; i < w.length; ++i) {
this.preSum[i + 1] = this.preSum[i] + w[i]
}
}

/**
* @return {number}
*/
Solution.prototype.pickIndex = function () {
const x = Math.floor(Math.random() * this.presum[this.presum.length - 1]) + 1
return findLeftIndex(this.presum, x) - 1 // 前缀和的索引比原数组的索引都大1, 所以减掉
}

function findLeftIndex(presum, target) {
let l = 1,
r = presum.length // 前缀和首尾是占位0, 所以从1开始到最后一个索引结束(这里采用左闭右开)
while (l < r) {
const mid = l + ((r - l) >> 1)
if (presum[mid] < target) {
l = mid + 1
} else {
r = mid
Solution.prototype.pickIndex = function() {
// 随机数 randomX 应该落在 pre[i] >= randomX >= pre[i] - w[i] + 1
const randomX = Math.random() * (this.preSum[this.preSum.length - 1]) + 1 | 0
// 又因为 pre[i] 是单调递增的,那么 pre[i] >= randomX 转化为了一个二分搜索左边界的问题了
const binarySearchlow = (x) => {
let low = 1, high = this.preSum.length
while(low < high) {
const mid = low + (high - low >> 1)
if(this.preSum[mid] < x) { // target > mid 接着去搜索右边,low 进化
low = mid + 1
}else if(this.preSum[mid] > x){ // target < mid 接着去搜索左边,high 进化
high = mid
}else if(this.preSum[mid] === x) { // target === mid, 因为是搜索左边界,所以排除 mid, high = mid (牢记可行解区间为 [low...high))
high = mid
}
}
return low
}
}
return l
}

return binarySearchlow(randomX) - 1 // 我们定义的前缀和的索引比原数组索引大 1,所以要减去 1,按照官解定义的前缀和就不需要了
};
```

### [lc.560 和为 k 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/)
Expand Down
38 changes: 28 additions & 10 deletions content/posts/algorithm/trick/二分法.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,38 @@ title: '二分法'
date: 2023-01-03T11:24:54+08:00
---

其实是很容易理解的一种算法理论,从小学我们就懂,举个例子:考试,你去猜同学考了多少分,然后他只会告诉你猜的分数相比实际分数是高了还是低了,怎么猜最快得到答案?满分 100 分,如果他考了 80 分,假设他考所有分数的概率是一样的(忽略真是水平哈哈),那么一定是先猜 50 分,然后 75,87,81,80(命中)。
## 二分法适用条件

那么在算法中,我们针对的往往是具有**单调性**的(有序)场景,采用二分法逼近,能较快的提升查找速度,只不过二分法想要用好,一定要对**区间****边界条件**敏感
数据需要具有**单调性**,即有序

用以下题目,来感受一下 👻
## 重点理解!!!

## 旋转数组系列
### 循环不变式

- `while low <= high`

1. 终止条件,一定是 `low + 1 == high`,也即 low > right
2. 意味着,整个区间 `[low..high]` 里的每个元素都被遍历过了

- `while low < high`

1. 终止条件,一定是 `low == high`
2. 意味着,整个区间 `[low..high]` 里当 low == high 时的元素可能会没有被遍历到,需要打个补丁

### 可行解区间

对于二分搜索过程中的每一次循环,它的可行解区间都应当一致,结合对于循环不变式的理解:

把这个系列放到前面,是为了更好的理解 **循环不变式****区间**
- `[low...high]`,左右都闭区间,一般根据 mid 就能判断下一个搜索区间是 `low = mid + 1` 还是 `high = mid - 1`
- `[low...high)`,左闭右开区间,维持可行解区间,下一个搜索区间左边是 `low = mid + 1`,右边是 `high = mid`

因为数组的特性,常用的就是这两种区间。当然也有左右都开的区间 `(low...high)`,对应的循环不变式为 `while low + 1 < high`,不过比较少见。

> 另外请务必理解可行解区间到底是个啥!不是说定义了指针为 `low = 0, high = len - 1`,就代表着可行解区间为 `[low...high]`,而是需要看实际题意。比如,你能确定 low = 0 指针和 high = len - 1 指针的解一定不在我需要的结果之中,那么我的可行解区间就是 `(low...high)`,对应的就可以使用 `while low + 1 < high` 的循环不变式
> 对于寻找左右边界的问题,也是根据可行解区间,去决定 low 或 high 的每一轮 update。
## 旋转数组系列

### [153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/description/)

Expand Down Expand Up @@ -342,8 +365,3 @@ TODO
- 如果是 `<=`,结束条件是 left > right,注意 📢:这里就会有问题了,是 left 先越过 right 还是 right 先越过 left,这个是要搞清楚的,题目不同,也就不一样,但是会影响我们的结果,有时候会产生越界问题

第三点,在二分搜索中,while 循环时的循环条件称为**循环不变式**,它对应的区间称之为**循环不变量****_就是在 while 寻找中每一次边界的处理都要坚持根据区间的定义来操作_**,这直接决定了我们的逼近策略,very important!

## 参考

- [labuladong](https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-48c1d/wo-xie-le--9c7a4/)
- [代码随想录](https://programmercarl.com/0704.%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.html#%E6%80%9D%E8%B7%AF)

0 comments on commit 51883fc

Please sign in to comment.