原题链接:LCP 24. 数字游戏
题目描述:
小扣在秋日市集入口处发现了一个数字游戏。主办方共有 N
个计数器,计数器编号为 0 ~ N-1
。每个计数器上分别显示了一个数字,小扣按计数器编号升序将所显示的数字记于数组 nums
。每个计数器上有两个按钮,分别可以实现将显示数字加一或减一。小扣每一次操作可以选择一个计数器,按下加一或减一按钮。
主办方请小扣回答出一个长度为 N
的数组,第 i
个元素(0 <= i < N)表示将 0~i
号计数器 初始 所示数字操作成满足所有条件 nums[a]+1 == nums[a+1],(0 <= a < i)
的最小操作数。回答正确方可进入秋日市集。
由于答案可能很大,请将每个最小操作数对 1,000,000,007
取余。
输入输出描述:
示例 1:
输入:
nums = [3,4,5,1,6,7]
输出:
[0,0,0,5,6,7]
解释: i = 0,[3] 无需操作 i = 1,[3,4] 无需操作; i = 2,[3,4,5] 无需操作; i = 3,将 [3,4,5,1] 操作成 [3,4,5,6], 最少 5 次操作; i = 4,将 [3,4,5,1,6] 操作成 [3,4,5,6,7], 最少 6 次操作; i = 5,将 [3,4,5,1,6,7] 操作成 [3,4,5,6,7,8],最少 7 次操作; 返回 [0,0,0,5,6,7]。
示例 2:
输入:
nums = [1,2,3,4,5]
输出:
[0,0,0,0,0]
解释:对于任意计数器编号 i 都无需操作。
示例 3:
输入:
nums = [1,1,1,2,3,4]
输出:
[0,1,2,3,3,3]
解释: i = 0,无需操作; i = 1,将 [1,1] 操作成 [1,2] 或 [0,1] 最少 1 次操作; i = 2,将 [1,1,1] 操作成 [1,2,3] 或 [0,1,2],最少 2 次操作; i = 3,将 [1,1,1,2] 操作成 [1,2,3,4] 或 [0,1,2,3],最少 3 次操作; i = 4,将 [1,1,1,2,3] 操作成 [-1,0,1,2,3],最少 3 次操作; i = 5,将 [1,1,1,2,3,4] 操作成 [-1,0,1,2,3,4],最少 3 次操作; 返回 [0,1,2,3,3,3]。
提示:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^3
解题思路:
首先我们要将前i个数变为公差为1的等差数列,相当于前i个数nums[i],每个nums[i]减去i,然后将前i个数变为相等的最少操作次数,将nums[i]减去i就是第0个位置减去0,第一个位置减去1,第二个位置减去2,依次类推,也就是说对于所有相邻的俩个位置i,i+1,第i+1个位置都比第i个位置多减去1,对于每个nums[i]减去i之后只需要再让每个数变为相等即可,此时就相当于将原来的前i个数变为公差为1的等差数列,所以我们先对每个nums[i]减去i之后,求对于所有的前i个数变为一样的最少操作次数即可,那么我们可以知道这是一个经典的问题,肯定是将前i个数都变为前i个数的中位数时操作次数最少,我们可以使用俩个堆来维护这前i个数的中位数,一个小根堆,一个大根堆,具体维护过程如下。
首先我们知道当i为偶数时,大根堆维护较小的i/2个数,小根堆维护较大的i/2个数,此时把所有数变为中位数的最少操作次数为小根堆中所有数的和减去大根堆中所有数的和。
当i为奇数时,大根堆维护较小的i/2个数,小根堆维护较大的i/2+1个数,设小根堆堆顶元素为x,此时小根堆的堆顶就是中位数,那么此时把所有数变为中位数的最少操作次数为小根堆中所有数的和减去x减去大根堆中所有元素的和。
当前面已经处理的元素个数为偶数时,我们先将当前数放入大根堆,然后将大根堆的堆顶元素弹出加入小根堆。
当前面已经处理的元素个数为奇数时,那么前面的小根堆里面元素个数比大根堆里面元素个数多1,此时将当前元素加入小根堆,然后将小根堆堆顶元素弹出加入大根堆。
时间复杂度:使用俩个堆维护中位数,时间复杂度为O(nlogn).
空间复杂度:使用俩个堆维护前i个数的中位数,空间复杂度为O(n).
cpp代码如下:
const int mod=1e9+7;
class Solution {
public:
vector<int> numsGame(vector<int>& nums) {
int n=nums.size();
vector<int>ans(n);
priority_queue<int>left; //大根堆
priority_queue<int,vector<int>,greater<int>>right; //小根堆
int leftsum=0,rightsum=0; //leftsum维护大根堆中元素的和,rightsum维护小根堆中元素的和
for(int i=0;i<n;i++)
{
int x=nums[i]-i; //先将每个元素nums[i]减去i
if(i%2==0){
left.push(x); //前面元素个数为偶数,先将当前元素插入大根堆,然后将大根堆最大元素加入小根堆
leftsum=(leftsum+x)%mod;
int v=left.top();
left.pop();
leftsum=(leftsum-v+mod)%mod;
right.push(v);
rightsum=(rightsum+v)%mod;
v=right.top();
ans[i]=(rightsum-v-leftsum+mod)%mod; //v是中位数
}else {
right.push(x); //前面元素个数是奇数,先将当前元素加入小根堆,然后将小根堆中最小元素加入大根堆
rightsum=(rightsum+x)%mod;
int v=right.top();
right.pop();
rightsum=(rightsum-v+mod)%mod;
left.push(v);
leftsum=(leftsum+v)%mod;
ans[i]=(rightsum-leftsum+mod)%mod;
}
}
return ans;
}
};