大礼包

2024-11-03

题目:

在 LeetCode 商店中, 有 n 件在售的物品。每件物品都有对应的价格。然而,也有一些大礼包,每个大礼包以优惠的价格捆绑销售一组物品。

给你一个整数数组 price 表示物品价格,其中 price[i] 是第 i 件物品的价格。另有一个整数数组 needs 表示购物清单,其中 needs[i] 是需要购买第 i 件物品的数量。

还有一个数组 special 表示大礼包,special[i] 的长度为 n + 1 ,其中 special[i][j] 表示第 i 个大礼包中内含第 j 件物品的数量,且 special[i][n] (也就是数组中的最后一个整数)为第 i 个大礼包的价格。

返回 确切 满足购物清单所需花费的最低价格,你可以充分利用大礼包的优惠活动。你不能购买超出购物清单指定数量的物品,即使那样会降低整体价格。任意大礼包可无限次购买。

示例 1:

输入:price = [2,5], special = [[3,0,5],[1,2,10]], needs = [3,2]
输出:14
解释:有 A 和 B 两种物品,价格分别为 ¥2 和 ¥5 。 
大礼包 1 ,你可以以 ¥5 的价格购买 3A 和 0B 。 
大礼包 2 ,你可以以 ¥10 的价格购买 1A 和 2B 。 
需要购买 3 个 A 和 2 个 B , 所以付 ¥10 购买 1A 和 2B(大礼包 2),以及 ¥4 购买 2A 。

示例 2:

输入:price = [2,3,4], special = [[1,1,0,4],[2,2,1,9]], needs = [1,2,1]
输出:11
解释:A ,B ,C 的价格分别为 ¥2 ,¥3 ,¥4 。
可以用 ¥4 购买 1A 和 1B ,也可以用 ¥9 购买 2A ,2B 和 1C 。 
需要买 1A ,2B 和 1C ,所以付 ¥4 买 1A 和 1B(大礼包 1),以及 ¥3 购买 1B , ¥4 购买 1C 。 
不可以购买超出待购清单的物品,尽管购买大礼包 2 更加便宜。

提示:

  • n == price.length == needs.length
  • 1 <= n <= 6
  • 0 <= price[i], needs[i] <= 10
  • 1 <= special.length <= 100
  • special[i].length == n + 1
  • 0 <= special[i][j] <= 50
  • 生成的输入对于 0 <= j <= n - 1 至少有一个 special[i][j] 非零。

思路:

首先,我们过滤掉不需要计算的大礼包。

如果大礼包完全没有优惠(大礼包的价格大于等于原价购买大礼包内所有物品的价格),或者大礼包内不包含任何的物品,那么购买这些大礼包不可能使整体价格降低。因此,我们可以不考虑这些大礼包,并将它们过滤掉,以提高效率和方便编码。

因为题目规定了「不能购买超出购物清单指定数量的物品」,所以只要我们购买过滤后的大礼包,都一定可以令整体价格降低。

然后,我们计算满足购物清单所需花费的最低价格。

因为 1 ≤ needs ≤ 6 和 0 ≤ needs[i] ≤ 10,所以最多只有 11^6 = 1771561 种不同的购物清单 needs。我们可以将所有可能的购物清单作为状态,并考虑这些状态之间相互转移的方法。

用 dp[needs] 表示满足购物清单 needs 所需花费的最低价格。在进行状态转移时,我们考虑在满足购物清单 needs 时的最后一次购买;其中,将原价购买购物清单中的所有物品也视作一次购买。具体地,有以下两种情况:
	第一种情况是购买大礼包,此时状态转移方程为:
		dp[needs] =  i∈K(min{price_i + dp[needs−needs_i]}。
	其中,K 表示所有可以购买的大礼包的下标集合,i 表示其中一个大礼包的下标,price_i 表示第 i 个大礼包的价格,needs_i 表示大礼包中包含的物品清单,needs − needs_i 表示购物清单 needs 减去第 i 个大礼包中包含的物品清单后剩余的物品清单。
	第二种情况是不购买任何大礼包,原价购买购物清单中的所有物品,此时 dp[needs] 可以直接求出。

dp[needs] 为这两种情况的最小值。

因为大礼包中可能包含多个物品,所以并不是所有状态都可以得到。因此,我们使用记忆化搜索而不是完全遍历的方法,来计算出满足每个购物清单 needs 所需花费的最低价格。

具体地,在计算满足当前购物清单 cur_needs 所需花费的最低价格 min_price 时,我们可以采用如下方法:
	将 min_price 初始化为原价购买购物清单 cur_needs 中的所有物品的花费;
	逐个遍历所有可以购买的大礼包,不妨设当前遍历的大礼包为 cur_special,其价格为 special_price:
		计算购买大礼包 cur_special 后新的购物清单 nxt_needs,并递归地计算满足购物清单 nxt_needs 所需花费的最低价格 nxt_price;
		计算满足当前购物清单 cur_needs 所需花费的最低价格 cur_price=special_price+nxt_price;
		如果 cur_price<min_price,则将 min_price 更新为 cur_price。
	返回计算满足购物清单 cur_needs 所需花费的最低价格 min_price。

代码:

class Solution {
public:
    map<vector<int>, int> memo;

    int shoppingOffers(vector<int>& price, vector<vector<int>>& special, vector<int>& needs) {
        int n = price.size();

        // 过滤不需要计算的大礼包,只保留需要计算的大礼包
        vector<vector<int>> filterSpecial;
        for (auto & sp : special) {
            int totalCount = 0, totalPrice = 0;
            for (int i = 0; i < n; ++i) {
                totalCount += sp[i];
                totalPrice += sp[i] * price[i];
            }
            if (totalCount > 0 && totalPrice > sp[n]) {
                filterSpecial.emplace_back(sp);
            }
        }

        return dfs(price, special, needs, filterSpecial, n);
    }

    // 记忆化搜索计算满足购物清单所需花费的最低价格
    int dfs(vector<int> price,const vector<vector<int>> & special, vector<int> curNeeds, vector<vector<int>> & filterSpecial, int n) {
        if (!memo.count(curNeeds)) {
            int minPrice = 0;
            for (int i = 0; i < n; ++i) {
                minPrice += curNeeds[i] * price[i]; // 不购买任何大礼包,原价购买购物清单中的所有物品
            }
            for (auto & curSpecial : filterSpecial) {
                int specialPrice = curSpecial[n];
                vector<int> nxtNeeds;
                for (int i = 0; i < n; ++i) {
                    if (curSpecial[i] > curNeeds[i]) { // 不能购买超出购物清单指定数量的物品
                        break;
                    }
                    nxtNeeds.emplace_back(curNeeds[i] - curSpecial[i]);
                }
                if (nxtNeeds.size() == n) { // 大礼包可以购买
                    minPrice = min(minPrice, dfs(price, special, nxtNeeds, filterSpecial, n) + specialPrice);
                }
            }
            memo[curNeeds] = minPrice;
        }
        return memo[curNeeds];
    }
};