zl程序教程

您现在的位置是:首页 >  其他

当前栏目

LeetCode - 1703 得到连续 K 个 1 的最少相邻交换次数

LeetCode 连续 次数 交换 得到 最少 相邻
2023-09-14 09:04:04 时间

目录

题目来源

题目描述

示例

提示

题目解析

算法源码


题目来源

1703. 得到连续 K 个 1 的最少相邻交换次数 - 力扣(LeetCode)

题目描述

给你一个整数数组 nums 和一个整数 k 。 nums 仅包含 0 和 1 。每一次移动,你可以选择 相邻 两个数字并将它们交换。

请你返回使 nums 中包含 k 个 连续 1 的 最少 交换次数。

示例

输入nums = [1,0,0,1,0,1], k = 2
输出1
解释在第一次操作时,nums 可以变成 [1,0,0,0,1,1] 得到连续两个 1 。
输入nums = [1,0,0,0,0,0,1,1], k = 3
输出5
解释通过 5 次操作,最左边的 1 可以移到右边直到 nums 变为 [0,0,0,0,0,1,1,1] 。
输入nums = [1,1,0,1], k = 2
输出0
解释nums 已经有连续 2 个 1 了。

提示

  • 1 <= nums.length <= 105
  • nums[i] 要么是 0 ,要么是 1 。
  • 1 <= k <= sum(nums)

题目解析

本题和前面的

算法 - 最少交换次数来组合所有的1_伏城之外的博客-CSDN博客

算法 - 最少交换次数_伏城之外的博客-CSDN博客

题目类似,区别在于本题:“每一次移动,你可以选择 相邻 两个数字并将它们交换。”

而前面两题的交换是任意两个位置的数字可以直接交换。

因此本题的滑窗设计和前面略有不同。

比如

nums = [1,0,0,1,0,1], k = 2

如果才有下面这种滑窗设计,则是非常低效,不仅滑窗移动次数多,并且需要判断滑窗外部的1来到滑窗内部的最近距离

因此,我们不能采用上面滑窗设计。

题目要求,我们要让K个1组合到一起,我们可以直接让滑窗内含有K个1即可,如下

 可以发现,此时滑窗情况大大减少,并且我们只需要关注滑窗内部K个1的移动即可,不需要关注滑窗外部的1。

但是此时滑窗的移动是非连续的,如上图,第一个滑窗起始位置是0,第二个滑窗起始位置直接跳到了3,但是我们可以换个角度看问题,第一个滑窗的起始位置是第一个1,第二个滑窗位置起始位置是第二个1,这样看的话,滑窗的移动就是连续的了。

因此我们需要统计输入数列中1的位置,将输入数列转为1位置数列,如下图所示

 此时滑窗移动就可以转为下面连续情况

 滑窗的移动问题解决后,我们就可以着手解决滑窗内部K个1组合到一起的问题,比如下面情况,

我们需要将索引0的一和索引3的一组合到一起,则可以让有两种方式:

  1. 将索引0的一交换到索引2的位置,即2-0,需要交换2次
  2. 将索引3的一交换到索引1的位置,即3-1,需要交换2次

上面两种方式是等价的。

我们将nums中1元素的索引选取出来定义为数组ones,

 则当K=2时,每种滑窗情况需要交换:

  • ones[1] - ones[0] 次
  • ones[2] - ones[1] 次
  • ....
  • ones[ones.length - 1] - ones[ones.length - 2] 次

 我们比较上面每种滑窗的交换次数,取最小值。

上面是K=2时的情况,算是特殊情况,下面我们看下K>2的情况,比如

输入数组nums = [0,1,0,0,1,1,0,1,0,1,1,1,1,0,1,0],K=7

则滑窗情况如下

此时,我们会发现想要将7个1组合到一起,则情况非常多,比如第一种滑窗情况,我们可以选取一个1的位置,然后让剩余的1都往选取1的位置靠拢,那么就会有K种移动策略,我们要选取其中移动次数最少的那种策略。那么选取哪个位置的1的移动次数最少呢?

答案是中间位置的1。

我们通过下面例子来理解

