From 51883fcb525457ed14b3585cad55fedf10300bb6 Mon Sep 17 00:00:00 2001 From: yk Date: Tue, 20 Feb 2024 00:11:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E5=88=86=E6=B3=95=E7=9A=84=E4=BA=8C?= =?UTF-8?q?=E5=BA=A6=E7=90=86=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...00\345\222\214\346\225\260\347\273\204.md" | 53 +++++++++++-------- .../\344\272\214\345\210\206\346\263\225.md" | 38 +++++++++---- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git "a/content/posts/algorithm/data structure/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" "b/content/posts/algorithm/data structure/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" index 94a9899..5f4d954 100644 --- "a/content/posts/algorithm/data structure/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" +++ "b/content/posts/algorithm/data structure/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" @@ -6,7 +6,7 @@ series: [data structure, trick] categories: [algorithm] --- -## 概念 +### 概念 应用场景:在原始数组不会被修改的情况下,**快速、频繁查询某个区间的累加和**。 @@ -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/) diff --git "a/content/posts/algorithm/trick/\344\272\214\345\210\206\346\263\225.md" "b/content/posts/algorithm/trick/\344\272\214\345\210\206\346\263\225.md" index 0a62916..37b366a 100644 --- "a/content/posts/algorithm/trick/\344\272\214\345\210\206\346\263\225.md" +++ "b/content/posts/algorithm/trick/\344\272\214\345\210\206\346\263\225.md" @@ -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/) @@ -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)