📌  相关文章
📜  通过重复删除给定操作获得的最大值来清空给定数组所需的成本(1)

📅  最后修改于: 2023-12-03 15:28:28.087000             🧑  作者: Mango

通过重复删除操作的最大化清空数组成本

简介

在这个问题中,我们需要通过删除操作将一个给定的数组清空。每次操作是删除数组中的某个元素。每次删除操作的成本是元素在数组中的位置。也就是说,用于删除第 i 个元素的成本为 i。我们需要找到一种删除元素的顺序,使得最终清空数组的总成本最小。这里我们将此总成本称为数组的清空成本。

在本文中,我们将探究如何通过贪心算法获得最小的数组清空成本。我们将首先介绍朴素贪心算法(即贪心算法的第一种实现),然后介绍一种更有效的贪心算法。我们还将在最后的部分中讨论如何在特定情况下使用动态规划来解决此问题。

朴素贪心算法

我们首先考虑最朴素的贪心策略:我们在每一步中删除数组中具有最小成本的元素。这意味着我们做出的选择仅基于当前操作的成本,而忽略了将来的成本。

我们可以使用以下 Python 代码来实现此算法:

def naive_solve(A):
    cost = 0
    while A:
        i = A.index(min(A))
        cost += i
        A.pop(i)
    return cost

该函数接受一个数组 A 并返回清空该数组所需的成本。在 while 循环中,我们让 i 是在 A 中具有最小值的元素的索引。我们累加 i 并从 A 中删除该元素。当数组 A 变为空时,我们返回累加的成本。

这个算法明显是贪心的。我们选择当前具有最小成本的元素,而无需考虑将来的成本。因此,有时会将其称为“局部最小值选择”。

更优秀的贪心算法

朴素贪心算法很容易证明不是最优的。我们来考虑一个例子:array([3, 1, 2, 7, 8, 4, 5, 6])。在朴素贪心算法中,我们将首先删除元素 1 并选择替换它的元素 2,接着我们将删除元素 2 选择 3 以及以此类推。这样做总成本为 28。

我们分析这个例子,我们会发现删除元素 1 之后,数组的剩余部分是 array([2, 7, 8, 4, 5, 6])。也就是说,我们可以通过删除元素 2 来跨过元素 1,获得更小的清空成本,因为删除元素 2 的成本为 1。这意味着,尽管元素 1 具有最小的成本,但有时通过“跨过”某些元素可以获得更小的总成本。

有一个算法可以处理这种情况。我们可以将该算法描述如下:

  1. 首先,我们将元素索引添加到它们的值中,并根据升序排序数组 A。
  2. 接下来,我们将从 A 中删除元素最少的子串,并为该子串中的每个删除选择最小成本的元素。我们将跟踪已删除元素的当前索引,并在每个子串中找到下一个(最接近当前索引的)元素以删除。
  3. 最后,我们返回已删除元素的总成本。

请看以下 Python 代码实现该算法:

def solve(A):
    B = [(v, i) for i, v in enumerate(A)]
    B.sort()
    cost = 0
    j = 0
    while j < len(B):
        i = j
        while j < len(B) and B[j][1] <= B[i][1]:
            j += 1
            continue
        sub_B = B[i:j]
        sub_B.sort(reverse=True)
        cost += sum([abs(k - idx) * v for k, (v, idx) in enumerate(sub_B)])
    return cost

在首部,我们将构建一个新的元组数组 B,其中每个元组包含元素的值和索引。我们按升序排序该数组,并将其中的索引合并到元素值中。在删除元素时,我们可以按 B 的顺序重建原始数组 A。

在主循环中,我们选择一个子串并计算从该子串中删除每个元素的成本。对于每个子串,我们将元组按元素露出排序,以使我们可以按其顺序遍历子串并获得最小的成本元素。最后,我们将每个元素的成本与其索引之差相乘并更改 cost。

动态规划

尽管更优秀的贪心算法相对简单且有效,但在某些情况下,使用动态规划可获得优于贪心算法的结果。一种情况是当某些删除成本具有特殊关系时。特别是:

  • 当所有删除成本相同时
  • 当删除成本相同的元素出现在数组中的固定间隔时

在这种情况下,动态规划可以产生更优秀的结果,并且可以在更短的时间内完成。

在本节中,我们将研究这两种特定情况,并展示如何使用动态规划来解决它们。

所有删除成本相同

当所有删除成本相同时,我们不能使用贪心算法。事实上,由于此策略并不考虑将来的成本,我们无法获得比每个元素随机删除更优的策略。我们可以证明,最小的数组清空成本至少与每个元素的成本的和一样大。

