Links

1649. 通过指令创建有序数组

https://leetcode-cn.com/problems/create-sorted-array-through-instructions/

题目描述

给你一个整数数组 instructions ,你需要根据 instructions 中的元素创建一个有序数组。一开始你有一个空的数组 nums ,你需要 从左到右 遍历 instructions 中的元素,将它们依次插入 nums 数组中。每一次插入操作的 代价 是以下两者的 较小值 :
nums 中 严格小于  instructions[i] 的数字数目。
nums 中 严格大于  instructions[i] 的数字数目。
比方说,如果要将 3 插入到 nums = [1,2,3,5] ,那么插入操作的 代价 为 min(2, 1) (元素 1 和  2 小于 3 ,元素 5 大于 3 ),插入后 nums 变成 [1,2,3,3,5] 。
请你返回将 instructions 中所有元素依次插入 nums 后的 总最小代价 。由于答案会很大,请将它对 109 + 7 取余 后返回。
 
示例 1:
输入:instructions = [1,5,6,2]
输出:1
解释:一开始 nums = [] 。
插入 1 ,代价为 min(0, 0) = 0 ,现在 nums = [1] 。
插入 5 ,代价为 min(1, 0) = 0 ,现在 nums = [1,5] 。
插入 6 ,代价为 min(2, 0) = 0 ,现在 nums = [1,5,6] 。
插入 2 ,代价为 min(1, 2) = 1 ,现在 nums = [1,2,5,6] 。
总代价为 0 + 0 + 0 + 1 = 1 。
示例 2:
输入:instructions = [1,2,3,6,5,4]
输出:3
解释:一开始 nums = [] 。
插入 1 ,代价为 min(0, 0) = 0 ,现在 nums = [1] 。
插入 2 ,代价为 min(1, 0) = 0 ,现在 nums = [1,2] 。
插入 3 ,代价为 min(2, 0) = 0 ,现在 nums = [1,2,3] 。
插入 6 ,代价为 min(3, 0) = 0 ,现在 nums = [1,2,3,6] 。
插入 5 ,代价为 min(3, 1) = 1 ,现在 nums = [1,2,3,5,6] 。
插入 4 ,代价为 min(3, 2) = 2 ,现在 nums = [1,2,3,4,5,6] 。
总代价为 0 + 0 + 0 + 0 + 1 + 2 = 3 。
示例 3:
输入:instructions = [1,3,3,3,2,4,2,1,2]
输出:4
解释:一开始 nums = [] 。
插入 1 ,代价为 min(0, 0) = 0 ,现在 nums = [1] 。
插入 3 ,代价为 min(1, 0) = 0 ,现在 nums = [1,3] 。
插入 3 ,代价为 min(1, 0) = 0 ,现在 nums = [1,3,3] 。
插入 3 ,代价为 min(1, 0) = 0 ,现在 nums = [1,3,3,3] 。
插入 2 ,代价为 min(1, 3) = 1 ,现在 nums = [1,2,3,3,3] 。
插入 4 ,代价为 min(5, 0) = 0 ,现在 nums = [1,2,3,3,3,4] 。
​​​​​插入 2 ,代价为 min(1, 4) = 1 ,现在 nums = [1,2,2,3,3,3,4] 。
插入 1 ,代价为 min(0, 6) = 0 ,现在 nums = [1,1,2,2,3,3,3,4] 。
插入 2 ,代价为 min(2, 4) = 2 ,现在 nums = [1,1,2,2,2,3,3,3,4] 。
总代价为 0 + 0 + 0 + 0 + 1 + 0 + 1 + 0 + 2 = 4 。
 
提示:
1 <= instructions.length <= 105
1 <= instructions[i] <= 105

前置知识

公司

  • 暂无

二分法

思路

二分法的思路比较简单,直接模拟插入即可。每次只需要保证插入之后还是有序的,这样就可以通过二分查找,计算出严格大于严格小于 x 的数目了。
  • 使用 bisect.bisect_left(nums, instruction) 可以计算出 instruction 如果插入到 nums ,instruction 在 nums 中的索引是。
  • 使用 bisect.bisect_right(nums, instruction) 和 bisect_left 类似,只不过对于 nums 已经存在 instruction 了, bisect_left 会尝试插入到其左侧,bisect_right 则会尝试插入到其右侧。
根据 bisect_left 和 bisect_right,我们就可计算出 严格大于严格小于 instruction 的数目了。接下来,我们只需要模拟插入即可。

代码

