- 科学计算
【免费下载链接】codeforces-go
算法竞赛模板库 by 灵茶山艾府 💭💡🎈
本篇技术指南围绕第 275 场 LeetCode 周赛 B 题「最少交换次数来使所有的 1 围成一个圈」(题解文档位于 2134.md)展开,完整讲解如何把"环形数组中聚拢所有 1"的交换问题转化为定长滑动窗口求最小值问题,并逐行剖析 Python、Java、C++、C、Go、JavaScript、Rust 七种语言的实现。结合 codeforces-go 仓库中该题的 Go 实现、单测用例 与反射式测试框架 testutil,你可以掌握环形数组的窗口枚举边界、取模映射技巧,以及"统计 1 比统计 0 更方便"这类优化思维,并学会如何在本地复现并验证该题解。
一、题目本质:把"聚拢 1"转化为"交换次数"
设 $\textit{nums}$ 中有 $k$ 个 $1$。题目要求在环形数组 $\textit{nums}$ 中找到一个长为 $k$ 的子数组,并把这个子数组变成全 $1$ 子数组。
为什么要找"长为 $k$"的子数组?因为全 $1$ 子数组的长度必然是 $k$——数组里一共只有 $k$ 个 $1$,任何长度为 $k$ 的子数组若能变成全 $1$,恰好能容纳全部 $1$,不多不少。
关键在于交换次数的等价变换:
- 对于子数组中的 $1$,它已经在正确位置,无需操作;
- 对于子数组中的 $0$,可以把 $0$ 与在子数组外面的 $1$ 交换。不断交换,就能让所有 $1$ 都进入子数组中。
一次交换恰好消除子数组内的一个 $0$(子数组外的那个 $1$ 被换进来),因此子数组中有多少个 $0$,就需要交换多少次。$0$ 的个数越少,交换次数就越少。于是原问题被等价转化为:
求环形数组 $\textit{nums}$ 中,长为 $k$ 的子数组中 $0$ 的个数的最小值。
这一步转化是整个题解的灵魂:它把"交换"这种操作性很强的动作,翻译成"数 0"这种纯统计问题,从而可以直接套用滑动窗口。
二、定长滑动窗口:环形数组的枚举边界与细节
"环形数组上长度为 $k$ 的子数组"正是定长滑动窗口的经典应用场景。由于目标是求最小值,且窗口大小固定为 $k$,我们可以枚举所有可能的窗口起点,对每个窗口统计信息。
细节 1:窗口枚举的边界
本题是环形数组,第一个窗口是 $[0,k-1]$,最后一个窗口是 $[n-1,n+k-2]$。注意 $[n-1,n+k-2]$ 的下一个窗口是 $[n,n+k-1]$,在环形数组中,这与 $[0,k-1]$完全一样,所以不需要再继续枚举窗口了。因此只需枚举 $n$ 个不同的窗口起点(从 $0$ 到 $n-1$),对应循环次数为 $n+k-1$ 次迭代。
细节 2:下标取模
如果下标 $\ge n$,我们可以将其模 $n$,映射到闭区间 $[0,n-1]$ 中。代码中的nums[i % n]正是利用取模操作把"延长"后的数组坐标折叠回原数组,这也是处理环形数组最通用的手段。
细节 3:统计 1 比统计 0 更方便
统计 $1$ 的个数比统计 $0$ 的个数方便:因为窗口长度 $k$ 已知,若窗口内有 $c$ 个 $1$,则 0 的个数就是 $k-c$。所以可以先计算出窗口中的 $1$ 的个数的最大值 $\textit{max1}$,然后用 $k$ 减去最大值,得到 $0$ 的个数的最小值。这既减少了取反计算,也让代码语义更直观。
三、七种语言的完整实现
以下实现均出自 2134.md 题解,核心逻辑完全一致:先统计 $1$ 的个数 $k$;特判 $k=0$;然后以 $i=0$ 到 $n+k-2$ 滑动定长窗口,进入窗口、更新答案、离开窗口三步循环,最后返回 $k-\textit{max1}$。
Python3
class Solution: def minSwaps(self, nums: List[int]) -> int: k = sum(nums) # 1 的个数 if k == 0: # 没有 1,无需交换 return 0 n = len(nums) max1 = cnt1 = 0 for i in range(n + k - 1): # 1. 进入窗口 cnt1 += nums[i % n] if i < k - 1: # 窗口大小不足 k continue # 2. 更新答案 max1 = max(max1, cnt1) # 3. 离开窗口,为下一个循环做准备 cnt1 -= nums[i - k + 1] # 由于我们保证 i < n+k-1,所以 i-k+1 < n,无需取模 return k - max1Java
class Solution { public int minSwaps(int[] nums) { // 统计 1 的个数 int k = 0; for (int x : nums) { k += x; } if (k == 0) { // 没有 1,无需交换 return 0; } int n = nums.length; int max1 = 0; int cnt1 = 0; for (int i = 0; i < n + k - 1; i++) { // 1. 进入窗口 cnt1 += nums[i % n]; if (i < k - 1) { // 窗口大小不足 k continue; } // 2. 更新答案 max1 = Math.max(max1, cnt1); // 3. 离开窗口,为下一个循环做准备 cnt1 -= nums[i - k + 1]; // 由于我们保证 i < n+k-1,所以 i-k+1 < n,无需取模 } return k - max1; } }C++
class Solution { public: int minSwaps(vector<int>& nums) { int k = reduce(nums.begin(), nums.end(), 0); // 1 的个数 if (k == 0) { // 没有 1,无需交换 return 0; } int n = nums.size(); int max1 = 0, cnt1 = 0; for (int i = 0; i < n + k - 1; i++) { // 1. 进入窗口 cnt1 += nums[i % n]; if (i < k - 1) { // 窗口大小不足 k continue; } // 2. 更新答案 max1 = max(max1, cnt1); // 3. 离开窗口,为下一个循环做准备 cnt1 -= nums[i - k + 1]; // 由于我们保证 i < n+k-1,所以 i-k+1 < n,无需取模 } return k - max1; } };C
#define MAX(a, b) ((b) > (a) ? (b) : (a)) int minSwaps(int* nums, int n) { // 统计 1 的个数 int k = 0; for (int i = 0; i < n; i++) { k += nums[i]; } if (k == 0) { // 没有 1,无需交换 return 0; } int max1 = 0, cnt1 = 0; for (int i = 0; i < n + k - 1; i++) { // 1. 进入窗口 cnt1 += nums[i % n]; if (i < k - 1) { // 窗口大小不足 k continue; } // 2. 更新答案 max1 = MAX(max1, cnt1); // 3. 离开窗口,为下一个循环做准备 cnt1 -= nums[i - k + 1]; // 由于我们保证 i < n+k-1,所以 i-k+1 < n,无需取模 } return k - max1; }Go
func minSwaps(nums []int) int { // 统计 1 的个数 k := 0 for _, x := range nums { k += x } if k == 0 { // 没有 1,无需交换 return 0 } n := len(nums) max1, cnt1 := 0, 0 for i := range n + k - 1 { // 1. 进入窗口 cnt1 += nums[i%n] if i < k-1 { // 窗口大小不足 k continue } // 2. 更新答案 max1 = max(max1, cnt1) // 3. 离开窗口,为下一个循环做准备 cnt1 -= nums[i-k+1] // 由于我们保证 i < n+k-1,所以 i-k+1 < n,无需取模 } return k - max1 }JavaScript
var minSwaps = function(nums) { const k = _.sum(nums); // 1 的个数 if (k === 0) { // 没有 1,无需交换 return 0; } const n = nums.length; let max1 = 0, cnt1 = 0; for (let i = 0; i < n + k - 1; i++) { // 1. 进入窗口 cnt1 += nums[i % n]; if (i < k - 1) { // 窗口大小不足 k continue; } // 2. 更新答案 max1 = Math.max(max1, cnt1); // 3. 离开窗口,为下一个循环做准备 cnt1 -= nums[i - k + 1]; // 由于我们保证 i < n+k-1,所以 i-k+1 < n,无需取模 } return k - max1; };Rust
impl Solution { pub fn min_swaps(nums: Vec<i32>) -> i32 { let k = nums.iter().sum::<i32>() as usize; // 1 的个数 if k == 0 { // 没有 1,无需交换 return 0; } let n = nums.len(); let mut max1 = 0; let mut cnt1 = 0; for i in 0..n + k - 1 { // 1. 进入窗口 cnt1 += nums[i % n]; if i < k - 1 { // 窗口大小不足 k continue; } // 2. 更新答案 max1 = max1.max(cnt1); // 3. 离开窗口,为下一个循环做准备 cnt1 -= nums[i - k + 1]; // 由于我们保证 i < n+k-1,所以 i-k+1 < n,无需取模 } k as i32 - max1 } }实现要点逐条解读
以上代码共享同一个"三步走"骨架,理解它即可通读所有语言版本:
- 进入窗口:
cnt1 += nums[i % n],用取模把坐标 $i$ 折叠回 $[0,n-1]$,保证环形语义; - 窗口未满跳过:当 $i < k-1$ 时窗口大小不足 $k$,
continue跳过更新,直到第 $k-1$ 个元素进窗后窗口恰好满 $k$; - 更新答案:
max1 = max(max1, cnt1),维护所有窗口内 $1$ 的个数的最大值; - 离开窗口:
cnt1 -= nums[i-k+1]。由于循环保证 $i < n+k-1$,所以 $i-k+1 < n$,无需取模(这是对细节 1 的代码级落实:窗口起点最多到 $n-1$)。
滑动窗口一共迭代 $n+k-1$ 次,恰好覆盖环形数组中 $n$ 个不同的长度为 $k$ 的窗口(最后一个窗口 $[n-1,n+k-2]$ 的下一个 $[n,n+k-1]$ 与 $[0,k-1]$ 重合,停止枚举)。
四、复杂度分析与边界情况
- 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。统计 $k$ 一次遍历,滑动窗口一次遍历,总计线性时间;
- 空间复杂度:$\mathcal{O}(1)$,只用了常数个变量($k$、$\textit{cnt1}$、$\textit{max1}$),没有额外数组。
边界情况值得单独强调:
- $k = 0$:数组里没有 $1$,无需任何交换,直接返回 $0$。所有语言的实现都在开头做了这个特判,避免窗口长度为 0 时产生异常逻辑;
- $k = n$:整个数组就是唯一窗口,窗口内就是全部元素。若全部都是 $1$ 则返回 $0$;若中间有 $0$,则无论怎么交换都只能得到 $k-\textit{max1}=k-k=0$ 与实际情况(没有外部 $1$ 可交换)相吻合——此时 $0$ 根本无法被换出去,但若数组不全为 $1$ 则 $k<n$,不存在此冲突。事实上当 $k=n$ 时必有 $k-\textit{max1}=0$,返回 0 是正确的。
五、仓库源码佐证:Go 实现与反射式测试
1. 官方 Go 实现
仓库中该题的实现位于 b.go,与题解文档中的 Go 版本逐行一致:统计 $k$ → 特判 $k=0$ → 定长窗口三步走 → 返回k - max1。特别值得注意的是第 16 行使用了for i := range n + k - 1这种range 整数写法,这是 Go 1.22 引入的语法糖,等价于for i := 0; i < n+k-1; i++,在竞赛代码中更简洁。函数体内的max也是 Go 1.21 起内置的泛型函数,无需再自行实现比较逻辑。
2. 测试用例与本地验证
仓库为每道周赛题都配套生成了单测,见 b_test.go。三个官方样例覆盖了典型场景:
| 输入 | 说明 | 期望输出 |
|---|---|---|
[0,1,0,1,1,0,0] | 环形数组中 1 较分散 | 1 |
[0,1,1,1,0,0,1,1,0] | 9 个元素、5 个 1,需交换 2 次 | 2 |
[1,1,0,0,1] | 所有 1 已相邻(环形),无需交换 | 0 |
其中第三个用例[1,1,0,0,1]非常能检验环形语义:线性上看 1 被 0 隔开,但环形数组首尾相接,下标 4 的 1 与下标 0、1 的 1 相邻,因此长 $k=3$ 的窗口 $[3,4,0]$ 或 $[4,0,1]$ 内已有 3 个 1,$k-\textit{max1}=3-3=0$,无需交换。
在仓库根目录执行以下命令即可运行验证:
go test -v ./leetcode/weekly/275/b/测试由 testutil/leetcode.go 中的RunLeetCodeFuncWithExamples驱动:它通过反射把测试文件中的[][]string样例(输入数组、期望输出)解析为真实参数并调用被测函数 b.go 中的minSwaps,自动比对输出,并且支持-1之类的目标用例号定位到具体某个样例。这套"题目目录 + 题解 md + 实现 go + 测试 go"的组织方式,正是该仓库把每一道周赛题沉淀为可复现实验的标准流程(生成逻辑见 copypasta/template/leetcode/generator.go)。
3. 与仓库滑动窗口模板的呼应
从源码结构看,定长滑动窗口是该仓库高频使用的模式。例如 monotone_queue.go 中就有"滑动窗口最值(固定区间大小的区间最值)"的模板注释,指出单调队列可以解决滑动窗口区间最值问题;而本题因为窗口内只需要计数而非维护最值/单调性,用普通定长窗口即可 $\mathcal{O}(n)$ 完成,无需单调队列等高级结构——这也解释了为何题解将其归入"定长滑动窗口"这一基础类别。
六、专题训练与学习路径
本题是"定长滑动窗口"的入门模板题。题解文档末尾给出的专题训练指向滑动窗口题单的「一、定长滑动窗口」分类,建议按以下路线巩固:
- 定长滑动窗口入门:先掌握"进入窗口 → 窗口未满跳过 → 更新答案 → 离开窗口"的四步模板,再练习同类定长窗口计数题(如求给定长度子串中元音的最大个数等);
- 环形数组处理:本题的取模技巧
nums[i % n]是环形数组题目的通用手段,可进一步结合环形 DP、环形单调队列等进阶题型; - 窗口内容的变体:本题窗口内统计的是"1 的个数",若把统计对象换成"窗口内满足某条件的最小/最大值",则可与单调栈、单调队列(参考 monotone_queue.go)结合;
- 不定长与多指针:在掌握定长窗口后,可继续扩展至不定长滑动窗口、双指针/三指针等,仓库的滑动窗口题单覆盖了定长/不定长/单序列/双序列/三指针/分组循环等全部分类。
仓库中 leetcode/weekly/275 目录下还有同场周赛的 a(签到题)、c、d 三题及其题解 md,可作为同一场比赛中难度递进的对照学习材料。
七、总结
LeetCode 2134 的核心套路可以一句话概括:把"最少交换次数"翻译成"长为 $k$ 的环形子数组中 $0$ 的最少个数",再用定长滑动窗口 $\mathcal{O}(n)$ 求出 $k$ 减去窗口内 $1$ 的最大个数。这个转化过程体现了算法竞赛中典型的"操作等价变换"思维,而 2134.md 提供的七语言实现与 b.go + b_test.go 的仓库配套,让读者既能看到思路,也能在本地立即验证、反复实验。掌握本题后,定长滑动窗口的四步模板和环形数组的取模技巧都可以直接迁移到同类的区间统计问题中。
- 科学计算
【免费下载链接】codeforces-go
算法竞赛模板库 by 灵茶山艾府 💭💡🎈
相关推荐
LeetCode 1297「子串的最大出现次数」定长滑动窗口解法详解(codeforces-go 仓库实战)
LeetCode 1297「子串的最大出现次数」定长滑动窗口解法详解(codeforces go 仓库实战) 导读 本文围绕 LeetCode 1297「Max
科学计算codeforces-go 算法题解:LeetCode 2009 使数组连续的最少操作数(排序去重 + 定长滑动窗口)
codeforces go 算法题解:LeetCode 2009 使数组连续的最少操作数(排序去重 + 定长滑动窗口) 导读 本篇是 codeforces go
科学计算codeforces-go 题解精讲:LeetCode 2958「最多 K 次频率的最长子数组」——不定长滑动窗口的经典实战
codeforces go 题解精讲:LeetCode 2958「最多 K 次频率的最长子数组」——不定长滑动窗口的经典实战 本文以算法竞赛模板库 codefo
科学计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考