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

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

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

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

示例 1:

输入:nums = [1,0,0,1,0,1], k = 2
输出:1
解释:在第一次操作时,nums 可以变成 [1,0,0,0,1,1] 得到连续两个 1 。

示例 2:

输入:nums = [1,0,0,0,0,0,1,1], k = 3
输出:5
解释:通过 5 次操作,最左边的 1 可以移到右边直到 nums 变为 [0,0,0,0,0,1,1,1] 。

示例 3:

输入:nums = [1,1,0,1], k = 2
输出:0
解释:nums 已经有连续 2 个 1 了。

提示:

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

提示 1

Choose k 1s and determine how many steps are required to move them into 1 group.


提示 2

Maintain a sliding window of k 1s, and maintain the steps required to group them.


提示 3

When you slide the window across, should you move the group to the right? Once you move the group to the right, it will never need to slide to the left again.

解法1:前缀和 + 距离和 + 中位数贪心 + 滑动窗口

1. 初始化和收集1的位置

我们首先遍历数组nums,找到所有的1,并将每个1的位置(索引)相对于之前所有1的数量进行偏移后存入列表p中。

2. 计算前缀和

计算p列表的前缀和,presum[i]=sum(nums[0...i-1])。这将用于快速计算任意子数组的和。

3. 计算最小交换次数
  • lr变量定义了当前考虑的窗口的左右边界。
  • mid是窗口的中点,我们选择中点作为参考点来计算交换次数。
  • x的计算公式考虑了将窗口内的1移动到中点左侧和右侧所需的交换次数。具体来说:
    • p.get(mid) * (mid - l)是中点左侧的1的数量乘以它们需要向左移动的距离。
    • (presum[mid] - presum[l])是计算从lmid的前缀和,表示中点左侧的0的总数。
    • presum[r + 1] - presum[mid + 1]是计算从mid + 1r的前缀和,表示中点右侧的0的总数。
    • p.get(mid) * (r - mid)是中点右侧的1的数量乘以它们需要向右移动的距离。

每次循环,我们都会更新ans为当前窗口的最小交换次数。

4. 返回结果

最后,返回ans作为整个数组中包含k个连续1的最少交换次数。

为什么中位数是最优的?

选择中位数作为所有1聚集的位置可以最小化总的交换次数。如果选择的聚集位置不在中位数,那么一些1需要移动更远的距离,从而增加了交换次数。通过数学证明,我们可以知道,对于任意给定的1的索引序列,选择中位数作为聚集点总是最优的。

问:为什么图中的 x 取在中位数上是最优的?

答:首先,如果 x 取在区间 [p[0],p[k−1]] 之外,那么 x 向区间方向移动可以使距离和变小;同时,如果 x 取在区间 [p[0],p[k−1]] 之内,无论如何移动 x,它到 p[0] 和 p[k−1] 的距离和都是一个定值 p[k−1]−p[0],那么去掉 p[0] 和 p[k−1] 这两个最左最右的数,问题规模缩小。不断缩小问题规模,如果最后剩下 1 个数,那么 x 就取它;如果最后剩下 2 个数,那么 x 取这两个数之间的任意值都可以(包括这两个数)。因此中位数可以取 p[k/2]。

Java版:

class Solution {
    public int minMoves(int[] nums, int k) {
        List<Integer> p = new ArrayList<>();
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            if (nums[i] == 1) {
                p.add(i - p.size());
            }
        }
        int m = p.size();
        int[] presum = new int[m + 1];
        for (int i = 0; i < m; i++) {
            presum[i + 1] = presum[i] + p.get(i);
        }
        int ans = Integer.MAX_VALUE;
        for (int l = 0; l <= m - k; l++) {
            int r = l + k - 1;
            int mid = l + (k - 1) / 2;
            int x = p.get(mid) * (mid - l) - (presum[mid] - presum[l]) + presum[r + 1] - presum[mid + 1] - p.get(mid) * (r - mid);
            ans = Math.min(ans, x);
        }
        return ans;
    }
}

Python3版:

class Solution:
    def minMoves(self, nums: List[int], k: int) -> int:
        p = []
        for i in range(len(nums)):
            if nums[i] == 1:
                p.append(i - len(p))
        m = len(p)
        presum = [0] * (m + 1)
        for i in range(m):
            presum[i + 1] = presum[i] + p[i]
        ans = inf 
        for l in range(0, m - k + 1):
            r = l + k - 1
            mid = l + k // 2
            x = p[mid] * (mid - l) - (presum[mid] - presum[l]) + presum[r + 1] - presum[mid + 1] - p[mid] * (r - mid)
            ans = min(ans, x)
        return ans 
复杂度分析
  • 时间复杂度:O(n),其中 n 为 nums 的长度。
  • 空间复杂度:O(n)。
  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值