In LeetCode Store, there are some kinds of items to sell. Each item has a price.
However, there are some special offers, and a special offer consists of one or more different kinds of items with a sale price.
You are given the each item’s price, a set of special offers, and the number we need to buy for each item. The job is to output the lowest price you have to pay for exactly certain items as given, where you could make optimal use of the special offers.
Each special offer is represented in the form of an array, the last number represents the price you need to pay for this special offer, other numbers represents how many specific items you could get if you buy this offer.
You could use any of special offers as many times as you want.
Example 1:
Input: [2,5], [[3,0,5],[1,2,10]], [3,2]
Output: 14
Explanation:
There are two kinds of items, A and B. Their prices are $2 and $5 respectively.
In special offer 1, you can pay $5 for 3A and 0B
In special offer 2, you can pay $10 for 1A and 2B.
You need to buy 3A and 2B, so you may pay $10 for 1A and 2B (special offer #2), and $4 for 2A.
Example 2:
Input: [2,3,4], [[1,1,0,4],[2,2,1,9]], [1,2,1]
Output: 11
Explanation:
The price of A is $2, and $3 for B, $4 for C.
You may pay $4 for 1A and 1B, and $9 for 2A ,2B and 1C.
You need to buy 1A ,2B and 1C, so you may pay $4 for 1A and 1B (special offer #1), and $3 for 1B, $4 for 1C.
You cannot add more items, though only $9 for 2A ,2B and 1C.
Note:
There are at most 6 kinds of items, 100 special offers.
For each item, you need to buy at most 6 of them.
You are not allowed to buy more items than you want, even if that would lower the overall price.
问题描述
购买一批东西,可以原价购买,可以打折促销购买,求满足购买数量的最小的总价钱。
代码实现
1、暴力DFS(3ms)
public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
return helper(price, special, needs, 0);
}
private int helper(List<Integer> price, List<List<Integer>> special, List<Integer> needs, int pos) {
int local_min = directPurchase(price, needs);
for (int i = pos; i < special.size(); i++) {
List<Integer> offer = special.get(i);
List<Integer> temp = new ArrayList<Integer>();
for (int j= 0; j < needs.size(); j++) {
//当前的special不满足
if (needs.get(j) < offer.get(j)) { // check if the current offer is valid
temp = null;
break;
}
temp.add(needs.get(j) - offer.get(j));
}
//使用special购买+剩下的need的DFS总价钱
if (temp != null) { // use the current offer and try next
local_min = Math.min(local_min, offer.get(offer.size() - 1) + helper(price, special, temp, i));
}
}
return local_min;
}
//直接购买的总价钱
private int directPurchase(List<Integer> price, List<Integer> needs) {
int total = 0;
for (int i = 0; i < needs.size(); i++) {
total += price.get(i) * needs.get(i);
}
return total;
}
DFS的优化
2、Backtracking with two optimizations (8ms)
- 防止backtracking中重复计算,记忆化搜索, Map<needs, minimum value for this needs>
- 防止计算完了special offer 1, specifal offer 2,再次计算special offer 2, specifal offer 1的情况
class Solution {
public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
HashMap<List<Integer>, Integer> map = new HashMap<>();
return helper(price, special, needs, 0, map);
}
private int helper(List<Integer> price, List<List<Integer>> special, List<Integer> needs, int pos,HashMap<List<Integer>, Integer> map) {
//边界值
if (price == null || price.size() == 0 || needs == null || needs.size() == 0) {
return 0;
}
//如果这个need输入对应的总价钱已经算过直接返回
if (map.containsKey(needs)) {
return map.get(needs);
}
int local_min = directPurchase(price, needs);
for (int i = pos; i < special.size(); i++) {
List<Integer> offer = special.get(i);
List<Integer> temp = new ArrayList<Integer>();
for (int j= 0; j < needs.size(); j++) {
//当前的special不满足
if (needs.get(j) < offer.get(j)) { // check if the current offer is valid
temp = null;
break;
}
temp.add(needs.get(j) - offer.get(j));
}
//使用special购买+剩下的need的DFS总价钱
if (temp != null) { // use the current offer and try next
local_min = Math.min(local_min, offer.get(offer.size() - 1) + helper(price, special, temp, i,map));
}
}
map.put(needs, local_min);
return local_min;
}
//直接购买的总价钱
private int directPurchase(List<Integer> price, List<Integer> needs) {
int total = 0;
for (int i = 0; i < needs.size(); i++) {
total += price.get(i) * needs.get(i);
}
return total;
}
}
其他思路
1、回溯法 (30ms)
The basic idea is to pick each offer, and subtract the needs. And then compute the price without the offer.
Pick whichever is minimum.
public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
int result = Integer.MAX_VALUE;
//apply each offer to the needs, and recurse
for(int i = 0; i < special.size(); i++) {
List<Integer> offer = special.get(i);
boolean invalidOffer = false;
for(int j = 0; j < needs.size(); j++) { // subtract offer items from needs
int remain = needs.get(j) - offer.get(j);
needs.set(j, remain);
if(!invalidOffer && remain < 0) invalidOffer = true; // if offer has more items than needs
}
if(!invalidOffer) { //if valid offer, add offer price and recurse remaining needs
result = Math.min(result, shoppingOffers(price, special, needs) + offer.get(needs.size()));
}
for(int j = 0; j < needs.size(); j++) { // reset the needs
int remain = needs.get(j) + offer.get(j);
needs.set(j, remain);
}
}
// choose b/w offer and non offer
int nonOfferPrice = 0;
for(int i = 0; i < needs.size(); i++) {
nonOfferPrice += price.get(i) * needs.get(i);
}
return Math.min(result, nonOfferPrice);
}
问题1
回溯太多次数,导致指数级别复杂度。
UPDATE 1 : For the below test case, we get time limit exceeded
since it's exponential. TLE due to needs=30+.
I've requested admins to add this testcase.
[2,5]
[[1,0,5],[1,2,10]]
[39,39]
优化
提前算出每个offer可能被需要的次数,贪心的思想。 (55ms)
So I made some optimization to reduce the recursive calls, by precomputing the number of times offer can be applied. See UPDATE 3, there’s an example that breaks this greedy optimization.
public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
int result = Integer.MAX_VALUE;
//apply each offer to the needs, and recurse
for(int i = 0; i < special.size(); i++) {
List<Integer> offer = special.get(i);
boolean invalidOffer = false;
int offerCount = Integer.MAX_VALUE; // number of times offer can be applied
for(int j = 0; j < needs.size(); j++) { // pre-compute number of times offer can be called
int remain = needs.get(j) - offer.get(j);
if(!invalidOffer && remain < 0) invalidOffer = true; // if offer has more items than needs
if(offer.get(j) > 0)
offerCount = Math.min(offerCount, needs.get(j)/offer.get(j));
}
for(int j = 0; j < needs.size(); j++) { // subtract offer items from needs
int remain = needs.get(j) - offer.get(j) * offerCount;
needs.set(j, remain);
}
if(!invalidOffer) { //if valid offer, add offer price and recurse remaining needs
result = Math.min(result, shoppingOffers(price, special, needs) + (offerCount * offer.get(needs.size())));
}
for(int j = 0; j < needs.size(); j++) { // reset the needs
int remain = needs.get(j) + offer.get(j) * offerCount;
needs.set(j, remain);
}
}
// choose b/w offer and non offer
int nonOfferPrice = 0;
for(int i = 0; i < needs.size(); i++) {
nonOfferPrice += price.get(i) * needs.get(i);
}
return Math.min(result, nonOfferPrice);
}
问题2
UPDATE 2: I think OJ is breaking with the below test case. My code handles it though. Expected output is 8000, since it has two items of 1$ each. I’ve requested to add the test case. Also, another assumption is that result doesn’t exceed Integer.MAX_VALUE. @administrators
[1,1]
[[1,1,2],[1,1,3]]
[4000,4000]
问题3
UPDATE 3: From @Red_Eden 's thought, I found a test case that breaks my optimization. OJ is missing this test as well. My solution gives answer = 6, but actual is pick one offer just once = 4.
[500]
[[2,1],[3,2],[4,1]]
[9]
总结
题目的输入限制是数量不能买多了,并且每个数量 0 <= N <=6,这个数量上贪心好像也是对的,没证明,带入了几个例子算了一下。
假设
问题3
DFS的输出
贪心法的输出