代码支持:Python3
Python3 Code:
class Solution:
def createSortedArray(self, instructions: List[int]) -> int:
mod = 10 ** 9 + 7
nums = []
ans = 0
# eg: 1 2 2 3
for instruction in instructions:
l = bisect.bisect_left(nums, instruction)
r = bisect.bisect_right(nums, instruction)
nums[l:l] = [instruction]
ans = (ans + min(l, len(nums) - r - 1)) % mod
return ans
复杂度分析 令 N 为数组长度。
  • 时间复杂度:遍历 instructions 需要 $N$ 次,每次都需要插入数据, 由于插入数组的时间复杂度是 $O(N)$。 因此总的时间复杂度为 $O(N^2)$
  • 空间复杂度:$O(N)$
需要注意的是,如下代码会超时:
nums.insert(l, instruction)
也就是说必须使用切片语法才可以:
nums[l:l] = [instruction]
具体原因大家可以参考这个 stackoverflow 的回答

线段树(超时)

思路

这里我直接使用了计数线段树的模板。不懂线段树的可以先看下 线段树教程
我们可以维护一个 [lower,upper] 的一个线段树。线段树支持的操作:
  • query(l, r): 查询 [l, r] 范围内的数的个数
  • update(x): 将 x 更新到线段树
因此我们的目标其实就是 min(query(1, instruction - 1), query(instruction + 1, upper)),其中 upper 为 instructions 的最大树。
核心代码:
upper = max(instructions)
# 初始化线段树
seg = SegmentTree(upper, 1)
for instruction in instructions:
# 进行两次查询
l = seg.queryCount(1, instruction - 1)
r = seg.queryCount(instruction + 1, upper)
ans = (ans + min(l, r)) % mod
# 进行一次更新
seg.updateCount(instruction)
return ans

代码

代码支持:Python3
Python3 Code:
class SegmentTree:
def __init__(self, upper, lower):
"""
data:传入的数组
"""
self.lower = lower
self.upper = upper
# 申请4倍data长度的空间来存线段树节点
self.tree = [0] * (4 * (upper - lower + 1)) # 索引i的左孩子索引为2i+1,右孩子为2i+2
# 本质就是一个自底向上的更新过程
# 因此可以使用后序遍历,即在函数返回的时候更新父节点。
def update(self, tree_index, l, r, index):
"""
tree_index:某个根节点索引
l, r : 此根节点代表区间的左右边界
index : 更新的值的索引
"""
if l > index or r < index:
return
self.tree[tree_index] += 1
if l == r:
return
mid = (l + r) // 2
left, right = tree_index * 2 + 1, tree_index * 2 + 2
self.update(left, l, mid, index)
self.update(right, mid + 1, r, index)
def updateCount(self, index: int):
self.update(0, self.lower, self.upper, index)
def query(self, tree_index: int, l: int, r: int, ql: int, qr: int) -> int:
"""
递归查询区间[ql,..,qr]的值
tree_index : 某个根节点的索引
l, r : 该节点表示的区间的左右边界
ql, qr: 待查询区间的左右边界
"""
if qr < l or ql > r:
return 0
# l 和 r 在 [ql, qr] 内
if ql <= l and qr >= r:
return self.tree[tree_index]
mid = (l + r) // 2
left, right = tree_index * 2 + 1, tree_index * 2 + 2
return self.query(left, l, mid, ql, qr) + self.query(right, mid + 1, r, ql, qr)
def queryCount(self, ql: int, qr: int) -> int:
"""
返回区间[ql,..,qr]的计数信息
"""
return self.query(0, self.lower, self.upper, ql, qr)
class Solution:
def createSortedArray(self, instructions: List[int]) -> int:
mod = 10 ** 9 + 7
ans = 0
# eg: 1 2 2 3
upper = max(instructions)
seg = SegmentTree(upper, 1)
for instruction in instructions:
l = seg.queryCount(1, instruction - 1)
r = seg.queryCount(instruction + 1, upper)
ans = (ans + min(l, r)) % mod
seg.updateCount(instruction)
return ans
复杂度分析 令 N 为数组长度。
由于线段树更新和查询的时间复杂度为 $O(log(upper - lower))$,其中 upper 为 instructions 最大值,lower 为 instructions 最小值。由于题目限制了 $1 <= instructions[i] <= 10^5$,因此最坏情况下 upper - lower 为 10 ^5。
线段树使用了 $4 * (upper - lower + 1)$ 的空间。
  • 时间复杂度:$O(Nlog(upper-lower))$
  • 空间复杂度:$O(upper- lower)$