K 次调整数组大小浪费的最小总空间

标签: 数组 动态规划

难度: Medium

你正在设计一个动态数组。给你一个下标从 0 开始的整数数组 nums ,其中 nums[i] 是 i 时刻数组中的元素数目。除此以外,你还有一个整数 k ,表示你可以 调整 数组大小的 最多 次数(每次都可以调整成 任意 大小)。

t 时刻数组的大小 sizet 必须大于等于 nums[t] ,因为数组需要有足够的空间容纳所有元素。t 时刻 浪费的空间 为 sizet - nums[t] , 浪费空间为满足 0 <= t < nums.length 的每一个时刻 t 浪费的空间 之和 。

在调整数组大小不超过 k 次的前提下,请你返回 最小总浪费空间 。

注意:数组最开始时可以为 任意大小 ,且 不计入 调整大小的操作次数。

示例 1:

输入:nums = [10,20], k = 0
输出:10
解释:size = [20,20].
我们可以让数组初始大小为 20 。
总浪费空间为 (20 - 10) + (20 - 20) = 10 。

示例 2:

输入:nums = [10,20,30], k = 1
输出:10
解释:size = [20,20,30].
我们可以让数组初始大小为 20 ,然后时刻 2 调整大小为 30 。
总浪费空间为 (20 - 10) + (20 - 20) + (30 - 30) = 10 。

示例 3:

输入:nums = [10,20,15,30,20], k = 2
输出:15
解释:size = [10,20,20,30,30].
我们可以让数组初始大小为 10 ,时刻 1 调整大小为 20 ,时刻 3 调整大小为 30 。
总浪费空间为 (10 - 10) + (20 - 20) + (20 - 15) + (30 - 30) + (30 - 20) = 15 。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 106
  • 0 <= k <= nums.length - 1

Submission

运行时间: 805 ms

内存: 16.7 MB

class SkipList:
    def __init__(self):
        self.head = [None, [None]]
        self.tail = [self.head]
        
    def insert_back(self, val):
        new_node = [val, []]
        self.tail[0][1][0] = new_node
        new_node[1].append(None)
        self.tail[0] = new_node

    def query(self, val):
        curr = self.head
        for i in range(len(self.tail) - 1, -1, -1):
            while (node := curr[1][i]) and node[0][0][0] <= val * node[0][0][2]:
                curr = curr[1][i]
        return curr

    def split(self, criteria):
        new_list = SkipList()
        curr = self.head
        for i in range(len(self.tail) - 1, -1, -1):
            while curr[1][i] and criteria(curr[1][i]):
                curr = curr[1][i]
            if curr[1][i] is not None:
                if len(new_list.tail) <= i:
                    delta = i + 1 - len(new_list.tail)
                    new_list.head[1].extend([None] * delta)
                    new_list.tail.extend([new_list.head] * delta)
                t = curr[1][i]
                new_list.head[1][i] = t
                new_list.tail[i] = self.tail[i]
                curr[1][i] = None
                self.tail[i] = curr
                if i and curr is self.head:
                    self.tail.pop()
                    self.head[1].pop()
        return new_list

    def merge(self, new_list):
        for i in range(min(len(self.tail), len(new_list.tail))):
            if new_list.head[1][i] is not None:
                t = self.tail[i]
                t[1][i] = new_list.head[1][i]
                self.tail[i] = new_list.tail[i]
        for i in range(len(self.tail), len(new_list.tail)):
            self.head[1].append(new_list.head[1][i])
            self.tail.append(new_list.tail[i])
        new_list.tail = [new_list.head]
        new_list.head[1] = [None]

