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)$

最后更新于