2944. 购买水果需要的最少金币数
题目描述
给你一个 下标从 0 开始的 整数数组 prices ,其中 prices[i] 表示你购买第 i + 1 个水果需要花费的金币数目。
水果超市有如下促销活动:
- 如果你花费
prices[i]购买了下标为i + 1的水果,那么你可以免费获得下标范围在[i + 1, i + i]的水果。
注意 ,即使你 可以 免费获得水果 j ,你仍然可以花费 prices[j - 1] 个金币去购买它以获得它的奖励。
请你返回获得所有水果所需要的 最少 金币数。
示例 1:
输入:prices = [3,1,2]
输出:4
解释:
- 用
prices[0] = 3个金币购买第 1 个水果,你可以免费获得第 2 个水果。 - 用
prices[1] = 1个金币购买第 2 个水果,你可以免费获得第 3 个水果。 - 免费获得第 3 个水果。
请注意,即使您可以免费获得第 2 个水果作为购买第 1 个水果的奖励,但您购买它是为了获得其奖励,这是更优化的。
示例 2:
输入:prices = [1,10,1,1]
输出:2
解释:
- 用
prices[0] = 1个金币购买第 1 个水果,你可以免费获得第 2 个水果。 - 免费获得第 2 个水果。
- 用
prices[2] = 1个金币购买第 3 个水果,你可以免费获得第 4 个水果。 - 免费获得第 4 个水果。
示例 3:
输入:prices = [26,18,6,12,49,7,45,45]
输出:39
解释:
- 用
prices[0] = 26个金币购买第 1 个水果,你可以免费获得第 2 个水果。 - 免费获得第 2 个水果。
- 用
prices[2] = 6个金币购买第 3 个水果,你可以免费获得第 4,5,6(接下来的三个)水果。 - 免费获得第 4 个水果。
- 免费获得第 5 个水果。
- 用
prices[5] = 7个金币购买第 6 个水果,你可以免费获得第 7 和 第 8 个水果。 - 免费获得第 7 个水果。
- 免费获得第 8 个水果。
请注意,即使您可以免费获得第 6 个水果作为购买第 3 个水果的奖励,但您购买它是为了获得其奖励,这是更优化的。
提示:
1 <= prices.length <= 10001 <= prices[i] <= 105
解法
方法一:记忆化搜索
思考
买下标 \(i\)(从 \(1\) 计)可免费获得其后 \(i\) 个水果,求买完全部的最少代价。\(n \le 1000\),从位置 \(i\) 出发,下一步必须在 \(i+1 \ldots 2i+1\) 中选一个再买,区间长度 \(O(i)\)。
\(dfs(i)\) 记忆化该后继最小值;当 \(2i \ge n\) 时买当前即可覆盖尾部。从 \(1\) 调用。
我们定义一个函数 \(\textit{dfs}(i)\),表示从第 \(i\) 个水果开始购买所有水果所需要的最少金币数。那么答案就是 \(\textit{dfs}(1)\)。
函数 \(\textit{dfs}(i)\) 的执行逻辑如下:
- 如果 \(i \times 2 \geq n\),说明只要买第 \(i - 1\) 个水果即可,剩余的水果都可以免费获得,所以返回 \(\textit{prices}[i - 1]\)。
- 否则,我们可以购买水果 \(i\),然后在接下来的 \(i + 1\) 到 \(2i + 1\) 个水果中选择一个水果 \(j\) 开始购买,那么 \(\textit{dfs}(i) = \textit{prices}[i - 1] + \min_{i + 1 \le j \le 2i + 1} \textit{dfs}(j)\)。
为了避免重复计算,我们使用记忆化搜索的方法,将已经计算过的结果保存起来,下次遇到相同的情况时,直接返回结果即可。
时间复杂度 \(O(n^2)\),空间复杂度 \(O(n)\)。其中 \(n\) 为数组 \(\textit{prices}\) 的长度。
1 2 3 4 5 6 7 8 9 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
方法二:动态规划
思考
记忆化按递归展开,改成自底向上后语义不变:\(f[i]\) 仍是从 \(i\) 买到结尾的最少金币,转移枚举 \(j \in [i+1,2i+1]\)。从后往前填表,避免递归栈,并与方法一对照。
我们可以将方法一中的记忆化搜索改写成动态规划的形式。
与方法一类似,我们定义 \(f[i]\) 表示从第 \(i\) 个水果开始购买所有水果所需要的最少金币数。那么答案就是 \(f[1]\)。
状态转移方程为 \(f[i] = \min_{i + 1 \le j \le 2i + 1} f[j] + \textit{prices}[i - 1]\)。
时间复杂度 \(O(n^2)\),空间复杂度 \(O(n)\)。其中 \(n\) 为数组 \(\textit{prices}\) 的长度。
在代码实现上,我们可以直接使用 \(\textit{prices}\) 数组来存储 \(f\) 数组,那么空间复杂度可以优化到 \(O(1)\)。
1 2 3 4 5 6 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
1 2 3 4 5 6 7 8 9 10 | |
1 2 3 4 5 6 | |
1 2 3 4 5 6 | |
方法三:动态规划 + 单调队列优化
思考
方法二对每个 \(i\) 线性求一段 \(f[j]\) 的最小,总时间 \(O(n^2)\),在 \(n=1000\) 尚可。观察到 \(i\) 减小时窗口 \([i+1,2i+1]\) 的右端收缩,可用单调队列维护候选下标。
从后往前:弹出超出 \(2i+1\) 的队首,用队首更新 \(prices[i-1]\),再按 \(prices\) 值保持队列递增。原地改写后 \(prices[0]\) 即为答案。
我们观察方法二中的状态转移方程,可以发现,对于每个 \(i\),我们需要求出 \(f[i + 1], f[i + 2], \cdots, f[2i + 1]\) 的最小值,并且随着 \(i\) 的减小,这些值的范围也在减小。这实际上是求一个单调收窄的滑动窗口的最小值,我们可以使用单调队列来优化。
我们从后往前计算,维护一个单调递增的队列 \(q\),队列中存储的是下标。如果 \(q\) 的队首元素大于 \(i \times 2 + 1\),说明 \(i\) 之后的元素都不会被用到,所以我们将队首元素出队。如果 \(i\) 不大于 \((n - 1) / 2\),那么我们可以将 \(\textit{prices}[q[0] - 1]\) 加到 \(\textit{prices}[i - 1]\) 上,然后将 \(i\) 加入队尾。如果 \(q\) 的队尾元素对应的水果价格大于等于 \(\textit{prices}[i - 1]\),那么我们将队尾元素出队,直到队尾元素对应的水果价格小于 \(\textit{prices}[i - 1]\) 或者队列为空,然后将 \(i\) 加入队尾。
时间复杂度 \(O(n)\),空间复杂度 \(O(n)\)。其中 \(n\) 为数组 \(\textit{prices}\) 的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |