难度中等177
给你一个整数数组 nums
和一个整数 x
。每一次操作时,你应当移除数组 nums
最左边或最右边的元素,然后从 x
中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。
如果可以将 x
恰好 减到 0
,返回 最小操作数 ;否则,返回 -1
。
示例 1:
输入:nums = [1,1,4,2,3], x = 5 输出:2 解释:最佳解决方案是移除后两个元素,将 x 减到 0 。
示例 2:
输入:nums = [5,6,7,8,9], x = 4 输出:-1
示例 3:
输入:nums = [3,2,20,1,1,3], x = 10 输出:5 解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
提示:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^4
1 <= x <= 10^9
思路:
1.这题乍看起来很像搜索题,每次都可以选择数组的左端和右端
2.由于 nums[i]>=1,因此如果我们发现在当前这一步,x比当前数组的左端和右端都小,那么就不可能有可解方案,因此答案直接是-1
class Solution {
public:
int min_step = 0x7fffffff;
void DFS(vector<int>& nums, int L, int R, int x, int step){
if(step >= min_step) return;
if(!x) min_step = step;
if(x - nums[L] >= 0){
DFS(nums, L + 1, R, x - nums[L], step + 1);
}
if(x - nums[R] >= 0){
DFS(nums, L, R - 1, x - nums[R], step + 1);
}
}
int minOperations(vector<int>& nums, int x) {
DFS(nums , 0, nums.size() - 1, x, 0);
return min_step == 0x7fffffff ? -1 : min_step;
}
};
改进:在上述方法中加入了剪枝,但是实际上本题是算是一个最短路问题,说到最短路问题,第一个想到的自然就是BFS方法,我们对于每一个状态存储四个值[已走步数,区间左端点,区间右端点,剩余的x值],那么对于非法状态即rest_x<0时,我们并不生成其他节点,当rest=0时,拿到的步数一定是最少的
class Solution {
public:
struct state_node{
int step;
int L;
int R;
int rest_x;
};
int minOperations(vector<int>& nums, int x) {
int L = 0, R = nums.size() - 1;
queue<state_node> que;
state_node node;
que.push(state_node{0, L, R, x});
while(!que.empty()){
node = que.front();
que.pop();
if(node.rest_x == 0){
return node.step;
}
if(node.L <= node.R && node.rest_x - nums[node.L] >= 0){
que.push(state_node{node.step + 1, node.L + 1, node.R, node.rest_x - nums[node.L]});
}
if(node.L <= node.R && node.rest_x - nums[node.R] >= 0){
que.push(state_node{node.step + 1, node.L, node.R - 1, node.rest_x - nums[node.R]});
}
}
return -1;
}
};
最终解法:
现在我们换个角度看这个问题,其实题目可以这样看,从左边和右边任意选择几个连续的数,使得它们的和为x,如果我们引入前缀和,这道题是不是就变成了选择一个从左边开始的前缀和与一个从右边开始的前缀和,使得他们的和为x?
那么我们的思路就打开了,可以用的方法有很多:
1.定区间+二分
我们用left_sum[i]表示nums[0->i]的和,right_sum[i]表示nums[n>n-i]的和,n表示数组的最大索引。那么如果我们现在取一部分和为left_sum[i],现在我们要在right_sum[i]中找出值为x-left_sum[i]元素,但是需要注意的是,left_sum[i]表示我们选择了nums[0->i],那么我们在right_sum中寻找时,区间最多为[n->i+1],对应上right_sum的定义,right_sum可以取值的区间为right_sum[0->n-i-1],而由于nums[i]>0,因此我们可以保证left_sum与right_sum都是单调递增的,此时直接二分就行。
我们还需要对上述算法做一些改进,原因在于我们left_sum[i]与right_sum[i]里没有0元素,这会遇到什么问题呢?假如我们的答案是从左端任选j个元素,右端不选,抑或是左端不选,从右端任选k个元素,我们上述的算法是不能支持的,因此我们有必要加入0元素。
现在我们重新定义上述数组:
注:笔者为了强调right_sum数组是从最右端开始累加的,求和符合并没有满足数学里的定义即上面大下面小
现在我们重新考虑如何上述新定义的数组进行我们的算法:
如果我们选择了left_sum[i],那么就代表着选择了nums[0->i-1],剩余的可选择部分就是不选(即right_sum[0])以及nums[i->n](即,对应为right_sum[0->n+1-i]。
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int n = nums.size() - 1, left_sum[n + 5], right_sum[n + 5], i, min_step = 0x7fffffff, *p_right;
left_sum[0] = right_sum[0] = 0;
for(i = 0; i <= n; ++ i){
left_sum[i + 1] = left_sum[i] + nums[i];
right_sum[i + 1] = right_sum[i] + nums[n - i];
}
for(i = 0; i <= n; ++ i){
p_right = lower_bound(right_sum, right_sum + n + 2 - i, x - left_sum[i]); //左闭右开区间
if(p_right == right_sum + n + 2 - i){
continue;
}
if(*p_right == x - left_sum[i]){
min_step = min(min_step, int(i + p_right - right_sum));
}
}
return min_step == 0x7fffffff ? -1 : min_step;
}
};
注:笔者在这里如此定义right_sum会比较绕,但是可以直接调用库文件里的二分法;读者也可以定义为单调递减序列,但需要配套的二分法。
2.哈希集增删维护
上述方法定义的数组很绕,写代码比较容易出错,因此我们还可以直接维护集合。即在枚举左边序列和left_sum[i]的时候,维护一个集合,里面包含着从nums[n]开始最多到nums[i+1]的所有的和。当然也需要注意左边序列和或者右序列和为0的情况。
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
unordered_map<int, int> left_sum_map, right_sum_map;
unordered_map<int, int>::iterator iter;
int i, n = nums.size() - 1, left_sum = 0, right_sum = 0, min_step = 0x3f3f3f3f;
for(i = n + 1; i >= 0; -- i){
if(i <= n){
right_sum += nums[i];
}
right_sum_map[right_sum] = n + 1 - i;
}
for(i = -1; i <= n; ++ i){
if(i >= 0){
left_sum += nums[i];
iter = right_sum_map.find(right_sum);
right_sum_map.erase(iter);
right_sum -= nums[i];
}
if(right_sum_map.find(x - left_sum) != right_sum_map.end()){
min_step = min(min_step, right_sum_map[x - left_sum] + i + 1);
}
}
return min_step == 0x3f3f3f3f ? -1 : min_step;
}
};
我们也可以不删除right_sum_map里的元素,将那些不应该再被考虑的right_sum的步长调整为很大的值(无效值),使得其不能用来更新min_step。
小优化:
1.由于nums[i]>0,因此如果当前枚举的left_sum>x,则不需要再枚举,因为从右边选出来的最小值是0;同理,right_sum在生成的时候如果发现right_sum>x,也不需要再继续生成了。
2.如果所有元素的和sum<x,那么本题就不可能有解,因此直接返回-1即可。
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
unordered_map<int, int> left_sum_map, right_sum_map;
unordered_map<int, int>::iterator iter;
int i, n = nums.size() - 1, left_sum = 0, right_sum = 0, min_step = 0x3f3f3f3f;
for(i = n + 1; i >= 0; -- i){
if(i <= n){
right_sum += nums[i];
}
if(right_sum > x){
break;
}
right_sum_map[right_sum] = n + 1 - i;
}
if(right_sum < x){
return -1;
}
for(i = -1; i <= n; ++ i){
if(i >= 0){
left_sum += nums[i];
right_sum_map[right_sum] = 0x3f3f3f3f;
right_sum -= nums[i];
}
if(left_sum > x){
break;
}
if(right_sum_map.find(x - left_sum) != right_sum_map.end()){
min_step = min(min_step, right_sum_map[x - left_sum] + i + 1);
}
}
return min_step == 0x3f3f3f3f ? -1 : min_step;
}
};
最后一次优化,我保证这是最后一次(雾)
我们在上面为什么需要用哈希表存储呢,因为我们能够组合成答案的两个序列并不一定在原数组中相连,即nums[0,i]与nums[j,n]可能满足的情况是(j-i>1)。然而上面我们也说过了,这两个部分是满足单调递增的,既然我们要组合的数是固定的,那么如果left_sum变大,为了组合出x,很自然的想法是让right_sum变小,left_sum变大对应的是指针右移动,right_sum变小对应的也是指针右移。
接下来我们细化一下算法:
1.初始化R=0,left_sum=0,right_sum=sum(nums)
2.从L=-1开始,进入循环
A.如果L=-1,此时代表的是左边一个都不选,left_sum保持为0;如果L!=-1,那么代表需要从左边选出元素,此时需要更新left_sum=left_sum+nums[L]
B.让R移动到第一个满足left_sum+right_sum <= x的位置
C.判断left_sum+right_sum==x,更新答案
分析:首先我们可以知道,经过了2.B这个步骤后,一定满足left_sum+right_sum<=x
现在我们来看算法的合理性
如果left_sum+right_sum=x,那么下一步将left_sum变大后,应当右移R使得right_sum变小,否则不可能会得到left_sum+right_sum=x
如果left_sum+right_sum<x,那么下一步将left_sum变大后,有可能存在左移R(即变大right_sum)使得left_sum+right_sum=x的可能性吗?
为了细化这个问题,我们假设左边的区间为[0,i],右边的区间为[j,n],现在我们将左指针变大1,则左边的区间变为[0,i+1],我们需要看是否存在一种情况即右边区间选定[j-1,n]使得这两部分和为x。
实际上并不存在这种情况,我们看右指针右移动的情况发生在left_sum+right_sum > x时,既然右指针从j-1移动到了j,那么就说明,一定存在一个子区间[0,p](p<i),就已经使得sum(0,p)+sum(j-1,n)>x,而我们知道由于nums[i]>=1,所以必有sum(0,p)<sum(0,i),即sum(0,i)+sum(j-1,n)>x,而sum(0,i+1)>sum(0,i),故有sum(0,i+1)+sum(j-1,n)>x,因此可以保证两个指针只会向右移动。
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int i, n = nums.size() - 1, left_sum = 0, right_sum = 0, min_step = 0x3f3f3f3f, L, R;
for(R = n; R >= 0; -- R){
right_sum += nums[R];
}
if(right_sum < x) return -1;
for(L = -1, R = 0; L <= n; ++ L){
if(L != -1){
left_sum += nums[L];
}
while(R <=n && (R < L || left_sum + right_sum > x)){
right_sum -= nums[R++];
}
if(left_sum + right_sum == x){
min_step = min(min_step, L + 1 + n + 1 -R);
}
}
return min_step == 0x3f3f3f3f ? -1 : min_step;
}
};