【Leetcode】638. Shopping Offers

题目地址:

https://leetcode.com/problems/shopping-offers/

给定 n n n个物品的价格 A A A,再给定一个数组 B B B B [ i ] B[i] B[i]表示某个“套餐”,它是一个长 n + 1 n+1 n+1的数组,其前 B [ i ] [ j ] B[i][j] B[i][j]表示下标为 j j j的物品有多少个, B [ i ] [ n ] B[i][n] B[i][n]表示这些物品的套餐的价格。再给定一个长 n n n数组 C C C代表需求,表示要求买的各个物品数量,问恰好买这么多物品最低花费是多少。注意,必须是恰好买这么多,不能多买也不能少买。物品也是可以单独买的,即不参加套餐。每个套餐买的个数不受限制。物品个数不超过 6 6 6个。

思路是记忆化搜索。我们可以按照最后一步选了什么套餐来分类(当然什么套餐都不选也是种方案),然后进行DFS。对于每个需求,我们可以开个哈希表做记忆化。这里需要解释一下java语言的一些特性,在java里,List的哈希方式,是将其哈希成开头加个 1 1 1后的 31 31 31进制整数,例如对于列表 [ 1 , 2 , 3 ] [1,2,3] [1,2,3],java会将其哈希成 31 31 31进制下的 1123 1123 1123,也就是哈希成 1 × 3 1 3 + 1 × 3 1 2 + 2 × 31 + 3 = 30817 1\times 31^3+1\times 31^2+2\times 31+3=30817 1×313+1×312+2×31+3=30817,而List里判断是否equals,是逐个比较列表里的值,如果值全相等就返回true。所以在记忆化的时候,可以直接把key设为是List类型的。但是,如果将key设为List类型的,要注意尽量不要修改其值,即使修改了,也要记得在操作哈希表之前还原回来。当然最好的选择是将List转为字符串后存入哈希表里,这样不用怕key被修改。代码如下:

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Solution {
    public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
        return dfs(needs, new HashMap<>(), price, special);
    }
    
    private int dfs(List<Integer> needs, Map<List<Integer>, Integer> map, List<Integer> price, List<List<Integer>> special) {
    	// 如果对于当前的needs之前已经算出过结果了,则直接返回之
        if (map.containsKey(needs)) {
            return map.get(needs);
        }
        
        int res = 0;
        // 先考虑什么套餐都不用的情况,也就是每个商品都单独买
        for (int i = 0; i < price.size(); i++) {
            res += needs.get(i) * price.get(i);
        }
        
        // 接着考虑每个套餐
        for (int i = 0; i < special.size(); i++) {
            List<Integer> sp = special.get(i);
            // 如果当前套餐可以买,也就是买了不会超出需求,那么就尝试之
            if (fit(needs, sp)) {
            	// 将套餐的商品扣掉
                for (int j = 0; j < needs.size(); j++) {
                    needs.set(j, needs.get(j) - sp.get(j));
                }
                res = Math.min(res, sp.get(price.size()) + dfs(needs, map, price, special));
                // 尝试完了要恢复现场
                for (int j = 0; j < needs.size(); j++) {
                    needs.set(j, needs.get(j) + sp.get(j));
                }
            }
        }
        
        // 算完了做一下记忆化
        map.put(needs, res);
        return res;
    }
    
    // 判断一下套餐sp是否超出needs
    private boolean fit(List<Integer> needs, List<Integer> sp) {
        for (int i = 0; i < needs.size(); i++) {
            if (needs.get(i) < sp.get(i)) {
                return false;
            }
        }
        
        return true;
    }
}

时间复杂度 O ( l B n ∏ C ) O(l_Bn\prod C) O(lBnC),但实际上并不是每个小于 C C C的需求都会被遍历到,所以实际时间是比较小的,空间 O ( n ∏ C ) O(n\prod C) O(nC)

当然此题也可以用背包的递推公式来做记忆化搜索,并且使用状态压缩记录每个物品有多少个。由于最多有 6 6 6种物品,每个物品最多 6 6 6个,所以可以用个 6 × 3 6\times 3 6×3位二进制数来表示每个物品有多少个。代码如下:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Solution {
    public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
        int n = price.size();
        // 开一个记忆化数组,dp[i][s]表示如果只考虑前i个套餐的话,要买到s这个状态,至少需要多少花费
        int[][] dp = new int[special.size() + 1][1 << n * 3];
        // 先初始化为-1
        for (int[] row : dp) {
            Arrays.fill(row, -1);
        }
        
        // 求一下needs代表的状态,这里target的最低3位表示的是下标是0的商品要买多少个,以此类推
        int target = 0;
        for (int i = needs.size() - 1; i >= 0; i--) {
            target = (target << 3) + needs.get(i);
        }
        
        return dfs(special.size(), target, special, price, dp);
    }
    
    // 返回的是,如果只考虑前count个套餐的话,要达到state这个状态的最小花费(当然单买也是考虑的)
    private int dfs(int count, int state, List<List<Integer>> special, List<Integer> price, int[][] dp) {
    	// 如果之前已经算出来过,则直接返回
        if (dp[count][state] != -1) {
            return dp[count][state];
        }
        
        int n = price.size();
        // 如果一个套餐都不考虑,那就是全单买
        if (count == 0) {
            dp[count][state] = 0;
            // 求一下每个商品买多少个,然后累加一下花费
            for (int i = 0; i < n; i++) {
                int c = state >> i * 3 & 7;
                dp[count][state] += c * price.get(i);
            }
            
            return dp[count][state];
        }
        
        // 考虑不选第count个套餐的情况
        dp[count][state] = dfs(count - 1, state, special, price, dp);
        // 考虑选第count个套餐的情况
        List<Integer> sp = special.get(count - 1);
        // 存一下考虑完当前套餐后的需求状态
        int nextState = 0;
        // 逆序遍历是为了方便nextState的计算
        for (int i = n - 1; i >= 0; i--) {
            int c = state >> i * 3 & 7;
            // 小了,说明当前套餐是不能选的,标记为-1并退出循环
            if (c < sp.get(i)) {
                nextState = -1;
                break;
            }
            
            nextState = (nextState << 3) + c - sp.get(i);
        }
        
        // 如果当前套餐能选,再算一下选了当前套餐的情况下的最小花费
        if (nextState != -1) {
            dp[count][state] = Math.min(dp[count][state], sp.get(n) + dfs(count, nextState, special, price, dp));
        }
        
        return dp[count][state];
    }
}

时空复杂度一样。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值