题目描述
原题链接: LeetCode.1648 销售价值减少的颜色球
解题思路
- 题意很容易理解, 就是每次挑剩余同色球数量最大的颜色卖得到最大价值, 总共卖orders个球的最大总价值;
- 最快速直观暴力的解法是按照同色球数量排序, 每次取数量最大值累加到总价值中并且将数量减一后重新排序, 重复orders次即可。 第一反应是用堆排序求解, 堆顶是最大同色球数量, 每次堆顶减一后调整堆;
- 稍微用数组模拟一下就发现不需要堆排序将问题复杂化, 每次减一累加价值的解法效率太低, 进而想到基于排序后数据的一种快速解法:
- 将同色球数量从大到小排序后, 取当前最大值max以及第二大的值nextMax, 其中剩余同色球数量为max的颜色可能有maxCnt种;
- 在将所有max数量的同色球都卖到只剩nextMax之前, 最大价值都是逐个颜色将这些颜色的球卖出得到的, 即每种颜色都卖(max - nextMax)个得到累计 \(\sum_{i=nextMax+1}^{max} i\) * maxCnt 的价值;
- 如果将这批同色球卖到高于nextMax的某个值就已经卖足orders个, 假设maxCnt中有a种颜色球卖到finalMax, b种球卖到finalMax-1个, 这一轮卖出得到的价值之和为 \(\sum_{i=finalMax}^{max} i\) * maxCnt + b * finalMax;
- 如果卖到nextMax仍然没有卖够orders个, max更新为上一轮nextMax的值, 再向后遍历有序数组得到新的nextMax, 重复累加卖出价值的步骤;
- 排序时间复杂度是 O(n \(log_2 n\)), 后续循环遍历最坏情况下max和nextMax都是只相差1时性能降到O(n), 所以整体时间复杂度是 O(n + n \(log_2 n\));
- 上述思路代码提交后时间只击败了60%的用户, 显然并不高效, 看了眼其他人的示例代码才发现可以用二分法直接找到finalMax:
- 根据第一种思路模拟过程可以得到的结论是, 所有初始数量超过finalMax的颜色球卖到剩余finalMax或者finalMax-1个时能卖足orders个;
- 即必定存在唯一的finalMax值能满足\(\sum_{i=0}^{n-1} {Math.max(inv_i - finalMax, 0)}\) <= orders < \(\sum_{i=0}^{n-1} {Math.max(inv_i - finalMax + 1, 0)}\);
- 时间复杂度是 O(n \(log_2 M\)), 其中M是初始数量最大值;
- 由于初始数组长度不超过\(10^5\)但是数组元素最大值可以达到\(10^9\), 严格从时间复杂度角度来分析理论上二分查找的效率并不会高于排序解法, 但是提交结果显示二分法是最优的, 对此只能说数据用例倾向于二分查找的解法, 我自己也确实是因为提交发现性能不是最优才细看发现能用二分查找解决的, 学到了;
解题代码
-
排序模拟售出的思路:
final int MOD = 1_000_000_007; /** * 从大到小排序后,逐个计算卖出最高价值那批球的利润,等差递减序列求和的时候注意取余 * 执行用时: 26 ms , 在所有 Java 提交中击败了 61.54% 的用户 * 内存消耗: 53.91 MB , 在所有 Java 提交中击败了 12.82% 的用户 */ public int maxProfit(int[] inventory, int orders) { int len = inventory.length; if (len == 1) { return (int) (((long) orders * (inventory[0] + inventory[0] - orders + 1) >> 1) % MOD); } // 懒得自己写从大到小排序的自定义代码, 直接用API排序后再翻转数组实现了 Arrays.sort(inventory); for (int i = 0; i < (len >> 1); i++) { int tmp = inventory[i]; inventory[i] = inventory[len - 1 - i]; inventory[len - 1 - i] = tmp; } long ans = 0; // 当前剩余数量最多(即卖出价值最高)的颜色球价值, maxCnt是价值最高颜色球的种数 int max = inventory[0], maxCnt = 1; while (orders > 0) { while (maxCnt < len && inventory[maxCnt] == max) { maxCnt++; } int nextMax = maxCnt < len ? inventory[maxCnt] : 0; int diff = orders / maxCnt; if (diff == 0) { ans = (ans + (long) orders * max) % MOD; break; } if (diff > max - nextMax) { diff = max - nextMax; } // 从这maxCnt剩余数量为max的球中各自卖出diff个的总价值 long profit = (maxCnt * ((((long) max + max - diff + 1) * diff) >> 1 % MOD)) % MOD; max = max - diff; ans = (ans + profit) % MOD; orders -= diff * maxCnt; } return (int) ans; }
-
二分查找的解法:
/** * 二分查找确定卖完后的最大数量 * 执行用时: 13 ms , 在所有 Java 提交中击败了 94.87% 的用户 * 内存消耗: 53.84 MB , 在所有 Java 提交中击败了 43.59% 的用户 */ public int maxProfit(int[] inventory, int orders) { int max = 0; for (int i : inventory) { if (i > max) { max = i; } } int left = 0, right = max, t = 0; long tmpCnt = 0; while (left <= right) { int mid = (left + right) >> 1; // 数量超过mid的球卖到剩余mid时的数量之和是否超过orders long cnt = 0; for (int i : inventory) { if (i > mid) { cnt += (i - mid); // 不必每次都遍历完整数组求和, 提前break提升效率 if (cnt > orders) { break; } } } if (cnt <= orders) { t = mid; tmpCnt = cnt; right = mid - 1; } else { left = mid + 1; } } long ans = 0; for (int i : inventory) { if (i <= t) { continue; } // 等差数列求和公式计算数量从i递减到t的价值之和 ans = (ans + (((long) i - t) * (t + 1 + i) >> 1)) % MOD; } if (orders > tmpCnt) {// 还需要在卖到剩余t个的球中继续将部分颜色球依次卖1个才能满足orders值 ans = (ans + ((long) orders - tmpCnt) * t) % MOD; } return (int) ans; }