力扣【二分查找】:1300. 转变数组后最接近目标值的数组和(官方题解学习记录)
- 题目描述:
给你一个整数数组 arr 和一个目标值 target ,请你返回一个整数 value ,使得将数组中所有大于 value 的值变成 value 后,数组的和最接近 target (最接近表示两者之差的绝对值最小)。
如果有多种使得和最接近 target 的方案,请你返回这些整数中的最小值。
请注意,答案不一定是 arr 中的数字。
- 算法思路:
官方题解一共有两种方法,都涉及到对数组的排序与前缀和数组的使用,
第一种方式是对value枚举,对数组排序后就可以通过二分查找找到第一个满足不小于value的元素的位置,然后结合前缀和数组就可以O(1)的时间复杂度计算出数组和,
枚举所有情况时只在绝对值减小的时候更新,由于数组已经有序,所以后枚举到的数即便可以使绝对值与之前的绝对值相等,其枚举的value也依然大于之前的value,可以不用更新。
第二种方法是考虑在value的范围[0, max_elem]特别大时,枚举次数变多,而由于value枚举的范围是一个有序的范围,并且在value替换arr的成员后,其结果一定是一种严格的单调递增,
这里进行一个简单证明:
按照题目意思,设f(value) = Σmin(arr[i], value),因为value∈N,可以得到f(value + 1) = Σmin(arr[i], value + 1),
于是f(value + 1) - f(value) = Σ [min(arr[i], value + 1) - min(arr[i], value)],
设y = [min(arr[i], value + 1) - min(arr[i], value)],对每一个arr[i]有:
arr[i] >= value + 1,y = value + 1 - value = 1
arr[i] <= value,y = value - value = 0
而在arr一定存在至少一个max_elem > value,所以f(value + 1) - f(value) >= 1,所以f(value)随着value增长而严格单调递增,
所以当二分选取value时,可以确保每次选取value替换后的数组和都不一致,这样就可以通过两侧二分查找,外层是对value范围的二分查找,
内层是通过二分查找找到arr种第一个大于等于value的值的下标,
我们需要找到的是这样的两个value,f(value_lower) < target且绝对值是所有f(value_lower) < target中最小的,
f(value_upper) > target且绝对值是所有f(value_upper) > target中最小的,那么一定有value_lower + 1 = value_upper,通过反证法可以很容易得证,
所以只需要进行一次对value_lower的查找,然后比较f(value_lower)和f(value_lower + 1)谁与target更接近即可。
- 代码:
方法一:
时间复杂度:O((N + C)log(N)),其中N为arr长度,排序O(N log N),C为value的边界,枚举的每次都需要做二分查找 O(C log N)
空间复杂度:O(N),需要辅助的前缀和数组
int findBestValue(vector<int>& arr, int target) {
std::sort(arr.begin(), arr.end());
int size = arr.size();
std::vector<int> prefix(size + 1);
for (int idx = 1; idx <= size; ++idx) {
prefix[idx] = prefix[idx - 1] + arr[idx - 1];
}
int upper_bound = arr[size - 1];
int ans = 0; //实际的lower_bound
int diff = target;
for (int value = 1; value <= upper_bound; ++value) {
int idx = BinarySearch(arr, value);
int sum = prefix[idx] + (size - idx) * value;
if (std::abs(sum - target) < diff) {
ans = value;
diff = std::abs(sum - target);
}
}
return ans;
}
int BinarySearch(vector<int> &arr, int value) {
int left = 0, right = arr.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] < value) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
方法二:
时间复杂度:O(N log N),两层二分的时间复杂度应该是O(log(C) * log(N)),低于O(N log N),
空间复杂度:O(N)
int findBestValue(vector<int>& arr, int target) {
std::sort(arr.begin(), arr.end());
int size = arr.size();
std::vector<int> prefix(size + 1);
for (int idx = 1; idx <= size; ++idx) {
prefix[idx] = prefix[idx - 1] + arr[idx - 1];
}
int lower_bound = 0, upper_bound = arr[size - 1];
int value_lower = 0;
int diff = target;
while (lower_bound < upper_bound) {
int value = lower_bound + (upper_bound - lower_bound) / 2;
int idx = BinarySearch(arr, value);
int sum = prefix[idx] + (size - idx) * value;
if (sum < target) {
lower_bound = value + 1;
value_lower = value;
} else {
upper_bound = value;
}
}
int lower_abs = std::abs(CalculateSum(arr, value_lower) - target);
int upper_abs = std::abs(CalculateSum(arr, value_lower + 1) - target);
if (lower_abs <= upper_abs) {
return value_lower;
}
return value_lower + 1;
}
int CalculateSum(vector<int> &arr, int value) {
int sum = 0;
for (const int &num : arr) {
sum += std::min(num, value);
}
return sum;
}
int BinarySearch(vector<int> &arr, int value) {
int left = 0, right = arr.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] < value) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
浙公网安备 33010602011771号