1. 问题背景与题目解析
今天我们来拆解LeetCode第1300题——"Sum of Mutated Array Closest to Target"。这是一道中等难度的算法题,主要考察对数组操作和二分查找的应用能力。题目要求我们将给定数组转变为一个特定形式的数组,使得转变后的数组和最接近目标值。
题目具体描述如下: 给定一个整数数组arr和一个目标值target,我们需要找到一个整数value,使得将数组中所有大于value的元素替换为value后,数组的和最接近target。如果有多个value满足条件,我们选择最小的那个。
举个例子: 输入:arr = [4,9,3], target = 10 输出:3 解释:当选择value=3时,数组变为[3,3,3],和为9,与target的差值为1;选择value=4时,数组变为[3,4,4],和为11,差值也是1。我们选择较小的value=3。
2. 解题思路分析
2.1 暴力解法与优化方向
最直观的解法是暴力枚举所有可能的value值,计算对应的数组和,然后找出最接近target的那个value。但是这种方法的时间复杂度是O(n*max(arr)),当数组元素很大时效率极低。
我们需要寻找更高效的解法。观察题目特点:
- 当value增加时,数组和单调不减
- 我们需要找到使数组和最接近target的value 这些特征提示我们可以使用二分查找来优化搜索过程。
2.2 二分查找的应用
二分查找通常用于在有序序列中快速定位目标值。在本问题中,我们可以将value的可能取值看作一个有序序列(从0到max(arr)),然后通过二分法快速定位最优value。
具体思路:
- 确定搜索范围:value的最小可能值是0,最大值是原数组的最大值(因为更大的value不会改变数组和)
- 在搜索范围内进行二分查找
- 对于每个中间值mid,计算对应的数组和
- 根据数组和与target的比较结果调整搜索范围
- 记录最接近target的value
3. 详细实现步骤
3.1 预处理与边界情况
首先处理一些边界情况:
- 如果数组和已经小于等于target,直接返回数组最大值(因为此时增大value只会使和更大,偏离target)
- 如果数组最小值乘以数组长度大于target,返回target/数组长度(因为此时所有元素都需要缩小)
def findBestValue(arr, target): arr.sort() n = len(arr) prefix = [0] for num in arr: prefix.append(prefix[-1] + num) # 边界情况处理 if prefix[-1] <= target: return arr[-1] if arr[0] * n >= target: value = target // n if abs(value * n - target) <= abs((value + 1) * n - target): return value else: return value + 13.2 二分查找实现
接下来实现二分查找的核心部分:
left, right = 0, arr[-1] best_value = 0 min_diff = float('inf') while left <= right: mid = (left + right) // 2 # 找到第一个大于mid的元素索引 index = bisect.bisect_right(arr, mid) current_sum = prefix[index] + (n - index) * mid current_diff = abs(current_sum - target) # 更新最优解 if current_diff < min_diff or (current_diff == min_diff and mid < best_value): min_diff = current_diff best_value = mid # 调整搜索范围 if current_sum < target: left = mid + 1 else: right = mid - 1 return best_value3.3 计算数组和的优化
为了快速计算转变后的数组和,我们使用了前缀和技巧:
- 先对数组排序
- 计算前缀和数组
- 对于给定的value,使用二分查找确定哪些元素需要被替换
- 数组和 = 不需要替换的元素和 + 需要替换的元素个数 * value
这种方法将每次计算数组和的时间复杂度从O(n)降低到O(logn),大大提高了整体效率。
4. 复杂度分析与优化验证
4.1 时间复杂度分析
让我们分析算法的时间复杂度:
- 排序数组:O(nlogn)
- 计算前缀和:O(n)
- 二分查找:O(log(max(arr)))
- 每次二分查找中的操作:O(logn)(bisect操作) 因此总时间复杂度为O(nlogn + log(max(arr)) * logn)
4.2 空间复杂度分析
空间复杂度主要来自:
- 存储排序后的数组:O(n)
- 前缀和数组:O(n) 因此总空间复杂度为O(n)
4.3 正确性验证
让我们验证几个测试用例:
示例1: 输入:[4,9,3], target=10 输出:3 验证:value=3时和为9,差值为1;value=4时和为11,差值也是1。选择较小的3,正确。
示例2: 输入:[2,3,5], target=10 输出:5 验证:value=5时和为10,正好等于target,正确。
边界情况: 输入:[1,2,3], target=100 输出:3 验证:数组和已经小于target,返回最大值3,正确。
5. 实际编码中的注意事项
5.1 整数除法的处理
在计算target//n时,需要注意Python的整数除法是向下取整。我们需要比较value和value+1两种情况:
value = target // n if abs(value * n - target) <= abs((value + 1) * n - target): return value else: return value + 15.2 等距离情况的处理
当两个不同的value对应的数组和与target的差值相等时,我们需要选择较小的value。这在二分查找的更新步骤中需要特别注意:
if current_diff < min_diff or (current_diff == min_diff and mid < best_value): min_diff = current_diff best_value = mid5.3 二分查找终止条件
二分查找的终止条件是left > right,但在此之前我们已经记录了最佳解。不需要等到循环结束才返回结果。
6. 算法优化与变种思考
6.1 双指针优化
在已经排序的数组中,我们可以使用双指针技术替代二分查找来定位需要替换的元素,这将进一步优化时间复杂度:
index = 0 while index < n and arr[index] <= mid: index += 16.2 浮点数解的可能性
如果允许value为浮点数,我们可以得到更精确的解。但题目要求value必须是整数,因此我们需要在相邻整数中选择更优解。
6.3 多目标优化
考虑扩展问题:如果有多个target需要处理,我们可以预先计算所有可能的value和对应的数组和,然后对每个target进行查询。这种情况下,预处理的时间可能更值得。
7. 完整代码实现
以下是完整的Python解决方案:
import bisect def findBestValue(arr, target): arr.sort() n = len(arr) prefix = [0] for num in arr: prefix.append(prefix[-1] + num) # 边界情况处理 if prefix[-1] <= target: return arr[-1] if arr[0] * n >= target: value = target // n if abs(value * n - target) <= abs((value + 1) * n - target): return value else: return value + 1 left, right = 0, arr[-1] best_value = 0 min_diff = float('inf') while left <= right: mid = (left + right) // 2 index = bisect.bisect_right(arr, mid) current_sum = prefix[index] + (n - index) * mid current_diff = abs(current_sum - target) # 更新最优解 if current_diff < min_diff or (current_diff == min_diff and mid < best_value): min_diff = current_diff best_value = mid # 调整搜索范围 if current_sum < target: left = mid + 1 else: right = mid - 1 return best_value8. 测试用例设计
为了确保代码的正确性,我们应该设计全面的测试用例:
- 常规情况:
assert findBestValue([4,9,3], 10) == 3 assert findBestValue([2,3,5], 10) == 5- 边界情况:
assert findBestValue([1,2,3], 100) == 3 # 数组和小于target assert findBestValue([100,200,300], 50) == 17 # 所有元素都需要缩小- 等距离情况:
assert findBestValue([1,2,3,4,5], 11) == 3 # sum=10和sum=12都差1,选较小的3- 极值情况:
assert findBestValue([], 10) == 0 # 空数组 assert findBestValue([5], 10) == 5 # 单元素数组9. 实际应用场景
这类问题在实际开发中有多种应用场景:
- 资源分配:在有限的资源(target)下,如何公平地限制每个用户的资源使用(value)
- 图像处理:像素值归一化时,如何选择截断阈值
- 数据压缩:在保持数据总和接近原数据的前提下,如何减少数据值的范围
理解这类问题的解法,有助于我们在面对实际工程问题时,能够快速识别问题模式并应用合适的算法解决。