可以发现,向中间位置的1靠拢所花费的交换次数是最少的。 

因此我们需要求解出滑窗内,中间位置的1的索引,求解方程如下

i + Math.floor(k/2),

i 是滑窗中第一个1的索引位置(注意 i 取自ones数组,而不是nums数组),k就是滑窗内1的个数

公式最终结果也是ones数组的索引位置

 比如上面图示中,第一个滑窗情况中间位置的1的所有是:0 + Math.floor(7/2) = 3,即ones[3]就是滑窗内中间位置1的索引。

求得中间位置1后,我们就可以让两边的1往中间位置1靠拢,比如上面第一个滑窗情况,中间位置1的索引是7,而中间位置左边有3个1,右边有3个1,因此

我们需要预留索引4,5,6给左边的1,预留8,9,10索引给右边的1。

即如下图中,橙色部分就是预留位置

 此时我们发现预留位置上已经有1了,因此我们判断如果预留位置有1的话,则跳过,但是这种方式会增加判断逻辑,让本就复杂的算法更加复杂。

实际上,我们有两种移动策略,让左边的1到预留位置

第一种,如上图,需要交换5次

 

第二种,如上图,需要交换5次

因此上面两种方式其实是等价的。但是第二种方式用代码实现起来更简单。

同理我们可以让中间位置1的右边1移动到预留位置

上面是基于nums的角度看的,我们再换一个视角用ones来看,红色数字就是预留位置

即让 左边的 索引5交换到6,4交换到5,1交换到4,交换次数一共是 (6-5) + (5-4) + (4-1) = 5

    让 右边的 索引9交换到8,10交换到9,11交换到10 ,交换次数一共是 (9-8) + (10-9) + (11-10) = 3

因此,每一种滑窗都需花费O(K)的时间复杂度来计算最少交换次数。

而滑窗的情况有O(ones.length - K + 1),差不多是O(n)复杂度,因此计算所有滑窗情况种的最少交换次数需要O(n^2)复杂度,并不符合题目要求。

因此我们需要优化。

下面是起始位置在第1个一的滑窗和 起始位置在第2个一的滑窗,它们存在交集区域,如下红框

这两个交集区域是有关系的,它们其中元素相同,而中心位置不同,但是中心位置是相邻的

第一个滑窗第二个滑窗
4→ 5       交换次数+1→ 6    交换次数+2
5→ 6       交换次数+1→ 7   交换次数+2
7中心位置,两边向我靠拢→ 8   交换次数+1
9→ 8      交换次数+1中心位置,两边向我靠拢
10→ 9      交换次数+1→ 10  交换次数+0
11→ 10    交换次数+1→ 11  交换次数+0

我们可以发现,中心位置从7变为9后,左边元素4,5的交换次数在原有基础上多加了一次,而这多出来的一次,其实就是上一次中心位置7 交换到 本次中心位置9 左边的8后,多出来的一次。

而右边元素10,11在原有基础上少了一次,二者少的一次,也是上一次中心位置7 交换到 本次中心位置9 左边的8后的影响导致的。

而无论是9到中心位置7的右边8,还是7到中心位置9的左边8的交换次数都是相同的。因此上面两个滑窗交集区域的交换次数差别,取决于7左边(不包含7)元素新增的交换次数,和9右边(不包含9)元素减少的交换次数  的 总和。

上面情况滑窗中元素个数刚好是奇数个,因此两个滑窗交集区域的7左边元素和9右边元素的个数是相同的,因此两个交集区域的交换次数是相等的。

如果滑窗中元素个数是偶数个,则此时导致两个滑窗的交集区域的交换次数不一致,如下例所示

上面图示中,第二个滑窗的交集区域要比第一个滑窗的交集区域的交换次数 多1次 。

当我们知道了两次滑窗交集区域的交换次数  关系后,我们就可以避免重新计算每一个元素到中心位置的交换次数了,直接基于推导的关系计算出来即可。

而滑窗在向右移动一次后,就会失去一个元素,比如上图失去了ones[0],新增了ones[26]

