多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

LeetCode 2134 最少交换次数:环形数组定长滑动窗口解法解析(codeforces-go 仓库题解深度解读)

LeetCode 2134 最少交换次数:环形数组定长滑动窗口解法解析(codeforces-go 仓库题解深度解读) 科学计算【免费下载链接】codeforces-go算法竞赛模板库 by 灵茶山艾府 项目地址https://gitcode.com/GitHub_Trending/co/codeforces-go点击查看免费下载本篇技术指南围绕第 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,nk-2]$。注意 $[n-1,nk-2]$ 的下一个窗口是 $[n,nk-1]$在环形数组中这与 $[0,k-1]$完全一样所以不需要再继续枚举窗口了。因此只需枚举 $n$ 个不同的窗口起点从 $0$ 到 $n-1$对应循环次数为 $nk-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$特判 $k0$然后以 $i0$ 到 $nk-2$ 滑动定长窗口进入窗口、更新答案、离开窗口三步循环最后返回 $k-\textit{max1}$。Python3class 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 nk-1所以 i-k1 n无需取模 return k - max1Javaclass 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 nk-1所以 i-k1 n无需取模 } return k - max1; } }Cclass Solution { public: int minSwaps(vectorint 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 nk-1所以 i-k1 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 nk-1所以 i-k1 n无需取模 } return k - max1; }Gofunc 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-k1] // 由于我们保证 i nk-1所以 i-k1 n无需取模 } return k - max1 }JavaScriptvar 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 nk-1所以 i-k1 n无需取模 } return k - max1; };Rustimpl Solution { pub fn min_swaps(nums: Veci32) - 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 nk-1所以 i-k1 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-k1]。由于循环保证 $i nk-1$所以 $i-k1 n$无需取模这是对细节 1 的代码级落实窗口起点最多到 $n-1$。滑动窗口一共迭代 $nk-1$ 次恰好覆盖环形数组中 $n$ 个不同的长度为 $k$ 的窗口最后一个窗口 $[n-1,nk-2]$ 的下一个 $[n,nk-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-k0$ 与实际情况没有外部 $1$ 可交换相吻合——此时 $0$ 根本无法被换出去但若数组不全为 $1$ 则 $kn$不存在此冲突。事实上当 $kn$ 时必有 $k-\textit{max1}0$返回 0 是正确的。五、仓库源码佐证Go 实现与反射式测试1. 官方 Go 实现仓库中该题的实现位于 b.go与题解文档中的 Go 版本逐行一致统计 $k$ → 特判 $k0$ → 定长窗口三步走 → 返回k - max1。特别值得注意的是第 16 行使用了for i : range n k - 1这种range 整数写法这是 Go 1.22 引入的语法糖等价于for i : 0; i nk-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 相邻因此长 $k3$ 的窗口 $[3,4,0]$ 或 $[4,0,1]$ 内已有 3 个 1$k-\textit{max1}3-30$无需交换。在仓库根目录执行以下命令即可运行验证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 灵茶山艾府 项目地址https://gitcode.com/GitHub_Trending/co/codeforces-go点击查看免费下载相关推荐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科学计算上一篇HarmonyOS MicroG 配置指南安装、权限设置与可用性验证下一篇5步实现WiFi穿墙感知RuView革命性空间智能技术终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表