购买水果需要的最少金币数

2025-01-24

题目:

给你一个 下标从 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 <= 1000
  • 1 <= prices[i] <= 105

思路:

一、寻找子问题
我们需要解决的问题是:「获得第 1 个及其后面的水果所需要的最少金币数」。

第 1 个水果一定要买,然后呢?
第 2 个水果可以购买,也可以免费获得:
	如果购买,那么需要解决的问题为:「在购买第 2 个水果的前提下,获得第 2 个及其后面的水果所需要的最少金币数」。
	如果免费获得,那么根据题意,第 3 个水果必须购买,需要解决的问题为:「在购买第 3 个水果的前提下,获得第 3 个及其后面的水果所需要的最少金币数」。

无论哪种情况都会把原问题变成一个和原问题相似的、规模更小的子问题,所以可以用递归解决。
DP 做题技巧:见微知著,想清楚第一步(或者最后一步)怎么做,就想清楚了递归过程中的每一步要怎么做。

二、状态定义与状态转移方程
从上面的讨论可以知道,只需要一个 i 就能表达子问题,即定义 dfs(i) 表示在购买第 i 个水果的前提下,获得第 i 个及其后面的水果所需要的最少金币数。注意 i 从 1 开始。

买第 i 个水果,那么从 i+1 到 2i 的水果都是免费的。枚举下一个购买的水果 j,问题变成:在购买第 j 个水果的前提下,获得第 j 个及其后面的水果所需要的最少金币数,即 dfs(j)。

j 的范围是 [i+1,2i+1]。其中 2i+1 表示免费获得从 i+1 到 2i 的所有水果,那么第 2i+1 个水果不能免费,一定要买。

这些 dfs(j) 取最小值,再加上购买第 i 个水果的花费 prices[i],得
dfs(i)=prices[i]+  j=i+1 min 2i+1 dfs(j)
递归边界:注意到当 2i≥n,即 i≥⌈n / 2⌉= ⌊n+1 / 2⌋ 时,后面的水果都可以免费获得了,所以递归边界为 dfs(i)=prices[i] 其中 i≥⌊n+1 / 2⌋。

递归入口:dfs(1),这是原问题,也是答案。
由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
	如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 memo 数组中。
	如果一个状态不是第一次遇到(memo 中保存的结果不等于 memo 的初始值),那么可以直接返回 memo 中保存的结果。

代码:

class Solution {
public:
    int minimumCoins(vector<int>& prices) {
        int n = prices.size();
        vector<int> memo((n + 1) / 2);
        auto dfs = [&](this auto&& dfs, int i) -> int {
            if (i * 2 >= n) {
                return prices[i - 1]; // i 从 1 开始
            }
            int& res = memo[i]; // 注意这里是引用
            if (res) { // 之前算过
                return res;
            }
            res = INT_MAX;
            for (int j = i + 1; j <= i * 2 + 1; j++) {
                res = min(res, dfs(j));
            }
            res += prices[i - 1];
            return res;
        };
        return dfs(1);
    }
};