因此,我们只需要在上一个滑窗的交换次数基础上,减去ones[0]到上一次的中心位置one[13]的交换次数,加上ones[26]到本次的中心位置ones[14]的交换次数,再加上两次滑窗交叉区域的差异交换次数,就是最终的题解。

​​​​​​​

算法源码

var minMoves = function (nums, k) {
  // ones存储的是nums中1的索引位置
  let ones = [];
  nums.forEach((num, idx) => {
    if (num === 1) ones.push(idx);
  });

  // 如果K=1, 即只要求1个1相连,则不需要交换
  if (k === 1) {
    return 0;
  }
  // 如果K=2,即要求2个1相连,则只需要分别计算
  // ones[1] - 1 - ones[0],     第0个1  到 第1个1左边的位置的交换次数
  // ones[2] - 1 - ones[1],     第1个1  到 第2个1左边的位置的交换次数
  // ......
  // ones[ones.len-1] - 1 - ones[ones.len - 2],
  // 并求得以上所有结果中的最小值
  else if (k === 2) {
    let minSwapCount = ones[1] - 1 - ones[0];

    for (let i = 2; i <= ones.length - 1; i++) {
      minSwapCount = Math.min(minSwapCount, ones[i] - 1 - ones[i - 1]);
    }
    return minSwapCount;
  }

  // 如果K>2,即要求3个或3个以上1相连,则此时
  else {
    let minSwapCount = 0;

    // 我们需要先求得中间位置的1的索引mid(ones数组)
    let mid = Math.floor(k / 2);
    // 再求出中间位置左边预留位置left(nums数组)
    let left = ones[mid] - 1;
    // 然后将位于中间位置左边的1依次交换到预留位置
    for (let i = mid - 1; i >= 0; i--) {
      minSwapCount += left - ones[i];
      left--;
    }

    // 同理求出中间位置右边的预留位置right(nums数组)
    let right = ones[mid] + 1;
    // 然后将位于中间位置右边的1依次交换到预留位置
    for (let i = mid + 1; i < k; i++) {
      minSwapCount += ones[i] - right;
      right++;
    }
    // ⬆ 以上得到的minSwapCount,就是第一个滑窗的交换次数

    // 下面开始求解第一个滑窗之后的滑窗的交换次数,这里不再采用第一个滑窗的求解交换次数的方法,因为时间复杂度太大
    let tmpSwapCount = minSwapCount;
    // 第一个滑窗的起始位置是0(ones数组),则第二个滑窗的起始位置是1,最后一格滑窗的起始位置是ones.len - k
    for (let i = 1; i <= ones.length - k; i++) {
      // 本轮滑窗的中心位置curMid(ones数组)
      let curMid = Math.floor(k / 2) + i;
      // 上一轮滑窗的中心位置preMid(ones数组)
      let preMid = curMid - 1;
      // 计算两个中心位置间的交换次数
      let midSwapCount = ones[curMid] - 1 - ones[preMid];

      // 本轮和上一轮滑窗交集区域内,preMid左边的1个数
      let leftOneCount = preMid - i;
      // 本轮和上一轮滑窗交集区域内,curMid右边的1个数
      let rightOneCount = i + k - 2 - curMid;

      // 两轮滑窗交集区域交换次数的差异
      let diff = (leftOneCount - rightOneCount) * midSwapCount;

      // 本轮滑窗新增的1的索引位置
      let curRight = i + k - 1;
      // 本轮滑窗失去的1的所有位置
      let preLeft = i - 1;

      // 失去的1对应的交换次数
      let minusCount = ones[preMid] - (preMid - preLeft) - ones[preLeft];
      // 新增的1对应的交换次数
      let addCount = ones[curRight] - (ones[curMid] + (curRight - curMid));

      // 则本路滑窗的交换次数 = 上轮滑窗的交换次数 + diff -  minusCount + addCount
      tmpSwapCount = tmpSwapCount - minusCount + addCount + diff;

      // 比较本轮和上轮的交换次数,只保留最小的
      minSwapCount = Math.min(minSwapCount, tmpSwapCount);
    }

    return minSwapCount;
  }
};