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