参考题目「力扣_剑指offer_51_数组中的逆序对」、「力扣_315_计算右侧小于当前元素的个数」
1.归并排序
一个经典的算法,值得作为模板记录下来,首先归并排序是需要有一个“ 递归流程 ”作为前置的,因此需要设置停止条件,像本例中的递归中设置了“归并”的左右闭边界 " r " 、" l " 作为驱动参数,这种相对复杂的递归参数设置在N皇后问题中也有遇到。
另外还有一个技巧性的设置是mid,通过整除向下取整的方式使得即便考虑边界条件l=r-1,此时(l+r)//2也一定等于r-1,因此最多会发生"mid+1=r"的情况,这就规避了越界行为。另外模板化记忆递归调用实现“并”操作时要注意切分区间不重叠 i.e. f( l , mid ) + f( mid+1 , r )。
class Solution:
def mergeSort(self, nums, tmp, l, r):
if l >= r:
return 0
mid = (l + r) // 2
inv_count = self.mergeSort(nums, tmp, l, mid) + self.mergeSort(nums, tmp, mid + 1, r)
这里的递归调用还充当了“分割”的工具。再依官方视频讲解的逻辑,完成在一般步骤下对细分组件进行 计数 + 排序 + 合并 的操作,由于循环也是由递归形式驱动的,因此这里的指针就要实例化为局部变量: i (L区域:分割左半部分的指针), j (R区域:分割右半部分的指针),pos (全区域指针)。
使用while循环进行排序,排序有一个trick就是使用中间变量tmp来存放排序后内容,而不是直接就原变量进行修改,这是因为我们还需要“计(逆序对的)数”。while的规则就是L区域和R区域的指针都不越界,每次在tmp中计入有效数字后全区域指针右移 i.e.+1。
i, j, pos = l, mid + 1, l
while i <= mid and j <= r:
if nums[i] <= nums[j]:
tmp[pos] = nums[i]
i += 1
inv_count += (j - (mid + 1))
else:
tmp[pos] = nums[j]
j += 1
pos += 1
由于程序设置的原因,前面while的停止条件是左区域或右区域有一个已经排完了,这说明另一个区域很大可能还没排完,因此接下来用两个for循环覆盖两种可能出现的情况:左区域没排完,右区域没排完。另外值得一提的是,调用递归的步骤设置在inv_count的赋值上,因此其后的代码逻辑可以按照“最后一步递归之后,停止条件前的收尾”的一种视角来进行处理,这会令代码思路更清晰,编程也会顺畅很多。
for k in range(i, mid + 1):
tmp[pos] = nums[k]
inv_count += (j - (mid + 1))
pos += 1
for k in range(j, r + 1):
tmp[pos] = nums[k]
pos += 1
nums[l:r+1] = tmp[l:r+1]
return inv_count
def reversePairs(self, nums: List[int]) -> int:
n = len(nums)
tmp = [0] * n
return self.mergeSort(nums, tmp, 0, n - 1)
2.位运算“负号”规则
参考文章「【详解】位运算符--正数及负数的位运算」,这里需要补充的是负号二进制计算的补码规则,一般的流程:,而实际加上符号后转进制会经历符号赋码 + 主体反码 + 补码 的流程:
。
值得一提的是,在python中直接使用bin函数没办法显示补码规则,因为“在python中是没有位数这一概念的”,因此尽管在“与或非反”计算上它采取了补码规则,想要看到实际流程则是不可行的,相关尝试如上图。
3.离散化树状数组
简单且优质的科普材料参考b站视频「五分钟丝滑动画讲解 | 树状数组_哔哩哔哩_bilibili」,这个方法的实现逻辑实在太妙了,但是性能上比归并差了一点。
附注:力扣_剑指offer_51_官解对离散化的一点说明
我们显然可以用数组来实现这个桶,可问题是如果
中有很大的元素,比如
,我们就要开一个大小为
的桶,内存中是存不下的。这个桶数组中很多位置是
,有效位置是稀疏的,我们要想一个办法让有效的位置全聚集到一起,减少无效位置的出现,这个时候我们就需要用到一个方法——离散化。
附注:数组定义中静态方法的使用
参考文章「简述python中的@staticmethod作用及用法」,staticmethod(又称为“静态方法”)用于修饰类中的方法,使其可以在不创建类实例的情况下调用方法,这样做的好处是执行效率比较高。
- 使用静态方法支持的类名调用:Solution.f(*args);
- 像一般方法一样用实例调用该方法:c=Solution() --> c.f(*args);
静态方法不可以引用类中的属性或方法,其参数列表也不需要约定的默认参数self。
class BIT:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
@staticmethod
def lowbit(x):
return x & (-x)
def query(self, x):
ret = 0
while x > 0:
ret += self.tree[x]
x -= BIT.lowbit(x)
return ret
def update(self, x):
while x <= self.n:
self.tree[x] += 1
x += BIT.lowbit(x)
根据b站视频的讲解大概能理解树状数组总长即为原数组长度的逻辑:去芜存精。实际上这个数组结构的代码在query和update的部分可以自行调整,整个思想最核心的内容就是用到了lowbit函数,基于前述的负号位运算补码规则,“ x & (-x) ”得到的其实是x的最右端的“ 1 ”所在的位数,仍是以5为例:
class Solution:
def reversePairs(self, nums) -> int:
import bisect
n = len(nums)
# 离散化
tmp = sorted(nums)
for i in range(n):
nums[i] = bisect.bisect_left(tmp, nums[i]) + 1
# 树状数组统计逆序对
bit = BIT(n)
ans = 0
for i in range(n - 1, -1, -1):
ans += bit.query(nums[i] - 1)
bit.update(nums[i])
return ans
搭好树状数组后仍需要交互地更新树状数组的内容,这就有点定式的味道了,使用中间变量tmp记录排序内容而不是直接对nums进行修改的trick在这里又一次用到了(又可以理解为是“ 入桶 ”的必备流程),离散化入桶这个技巧则让我想到了绿皮里的彩虹帽子问题,因为都是一一映射技巧...(整个过程看起来仍旧是蛮复杂的,作为提升水平的知识点记录一下吧。)
每次“日拱一卒”都撅个两三天...只能说水平就在这儿了...总而言之也算有所得吧。
某乎上看到了下面几个反向水平测试问题,感觉有点意思就记录了下来:
- 为什么Fourier transform是一个unitary operator?
- 解释连续马尔科夫过程的generator matrix与transition matrix的关系。
- 说说数学里面kernel和卷积的应用。
今天还通过(拖了许久也没注册成功)的chatGPT得到了项链问题的计算思路。证明和公式推导具体论文可以在学术论文库中查找,necklace问题本身也是个经典问题了,这里仅附上一些有用链接:
stackoverflow的相关讨论「 explanation-circular-permutation」,
观感不错的bilibili视频讲解「《具体数学》4.19 项链着色问题的数论方法证明」,
这也侧面说明闭门造车不可取,毕竟智商限制在这,这些经典问题都能单发一篇论文了,怎么可能还靠干想就给想出来呢哈哈哈。