1674.使数组互补的最少操作次数(差分数组)

77 阅读3分钟

使数组互补的最少操作次数

给你一个长度为 偶数 n 的整数数组 nums 和一个整数 limit 。每一次操作,你可以将 nums 中的任何整数替换为 1limit 之间的另一个整数。

如果对于所有下标 i下标从 0 开始),nums[i] + nums[n - 1 - i] 都等于同一个数,则数组 nums互补的 。例如,数组 [1,2,3,4] 是互补的,因为对于所有下标 inums[i] + nums[n - 1 - i] = 5

返回使数组 互补最少 操作次数。

示例 1:

输入:nums = [1,2,4,3], limit = 4
输出:1
解释:经过 1 次操作,你可以将数组 nums 变成 [1,2,2,3](加粗元素是变更的数字):
nums[0] + nums[3] = 1 + 3 = 4.
nums[1] + nums[2] = 2 + 2 = 4.
nums[2] + nums[1] = 2 + 2 = 4.
nums[3] + nums[0] = 3 + 1 = 4.
对于每个 i ,nums[i] + nums[n-1-i] = 4 ,所以 nums 是互补的。

示例 2:

输入:nums = [1,2,2,1], limit = 2
输出:2
解释:经过 2 次操作,你可以将数组 nums 变成 [2,2,2,2] 。你不能将任何数字变更为 3 ,因为 3 > limit 。

示例 3:

输入:nums = [1,2,1,2], limit = 2
输出:0
解释:nums 已经是互补的。

提示:

  • n == nums.length
  • 2 <= n <= 10^5
  • 1 <= nums[i] <= limit <= 10^5
  • n 是偶数。

暴力解法

两数的和最终只能是nums[i] + nums[n - 1 - i]或非数组中的任何一对的和other。当和为other时,由1 <= nums[i] <= limit可推出每对数只需变更一次即可得到,即n/2,再依次遍历每个nums[i] + nums[n - 1 - i]的和,求出其他对变更为该和时需要变更的次数后k,最后取min(k,n/2)即可,代码实现如下:

var minMoves = function (nums, limit) {
  let len = nums.length
  let res = len / 2
  for (let i = 0; i < len / 2; i++) {
    let sum = nums[i] + nums[len - 1 - i]
    let count = 0
    for (let j = 0; j < len / 2; j++) {
      let tmpSum = nums[j] + nums[len - 1 - j]
      if (tmpSum === sum) {
        continue
      } else {
        if (
          (limit + Math.max(nums[j], nums[nums.length - 1 - j]) >= sum &&
            tmpSum < sum) ||
          (tmpSum > sum &&
            Math.min(nums[j], nums[nums.length - 1 - j]) + 1 <= sum)
        ) {
          count += 1
        } else {
          count += 2
        }
      }
    }
    res = Math.min(res, count)
  }
  return res
}

此实现方式时间复杂度为O(n^2)

差分数组

假设res[x]表示nums[i]+nums[n-1-i]x时需要进行的操作次数,只需计算出所有的x对应的res[x],取最小值即可。

由题意可知x最小值为2,将两个数都换为1,最大值为2*limit,将两个数都换为limit

此时,问题转变为如何求出每个res[x]的值,假设nums[i] = A,nums[n-1-i] = B,则有:

  • res[x] = A + B时,操作次数是0(都不改)
  • res[x]在区间[1 + min(A,B),limit + max(A,B)]时,操作次数是1(只需修改A或B即可)
  • 否则操作次数为2(都改)

所以,我们可以遍历每一组nums[i]nums[n - 1 - i],然后更新res[x]的值:

  • 先将[2,2*limit]区间每个数+2
  • 再将[1 + min(A,B),limit + max(A,B)]区间每个数-1(即2 - 1 = 1,操作1次)
  • 再将[A + B] 位置的值 -1(即1 - 1 = 0,操作0次)

可以看出,整个过程都是在做区间更新。最后,我们查询每一个位置的值,取最小值就好。

对于这个需求,有一种非常常规的数据结构,叫差分数组,完全满足需求,并且编程极其简单,整体可以在 O(n) 的时间解决。

简单来说,差分数组 diff[i],存储的是 res[i] - res[i - 1];而差分数组 diff[0...i] 的和,就是 res[i] 的值。如果我们想给 [l, r] 的区间加上一

个数字 a, 只需要 diff[l] += a,diff[r + 1] -= a。

这样做,diff[0...i] 的和,就是更新后 res[i] 的值。示例如下:

// 原始数组
let res = [2, 4, 9, 1, 7, 5, 8]
// 差分数组
let diff = [2, 4 - 2, 9 - 4, 1 - 9, 7 - 1, 5 - 7, 8 - 5] // [2, 2, 5, -8, 6, -2, 3]
// diff数组依次求和得出res数组
let to = [
  2,
  2 + 2,
  2 + 2 + 5,
  2 + 2 + 5 - 8,
  2 + 2 + 5 - 8 + 6,
  2 + 2 + 5 - 8 + 6 - 2,
  2 + 2 + 5 - 8 + 6 - 2 + 3
] // [2, 4, 9, 1, 7, 5, 8]
// 将原始数组索引 1 ~ 3  都 +1
res = [2, 5, 10, 2, 7, 5, 8]
// 差分数组变更  diff[1] += 1  diff[3 + 1] -= 1
diff = [2, 3, 5, -8, 5, -2, 3] 
// diff数组依次求和得出res数组
to = [
  2,
  2 + 3,
  2 + 3 + 5,
  2 + 3 + 5 - 8,
  2 + 3 + 5 - 8 + 5,
  2 + 3 + 5 - 8 + 5 - 2,
  2 + 3 + 5 - 8 + 5 - 2 + 3
] // [2, 5, 10, 2, 7, 5, 8]

有了差分数组这个武器,这个问题就很简单了。代码实现:

var minMoves = function (nums, limit) {
  let diff = Array(2 * limit + 2).fill(0)
  for (let i = 0; i < nums.length / 2; i++) {
    let A = nums[i]
    let B = nums[nums.length - 1 - i]
    let l = 2
    let r = 2 * limit
    diff[l] += 2
    diff[r + 1] -= 2

    // [1 + min(A, B), limit + max(A, B)] 范围 -1
    l = 1 + Math.min(A, B)
    r = limit + Math.max(A, B)
    diff[l] += -1
    diff[r + 1] -= -1
    // [A + B] 再 -1
    l = A + B
    r = A + B
    diff[l] += -1
    diff[r + 1] -= -1
  }
  // 依次求和,得到 最终互补的数字和 i 的时候,需要的操作数 sum
  // 取最小值
  let res = nums.length,
    sum = 0
  for (let i = 2; i <= 2 * limit; i++) {
    sum += diff[i]
    if (sum < res) res = sum
  }
  return res
}

时间复杂度O(n)