但是,我们可以使用动态规划来解决这个问题。我们定义 dp[i][j] 表示清空数组 A[i:j] 的最小成本。我们可以计算一个 dp 表来解决这个问题,其中 dp[i][j] 的递归公式如下:

dp[i][j] = 0,           j == i + 1
          min(dp[i][k] + dp[k][j]) + j - i, otherwise

换句话说,当我们尝试清空一个具有一个元素的子数组时,清空成本为 0。对于数组的其他子串,我们需要枚举可能的断点 k 并计算清空这两个子串的成本之和,再加上当前子串的长度。

使用这个公式,我们可以计算整个 dp 数组,最终返回 dp[0][len(A)](即整个数组 A 的清空成本)。

在 Python 代码中,我们可以实现这个算法如下:

def dp_solve(A):
    N = len(A)
    dp = [[0] * (N + 1) for _ in range(N + 1)]
    for i in range(N):
        for j in range(i + 2, N + 1):
            dp[i][j] = min([dp[i][k] + dp[k][j] for k in range(i + 1, j)]) + j - i
    return dp[0][N]
删除成本相同的元素出现在固定间隔中

当删除成本相同的元素出现在数组中的固定间隔时,我们也可以使用动态规划。在这种情况下,我们可以将问题分解为多个子问题,其中每个子问题本质上仅包含一些特定的元素,而所有其他元素的删除成本相同。

我们定义 dp[i][j][k] 为清空数组 A[i:j] 的最小成本,其中包含在 A[i:j] 中且需要按删除成本 k 进行删除的元素。一个显然的事实是,可以通过删除其中包含的任何其他元素来降低 k 元素的总成本。因此,我们将在递归定义中忽略 k 元素。

构建这个 dp 数组有点棘手,因为删除成本发生了变化。在本质上分割的子问题中,某些元素的删除成本是常数,而在其他子问题中,最小元素的删除成本可能随着子问题的变化而发生变化。

动态规划的一种解决方法是使用字典序变量来隐藏不必要的细节。在我们的情况下,我们可以为每个元素定义一个“前缀”,该前缀对应于删除该元素后删除成本变为常数的时刻。前缀是元素的索引模 k,其中 k 是两个相邻 k 元素之间的间隔。

我们可以使用以下公式计算 dp[i][j][k]:

dp[i][j][k] = min(dp[i][j][nk] + cost[s],   nk in range(i, j), A[nk] is a s)

这意味着,我们在 i 和 j 之间选择一个断点 nk,并计算删除所有 s 元素的成本,再加上删除在 nk 处找到的 k 元素的成本。最小值是所有子问题的最小值。

在 Python 代码中,我们可以实现这个算法如下:

def dp_solve_periodic(A, k, cost):
    N = len(A)
    pref = [[None] * (N + 1) for _ in range(k)]
    dp = [[[0] * k for _ in range(N + 1)] for _ in range(N + 1)]
    for i in range(N):
        for j in range(i + 1, N + 1):
            pref[A[i] % k][i] = i
            for s in range(k):
                if pref[s][i] is not None:
                    dp[i][j][s] = dp[i][pref[s][i]][s] + dp[pref[s][i] + 1][j][s] - cost[s]
            for s in range(k):
                if pref[s][i] is not None:
                    dp[i][j][s] += min([dp[i][nk][t] + cost[s] for t in range(k) for nk in range(pref[s][i], j) if A[nk] % k == t])
                    dp[i][j][s] += s * (j - i) - (s * (j - i)) % k
                pref[s][i] = min(pref[s][i], pref[s][(i - 1) % N])
    return min([dp[0][N][s] for s in range(k)])

此代码的输入是一个包含 k 个不同的删除成本的 cost 列表,每个元素的大小为 k 的数组 A 以及 k 值,对应于相邻 k 元素之间的间隔。输出是清空该数组所需的最小成本。我们首先计算每个元素的前缀。然后,我们使用前缀和公式计算 dp[i][j][s] 的初始值。

接下来,我们使用两个 for 循环遍历所有元素 s 并计算其最小成本子问题的每个 dp 值。使用一对 for 循环和一些带有条件的最小值计算 s 的 dp 值。最后,我们返回 dp[0][len(A)][0](即整个数组的清空成本,其中 s=0)的最小值。

结论

通过以上算法,我们已经学习了如何使用贪心算法在较短的时间内解决数组清空成本问题。我们首先介绍了朴素贪心算法,然后介绍了更优秀的贪心算法。最后,我们研究了一些特殊情况,并展示了如何使用动态规划解决这些情况。我们希望这些算法能够为你的工作和学习带来帮助。