初始思路:贪心 + 暴力搜索
思考过程
首先要搞清题意:所构造的数组 nums,每个元素都是正数,相邻的两个数相等或相差 1,所有元素的和不超过 maxSum。
题目的求解目标是使 nums[index] 尽可能大,对于最大或最小问题,要像条件反射一样,考虑动态规划和贪心算法。
为了使 nums[index] 尽可能大,那自然要让其他元素尽可能小。考虑到相邻的两个数相等或相差 1,那可以得到到一个山峰数列,index 是峰顶,其两侧的数逐个减小 1,直到最后遇到数组左右边界,或者减小到 1 后不再减小(因为元素必须是正数)。
山峰数列是我杜撰的词,不是专业词汇,仅仅是个比喻。
也就是说,index 的右侧,首先是一个公差为 -1 的递减的等差数列,递减到等于 1 后,后面还跟着 0 个或多个 1。所以右侧的第 i 个数,其大小等于 Math.max(sum[index] - i, 1)。
index 的左侧类似。
只要求出所有可能的山峰数列的和,找出和不超过 maxSum前提下的最大峰顶,这道题就解出来了。
计算机最擅长的就是暴力求解,加上我比较懒,我最喜欢暴力搜索了。这样写起来比较省时省事 🤣,也不容易出错。缺点是可能会超时。先不管超时,无脑暴力一番,过不了再改。
代码
class Solution {
public int maxValue(int n, int index, int maxSum) {
for(int ret = maxSum; ret >= 1; ret--){ // 从大到小暴力搜索
long sum = ret; // 注意用 long,防止整数溢出
for(int i = index + 1; i < n; i++) sum += Math.max(ret - i + index, 1);
for(int i = index - 1; i >= 0; i--) sum += Math.max(ret - index + i, 1);
if(sum <= maxSum)return ret;
}
return -1;
}
}
怎么样,这个代码是不是很简单?可惜,还是超时了,烦人:
复杂度
时间复杂度:,其中 。这道题中,峰顶 可能的取值范围是 1 到 maxSum,搜索空间的大小为 ,对每一种可能都要遍历 个数进行求和,所以总的时间复杂度是 。
复杂度:。
思路二:贪心 + 公式求和,优化到 O(m)
思考过程
其实一开始,直觉就告诉我,可以直接对这个山脉数列求和,毕竟只是个等差数列后面跟着一串 1。只要能在 内求和,那么复杂度就可以被优化到线性阶,大概率不会超时了。
设山峰处的大小为 ,先考山峰的右侧。假设右侧有 个数,则右侧的第 个数大小为 ,第 k 个数为 。
只要 ,则右侧一开始的等差数列后面,不会有多余的 1 了,于是右侧元素的和 为:
如果 ,则山峰右侧的前 个数是一个等差数列,右侧第一个数为 ,第 个数为 ;这个等差数列后面,还跟着 个 1。所以此时:
山峰左侧元素的和 ,求解公式是相同的。
代码
class Solution {
public int maxValue(int n, int index, int maxSum) {
int lCount = index, rCount = n - 1 - index; // 山峰 index 左右两侧的元素数量
for(int ret = maxSum; ret >= 1; ret--){ // 从大到小搜索
long sum = ret + helper(ret, lCount) + helper(ret, rCount);
if(sum <= maxSum)return ret;
}
return -1;
}
/*求山峰一侧的元素的和,max 为山峰大小,k 为某一侧的元素数量*/
private long helper(int max, int k){
if(max > k)return (long)k * (2 * max - 1 - k) / 2;
else return (long)max * (max - 3) / 2 + k + 1;
}
}
非常不幸,虽然看起来很完美,但是依然会超时。我抑郁了 🤣。
复杂度
时间复杂度 ,其中 。
空间复杂度 。
思路三:贪心 + 二分查找
思考过程
这道题没人性的点在于,线性复杂度,依然会超时。比线性阶还要低的,是对数阶。前两种解法,都是把题目转化成了搜索问题,二分查找也是一种搜索方式,且的复杂度是对数阶,很容易想到:能不能用二分查找来解?
通常情况下,适用二分查找的前提,是搜索空间具有某种单调性。这道题的搜索空间有没有单调性呢?有!
记数组的 index 处的元素大小为 时,对应的山峰数列的和为 ,那么必然有 :
其中 。这是因为,山峰等于 时,其左侧和右侧的第 个数,大小为 ,显然 , 故很容易得到式 1。
单调性得证,所以可用二分查找。
代码
/**
贪心 + 二分查找
执行用时:0 ms, 在所有 Java 提交中击败了100.00%
的用户内存消耗:39.1 MB, 在所有 Java 提交中击败了5.34%的用户
通过测试用例:370 / 370
*/
class Solution {
public int maxValue(int n, int index, int maxSum) {
int lCount = index, rCount = n - 1 - index; // 山峰左右两侧的元素数量
int l = 1, r = maxSum; // 二分查找的左右边界
while(l <= r){
int m = (l + r)/2;
long sum = m + helper(m, lCount) + helper(m, rCount);
if(sum > maxSum)r = --m;
else l = ++m;
}
return l - 1; // 此时,l 代表数组和恰好大于 maxSum 时的数组和。
}
/*求山峰一侧的元素的和,max 为山峰大小,k 为某一侧的元素数量*/
private long helper(int max, int k){
if(max > k)return (long)k * (2 * max - 1 - k) / 2;
else return (long)(max - 3) * max / 2 + k + 1;
}
}
复杂度
时间复杂度:,其中 。
空间复杂度 。
总结和扩展
一个万能二分查找模板
这道题思路并不复杂,关键是要在超时的时候,及时想到二分查找,并仔细地考察其正确性。
二分查找的原理很容易理解,但是二分查找就像茴香豆,有很多种写法,每种都需要仔细地处理各种边界条件。这里介绍我比较喜欢的一个万能模板。
给定一个单调不减数组 arr,和一个数 x,那么用下面的代码模板,可以查找出满足条件 arr[i] >= x 的最小 i:
class Solution {
/**
* 二分查找万能模板。 arr 是单调不减数组。
* 如果返回值等于 arr.length,代表 arr 中不存在 满足 arr[i] >= x 的 i,也就是所有元素都小于 x。
* 否则返回满足 arr[i] >= x 的最小 i(最左 i)。也就是说,如果有多个连续的 x,会返回最靠左的那个的下标。
* @param arr 单调不减数组
* @param x 边界值
* @return
*/
public int binarySearch(int [] arr, int x) {
int l = 0, r = arr.length - 1; // 二分查找的左右初始边界
while(l <= r){ // 注意这里不是 l < r
int m = l + (r - l)/2;
if(arr[m] >= x)r = --m;
else l = ++m;
}
return l; // 此时,l 代表arr[i] >= x 的最小 i。
}
}
之所以敢称这个模板是万能的,是因为它可以被灵活地调用,实现各种需求:
- 查找某个数
x首次出现的位置,如果不存在,返回-1。如果binarySearch(arr, x) == arr.length,代表所有元素都小于 x,不存在这样的位置;如果有多个元素等于 x,则binarySearch(arr, x)代表首个 x 的下标;如果arr[binarySearch(arr, x)] != x,则不存在某个数等于 x,binarySearch(arr, x)代表最靠左的大于 x 的数。 - 查找某个数
x最后出现的位置,如果不存在,返回-1。转换为用int ret = binarySearch(arr, x + 1) - 1来解决。 如果ret < 0,返回 -1;否则,如果arr[ret] == x,ret就是答案;否则,返回 -1。 - 查找某个数
x首次出现的位置,如果不存在x,则求出适合插入x的位置。binarySearch(arr, x)就是。 - 查找小于
x的最后一个数。转换为用binarySearch(arr, x) - 1来解决。 查找小于。这是个伪问题。x的第一个数- 查找大于
x的第一个的数。转换为用binarySearch(arr, x + 1)来解决。 查找大于。这是个伪问题。x的最后一个的数
如果把被查找的数组换成单调不增的,可以调整这个模板中的第五行中的 if 条件,把大于等于改为小于等于。
如果解二分查找的题比较耗时间,那么,从现在开始,把这个模板焊死在大脑里,并且找 10 道题练习一下。
如果在本文发现错漏,或者有更好的思路,或者有任何疑问,欢迎评论区交流。也可以点个赞支持一下作者 😊😊~~
本文首发于公众号「程序员老宋」,欢迎来逛逛~~