https://leetcode-cn.com/problems/sell-diminishing-valued-colored-balls/
分析
很容易找到这道题的贪心策略:每次卖出价值最大(数目最多)的那种球。
我们可以使用最大堆来辅助获取最大的inventory[i]
,把它加到ans
后再把inventory[i] - 1
放回堆中,同时orders--
。这种思路很简单,但效率太低了,时间复杂度为
O
(
o
r
d
e
r
s
⋅
l
o
g
n
)
O(orders\cdot logn)
O(orders⋅logn),
n
n
n为球的种类。
解法一
最直观的加速方法是什么?我们可以获取到最大值inventory[i]
和次最大值inventory[j]
,然后将第i
种球卖出inventory[i] - inventory[j]
次并计算这个过程的利润。接下来会有两个最大值inventory[i] == inventory[j]
,需要再寻找次最大值inventory[k]
,然后将第i
和第j
种球都卖出inventory[i] - inventory[k]
次,并计算利润。重复这个过程直到orders == 0
,就找到了答案。
我们可以先对inventory
进行排序来帮助我们找到每一次计算的最大值和次最大值。
以示例3为例展示该算法的过程:
阶段一:[10, 8, 6, 4, 2] orders = 20
\qquad \quad 最大值10,次最大值8,有1个最大值。因此1种球卖2次,利润为1 * (10 + 9) = 19。
阶段二:[8, 8, 6, 4, 2] orders = 18
\qquad \quad 最大值8,次最大值6,有2个最大值。因此2种球分别卖2次,利润为2 * (8 + 7) = 30。
阶段三:[6, 6, 6, 4, 2] orders = 14
\qquad \quad 最大值6,次最大值4,有3个最大值。因此3种球分别卖2次,利润为3 * (6 + 5) = 33。
阶段四:[4, 4, 4, 4, 2] orders = 8
\qquad \quad 最大值4, 次最大值2,有4个最大值。因此4种球分别卖2次,利润为4 * (4 + 3) = 28。
总利润为110。
当orders
小于当前可以卖的总次数时,需要计算数目最多的那几种球分别可以卖多少次,剩余的orders
用来给这其中的某几种球再卖一次。
代码:
class Solution {
public:
int mod = 1e9 + 7;
int maxProfit(vector<int>& inventory, int orders) {
sort(inventory.rbegin(), inventory.rend());
long ans = 0;
int j = 0;
while (orders > 0) {
// [0, j) 范围内的颜色的球数 == MAX
while (j < inventory.size() && inventory[j] >= inventory[0]) j++;
int next = 0;
// 可从MAX一直卖到next + 1
if (j < inventory.size()) next = inventory[j];
long bucks = j, delta = inventory[0] - next;
long rem = bucks * delta; // 可卖的次数
if (rem > orders) {
int delta = orders / bucks; // 每种球可卖的次数
long a1 = inventory[0], an = a1 - delta + 1;
ans += ((a1 + an) * delta / 2) * bucks;
ans += (inventory[0] - delta) * (orders % bucks);
}
else {
long a1 = inventory[0], an = next + 1;
ans += ((a1 + an) * delta / 2) * bucks;
inventory[0] = next;
}
orders -= rem;
ans %= mod;
}
return ans;
}
};
解法二
解法一的核心思路在于对数目最多的那几种球进行售卖,直到它们的数目等于次最多。通过这种方法,我们其实能够推导出最终卖完后的inventory
与初始的inventory
之间的关系:假设最终最大的inventory
值为T
,那么有:
o
r
d
e
r
s
≥
∑
i
n
v
e
n
t
o
r
y
[
i
]
>
T
i
n
v
e
n
t
o
r
y
[
i
]
−
T
orders \geq \sum_{inventory[i] > T}inventory[i] - T
orders≥inventory[i]>T∑inventory[i]−T
即我们要把数目多于T
的球一直卖出到数目为T
。为什么取不等号?这对应于解法一所提及的orders
小于当前可以卖的总次数的情况。因此,如果卖出到T
时orders > 0
,那么可以将数目为T
的orders
种球再卖出一次,其数目最终为T - 1
。
显然,如果能找到这个T
,那么计算销售过程的总利润也就很简单了。
采用二分法来寻找满足上述不等式的最小的T
,如果我们把满足不等式看成一个条件的话,不难理解,这就是一个用二分法寻找左边界的问题。
代码
代码中用了一点小trick,为了防止求和溢出int
范围,采用相减的方式来判断是否满足不等式。
class Solution {
public:
int mod = 1e9 + 7;
int maxProfit(vector<int>& inventory, int orders) {
long left = 0, right = *max_element(inventory.begin(), inventory.end());
// 二分查找寻找左边界
while (left <= right) {
int mid = left + (right - left) / 2;
if (valid(inventory, mid, orders)) right = mid - 1;
else left = mid + 1;
}
long ans = 0;
for (int& num : inventory) {
if (num > left) {
long a1 = num, an = left + 1, n = num - left;
ans += ((a1 + an) * n / 2) % mod;
orders -= n;
}
}
if (orders > 0) ans += left * orders;
ans %= mod;
return ans;
}
bool valid(vector<int>& inventory, int k, int orders) {
for (int& num : inventory) {
if (num > k) orders -= (num - k);
if (orders < 0) return false;
}
return orders >= 0;
}
};
算法时间复杂度
O
(
n
⋅
l
o
g
M
)
O(n\cdot logM)
O(n⋅logM),
M
M
M为inventory
的最大值。