https://leetcode.com/problems/create-sorted-array-through-instructions/
树状数组的典型问题,学习一下


个人解答

  1. class Solution:
  2. def createSortedArray(self, nums: List[int]) -> int:
  3. N = max(nums) + 1
  4. c = [0] * N
  5. def getSum(c, x):
  6. res = 0
  7. while x > 0:
  8. res += c[x]
  9. x -= x & -x
  10. return res
  11. def update(c, x):
  12. while x < len(c):
  13. c[x] += 1
  14. x += x & -x
  15. res = 0
  16. for i, x in enumerate(nums):
  17. res += min(getSum(c, x - 1), i - getSum(c, x))
  18. update(c, x)
  19. return res % (10**9+7)

题目分析

本题比较容易看出,是一个考察数据结构的题目,整个题目是以很直白的方式说明,没有各种算法的选择,单纯是数据结构的设计。

通过题目描述可以看出,需要对数组进行两个操作:

  • 添加元素
  • 计算元素左右区间的某一属性(小于/大于的元素个数)

涉及到动态的区间查询,基本上要用到类似于线段树这样的数据结构,但本题也没有那么的直白。线段树本来支持的操作是,修改与查询,但这里是添加元素,同时还要计算小于/大于的元素个数,因此要对问题做一个转化,将添加元素转化为修改元素,并能够维护大小关系!

用数组存储 [1, max(nums)] 区间的元素个数,初始都是0
如此一来:

  • 添加一个数,只是对应数字的值+1
  • 计算小于数字数量,即计算该数字之前的前缀和
  • 计算大于数字数量,即计算该数字之后的和,也就是总数-当前数字对应的前缀和

这就是一个标准的单点修改,区间查询的操作了

可以通过线段树解决:

最直观的,递归方式的线段树:(有概率超时)

  1. class Solution:
  2. def createSortedArray(self, nums: List[int]) -> int:
  3. N = max(nums) + 1
  4. depth = int(math.ceil(math.log2(N)))
  5. tree = [0] * (2 * 2 ** depth - 1)
  6. def update(tree, curr, l, r, x):
  7. tree[curr] += 1
  8. if l == r:
  9. return
  10. mid = (l + r) // 2
  11. if x <= mid:
  12. update(tree, 2 * curr + 1, l, mid, x)
  13. else:
  14. update(tree, 2 * curr + 2, mid + 1, r, x)
  15. def getPreSum(tree, curr, l, r, x): # inclusive
  16. if x >= r:
  17. return tree[curr]
  18. if x < l:
  19. return 0
  20. mid = (l + r) // 2
  21. return getPreSum(tree, 2 * curr + 1, l, mid, x) + getPreSum(tree, 2 * curr + 2, mid + 1, r, x)
  22. res = 0
  23. for i, x in enumerate(nums):
  24. res += min(getPreSum(tree, 0, 0, N - 1, x - 1), i - getPreSum(tree, 0, 0, N - 1, x))
  25. update(tree, 0, 0, N - 1, x)
  26. return res % (10 ** 9 + 7)

优化后线段树 迭代(勉强通过)

  1. class Solution:
  2. def createSortedArray(self, nums: List[int]) -> int:
  3. N = max(nums) + 1
  4. tree = [0] * (2 * N)
  5. def update(tree, x):
  6. i = N + x
  7. tree[i] += 1
  8. while i > 1:
  9. tree[i >> 1] = tree[i] + tree[i ^ 1]
  10. i >>= 1
  11. def getPreSum(tree, x): # inclusive
  12. # sum: [0, x]
  13. l = N
  14. r = N + x
  15. res = 0
  16. while l <= r:
  17. if l & 1 == 1:
  18. res += tree[l]
  19. l += 1
  20. l >>= 1
  21. if r & 1 == 0:
  22. res += tree[r]
  23. r -= 1
  24. r >>= 1
  25. return res
  26. res = 0
  27. for i, x in enumerate(nums):
  28. res += min(getPreSum(tree, x - 1), i - getPreSum(tree, x))
  29. update(tree, x)
  30. return res % (10 ** 9 + 7)

树状数组

本题的特征,更推荐使用树状数组,简洁高效
树状数组算是线段树的一个子集,通过一定程度的简化,对于一些简单问题,可以用更简洁更高效的操作代替树状数组复杂的操作,本题满足这样的条件。

树状数组实现特别简洁,但是搞明白并不是很容易,一个比较入门的解释:https://www.bilibili.com/video/BV1pE41197Qj
image.png
树状数组大致长上图这个样子,先不管为什么要这样设计,先了解具体有什么样的特征(然后发现,发明这玩意的人真他娘是个天才)

原始数组为 a ,8个元素(下标从1开始)
树状数组为 t ,同样8个元素,不过存储了原始数组的一些区间信息(区间和)
例如, t[8] 存储了1~8的区间和, t[6] 存储了5~6的区间和
为什么用这样的结构呢,我们来看一看这样的树结构的性质,尤其关注下标的二进制表示

在这之前,需要先有一个操作: lowbit(x) ,这个操作可以得到一个数二进制表示只保留最后一位之后得到的结果,例如:

  • lowbit(0b10010) = 0b10
  • lowbit(0b1001) = 0b1

这个操作可以通过: lowbit(x) = x & ~x 来得到,为什么可以,用补码表示简单推导一下就能得到

为什么引入这么一个操作呢,因为有了它,树状数组的操作变得异常简洁:

  • 当前节点x的父节点: x + lowbit(x)
  • 当前节点x的前一个子树: x - lowbit(x)

amazing!!!!

那么我们来看下单点修改的操作:从对应位置一路向上更新父节点就好,一路+lowbit(x)
那么前缀和查询呢:对应位置一路往前加,一路-lowbit(x)

这下明白为啥这样设计了吧,能做到两个操作极其简洁高效的处理,牛逼!!!!!


那么回到这道题,如果用树状数组的话,也是比较清晰的:

  • 初始都为0
  • 每次计算当前数之前的前缀和 以及 包含当前数的前缀和(用总数减去,就得到了比当前数大的)
  • 在这之后插入该数(更新对应值+1)

解法见个人解答,参考:https://leetcode.com/problems/create-sorted-array-through-instructions/discuss/927531/JavaC%2B%2BPython-Binary-Indexed-Tree