class Solution:
    def minSpaceWastedKResizing(self, nums: List[int], k: int) -> int:
        N = len(nums)
        dp = []
        m = 0
        for i, n in enumerate(nums, 1):
            if n > m:
                m = n
            dp.append(m * i)
        dp2 = [None] * len(dp)
        for j in range(k):
            ranges = []
            lines_arc = SkipList()
            for i in range(j + 1, N):
                new_arc = deque([(i - 1, dp[i - 1])])
                new_L = nums[i]
                while ranges and ranges[-1][1] <= new_L:
                    old_arc, _ = ranges.pop()
                    while True:
                        p1 = old_arc[-1]
                        while len(new_arc) > 1:
                            p2, p3 = new_arc[0], new_arc[1]
                            if (p2[0] - p1[0]) * (p3[1] - p2[1]) - (p3[0] - p2[0]) * (p2[1] - p1[1]) > 0:
                                break
                            else:
                                new_arc.popleft()
                        p0, p2 = (old_arc[-2] if len(old_arc) > 1 else None), new_arc[0]
                        if p0 is None or (p1[0] - p0[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (p1[1] - p0[1]) > 0:
                            new_arc = old_arc+new_arc
                            break
                        else:
                            old_arc.pop()
                    while len(new_arc) > 1:
                        p0, p1 = new_arc[0], new_arc[1]
                        if p1 and p0[1] - p0[0] * new_L >= p1[1] - p1[0] * new_L:
                            new_arc.popleft()
                        else:
                            break
                ranges.append((new_arc, new_L))
                x, y = new_arc[0]
                b = y - x * new_L
                new_list = lines_arc.split(lambda x: x[0][1] > new_L)
                while new_list.tail[0] is not new_list.head:
                    removed_list = new_list.head[1][0][0][2]
                    if removed_list.tail[0] is removed_list.head:
                        break
                    new_list = removed_list.split(lambda x: x[0][1] > new_L)
                    lines_arc.merge(removed_list)
                removed_list = lines_arc.split(lambda node: new_L * node[0][0][0] + b * node[0][0][2] > node[0][0][1])
                if lines_arc.tail[0] is lines_arc.head:
                    xp = x
                    yp = y
                    q = 1
                else:
                    (xp0, yp0, q0), k0, _ = lines_arc.tail[0][0]
                    b0 = (yp0 - k0 * xp0) // q0
                    q = k0 - new_L
                    xp = b - b0
                    yp = b * k0 - b0 * new_L
                lines_arc.insert_back(((xp, yp, q), new_L, removed_list))
                curr_node = lines_arc.query(i)
                (xp0, yp0, q0), k0, _ = curr_node[0]
                b0 = (yp0 - k0 * xp0) // q0
                dp2[i] = k0 * i + b0
            dp2, dp = dp, dp2
        return dp[-1] - sum(nums)

Explain

此题解采用动态规划和几何优化方法。初始阶段,它构建一个最大的DP数组,每个元素表示从起始到当前位置的最小浪费空间。随后,为了处理k次调整的问题,使用数据结构(如SkipList)来优化查找和更新操作,确保每次调整都是在最优的位置。在每一轮中,它尝试确定新的调整点,并计算从该点到下一个可能的调整点之间的最小浪费空间。最终,这种方法可以在给定调整次数的限制下,找到总浪费空间最小的策略。

时间复杂度: O(k * N^2)

空间复杂度: O(N)

class SkipList:
    def __init__(self):
        # 初始化头节点和尾节点
        self.head = [None, [None]]
        self.tail = [self.head]
        
    def insert_back(self, val):
        # 在跳表尾部插入新节点
        new_node = [val, []]
        self.tail[0][1][0] = new_node
        new_node[1].append(None)
        self.tail[0] = new_node

    def query(self, val):
        # 查询小于等于给定值的最大节点
        curr = self.head
        for i in range(len(self.tail) - 1, -1, -1):
            while (node := curr[1][i]) and node[0][0][0] <= val * node[0][0][2]:
                curr = curr[1][i]
        return curr

    def split(self, criteria):
        # 根据给定条件分割跳表
        new_list = SkipList()
        curr = self.head
        for i in range(len(self.tail) - 1, -1, -1):
            while curr[1][i] and criteria(curr[1][i]):
                curr = curr[1][i]
            if curr[1][i] is not None:
                if len(new_list.tail) <= i:
                    delta = i + 1 - len(new_list.tail)
                    new_list.head[1].extend([None] * delta)
                    new_list.tail.extend([new_list.head] * delta)
                t = curr[1][i]
                new_list.head[1][i] = t
                new_list.tail[i] = self.tail[i]
                curr[1][i] = None
                self.tail[i] = curr
                if i and curr is self.head:
                    self.tail.pop()
                    self.head[1].pop()
        return new_list

    def merge(self, new_list):
        # 合并两个跳表
        for i in range(min(len(self.tail), len(new_list.tail))):
            if new_list.head[1][i] is not None:
                t = self.tail[i]
                t[1][i] = new_list.head[1][i]
                self.tail[i] = new_list.tail[i]
        for i in range(len(self.tail), len(new_list.tail)):
            self.head[1].append(new_list.head[1][i])
            self.tail.append(new_list.tail[i])
        new_list.tail = [new_list.head]
        new_list.head[1] = [None]

class Solution:
    def minSpaceWastedKResizing(self, nums: List[int], k: int) -> int:
        # 主函数使用动态规划求解最小浪费空间
        N = len(nums)
        dp = []
        m = 0
        for i, n in enumerate(nums, 1):
            # 更新每个时间点的最大需求空间
            if n > m:
                m = n
            dp.append(m * i)
        dp2 = [None] * len(dp)
        for j in range(k):
            ranges = []
            lines_arc = SkipList()
            for i in range(j + 1, N):
                new_arc = deque([(i - 1, dp[i - 1])])
                new_L = nums[i]
                while ranges and ranges[-1][1] <= new_L:
                    old_arc, _ = ranges.pop()
                    while True:
                        # 几何优化处理交叉和更新范围
                        p1 = old_arc[-1]
                        while len(new_arc) > 1:
                            p2, p3 = new_arc[0], new_arc[1]
                            if (p2[0] - p1[0]) * (p3[1] - p2[1]) - (p3[0] - p2[0]) * (p2[1] - p1[1]) > 0:
                                break
                            else:
                                new_arc.popleft()
                        p0, p2 = (old_arc[-2] if len(old_arc) > 1 else None), new_arc[0]
                        if p0 is None or (p1[0] - p0[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (p1[1] - p0[1]) > 0:
                            new_arc = old_arc+new_arc
                            break
                        else:
                            old_arc.pop()
                    while len(new_arc) > 1:
                        p0, p1 = new_arc[0], new_arc[1]
                        if p1 and p0[1] - p0[0] * new_L >= p1[1] - p1[0] * new_L:
                            new_arc.popleft()
                        else:
                            break
                ranges.append((new_arc, new_L))
                x, y = new_arc[0]
                b = y - x * new_L
                new_list = lines_arc.split(lambda x: x[0][1] > new_L)
                while new_list.tail[0] is not new_list.head:
                    removed_list = new_list.head[1][0][0][2]
                    if removed_list.tail[0] is removed_list.head:
                        break
                    new_list = removed_list.split(lambda x: x[0][1] > new_L)
                    lines_arc.merge(removed_list)
                removed_list = lines_arc.split(lambda node: new_L * node[0][0][0] + b * node[0][0][2] > node[0][0][1])
                if lines_arc.tail[0] is lines_arc.head:
                    xp = x
                    yp = y
                    q = 1
                else:
                    (xp0, yp0, q0), k0, _ = lines_arc.tail[0][0]
                    b0 = (yp0 - k0 * xp0) // q0
                    q = k0 - new_L
                    xp = b - b0
                    yp = b * k0 - b0 * new_L
                lines_arc.insert_back(((xp, yp, q), new_L, removed_list))
                curr_node = lines_arc.query(i)
                (xp0, yp0, q0), k0, _ = curr_node[0]
                b0 = (yp0 - k0 * xp0) // q0
                dp2[i] = k0 * i + b0
            dp2, dp = dp, dp2
        return dp[-1] - sum(nums)

Explore

The provided question list contains only the word 'message' which does not specify a clear question about the algorithm or its implementation. To provide a meaningful response, please specify the question related to the logic, boundary details, or data structure characteristics of the provided solution for the LeetCode problem.