拓冰建站拓冰建站
首页 / 资讯中心 / 正文

贪心算法实战:中位数思想解决字符串相邻交换最小化问题

1. 问题引入一道看似简单的“难题”在信息学奥赛的练习题库里有一道编号为1223的题目名字叫“An Easy Problem”。很多初学者看到这个标题第一反应可能是放松警惕——既然都叫“简单问题”了那应该不难吧但实际情况往往相反这道题恰恰是检验你是否真正理解“贪心算法”思想精髓的经典门槛。它不像动态规划那样有复杂的递推方程也不像搜索算法那样需要庞大的状态空间它的核心挑战在于你能否从看似无序或复杂的要求中抽丝剥茧找到一个“每一步都采取当前看来最优选择”的策略并且能严格证明这个局部最优的选择最终能导向全局最优解。这道题的具体描述通常是给定一个由‘0’和‘1’组成的字符串你可以进行一种操作——交换任意两个相邻的字符。题目要求求出为了使字符串中所有的‘1’都连续即所有的‘1’都挨在一起中间没有‘0’最少需要多少次相邻交换。举个例子对于字符串“1010”最少交换次数是1交换第二个字符‘0’和第三个字符‘1’得到“1100”。问题本身描述非常简洁但解法背后的贪心思路却值得深入咀嚼。今天我们就来彻底拆解这道“简单问题”不仅给出代码更要讲清楚贪心策略是如何被发现的为什么它是正确的以及在编码实现时有哪些意想不到的坑。2. 贪心策略的发现与证明为什么中位数位置是关键面对这个问题最直接的暴力想法是模拟所有可能的‘1’的最终连续段的位置然后计算每个位置下的移动代价。假设字符串长度为n其中有m个‘1’。那么这m个‘1’在最终状态下会占据一段连续的位置假设这段连续区间的左端点是L那么‘1’的位置就是L, L1, ..., Lm-1。我们的任务就是为原始的m个‘1’它们散布在字符串的各个位置找到新家L到Lm-1并使得所有‘1’移动的总距离最短这里“移动”被定义为相邻交换的次数而相邻交换次数正好等于一个字符需要移动的格子数。这听起来有点像“仓库选址”问题。我们有m个点‘1’的原始位置需要将它们移动到m个连续的位置上使得总移动距离最小。一个经典的结论是当目标位置也是一个序列时让原始点与目标点按顺序一一配对总距离最小。换句话说我们把所有‘1’的原始位置记录在一个数组pos[]里按从左到右的顺序然后我们选择一段长度为m的连续区间[L, Lm-1]作为目标位置。最优的匹配方式就是将pos[0]移动到Lpos[1]移动到L1……pos[m-1]移动到Lm-1。这样总移动代价就是 Σ |pos[i] - (L i)|其中i从0到m-1。那么问题就转化为如何选择这个左端点L使得上面这个总和最小我们令target[i] L i则代价为 Σ |pos[i] - target[i]|。仔细观察target[i]是一个公差为1的等差数列。我们的目标是调整L使这个等差数列与pos序列的“差距”最小。这里就用到另一个经典结论要使一组数到一组连续整数等差数列的距离之和最小当连续整数的公差为1时最优解是让target序列的中位数与pos序列的中位数对齐。更具体地说如果我们把pos[i]减去i得到一个新的序列b[i] pos[i] - i。那么我们的总代价公式可以重写 总代价 Σ |pos[i] - (L i)| Σ |(pos[i] - i) - L| Σ |b[i] - L|。看问题神奇地简化了现在我们需要找一个L使得它到数组b所有元素的绝对值之和最小。这是一个经典的“选址问题”在数轴上找一点使得到一组已知点的距离之和最小。结论是选择这组点的中位数作为L可以使得绝对值之和最小。因此最优的左端点L就是序列b的中位数。贪心策略由此浮出水面记录所有‘1’的原始位置索引从0开始或从1开始需统一存入数组pos。构造新数组b其中b[i] pos[i] - i。这里的i是pos数组中的序号0-indexed。b[i]的物理意义可以理解为如果最终连续‘1’段的第一个‘1’放在位置0那么第i个原始‘1’的理想“基准位置”是多少。实际上b数组的每个元素代表了一个“偏移”需求。求出b数组的中位数记为median_b。最优的连续段左端点L就等于median_b。计算总代价ans Σ |pos[i] - (L i)|。这个策略的贪心性质体现在我们并没有动态地考虑交换的相互影响而是通过数学转化直接找到了一个全局最优的“聚集点”L。每一步选择L都基于当前信息的全局最优最小化绝对值和并且一旦L确定匹配方案第i个‘1’去第Li个位置也是确定且最优的。证明的关键在于绝对值函数求和的最小值点在中位数这一数学性质上。3. 从理论到代码实现细节与边界处理理解了算法原理代码实现就相对清晰了。但其中仍有几个细节需要仔细处理否则极易出错。3.1 索引的统⼀与中位数计算首先需要明确字符串索引。通常输入字符串长度为n索引从0到n-1比较方便。我们遍历字符串将字符为‘1’的索引i存入pos列表。假设pos列表的长度为m。接着构建b数组。这里i是pos列表中的序号0到m-1。所以b[i] pos[i] - i。然后求b数组的中位数。中位数的定义对于有序数组如果数组长度m是奇数中位数是正中间那个数如果是偶数中位数是中间两个数的任意一个吗对于“使绝对值和最小”这个问题当点数为偶数时选取中间两个数构成的闭区间内的任意一点都能达到最小值。通常为了方便我们取中间两个数的下中位数或上中位数或者直接取它们的平均值但平均值可能不是整数而我们的L必须是整数。由于L必须是整数位置而b数组中的元素都是整数所以当m为偶数时取b[m//2 - 1]或b[m//2]作为中位数都可以。为了简化我们可以直接对b数组排序后取下标为m // 2的元素作为中位数无论是奇是偶这个下标都能给出一个有效的中位数候选在偶数时是上中位数。即b.sort() median_b b[m // 2] # 整数除法 L median_b这里L就是计算出的最优左端点。3.2 代价计算与溢出风险得到L后计算总代价ans 0 for i in range(m): ans abs(pos[i] - (L i))这里需要注意pos[i]、L、i都是整数计算出的代价可能很大。题目虽然没有明确给出数据范围但假设字符串长度n最大为10^6且所有字符都是‘1’那么代价可能达到O(n^2)级别显然会超过32位整数的范围。因此在C中应使用long long在Python中整数自动支持大数但也要注意计算效率。一个容易忽略的边界情况是计算出的L可能小于0或者Lm-1可能大于等于n吗我们来分析一下。b[i] pos[i] - i。因为pos[i]是递增的且pos[i] i最紧凑的情况是前m个位置都是‘1’所以b[i] 0。实际上b数组是非递减的。中位数median_b也必然大于等于0。所以L 0。另一方面L m - 1的最大值是多少考虑最坏情况所有‘1’都在字符串末尾。假设最后一个‘1’在索引pos[m-1] n-1。那么b[m-1] (n-1) - (m-1) n - m。中位数不会超过b[m-1]所以L n - m。因此L m - 1 (n - m) m - 1 n - 1。这说明我们计算出的最优连续段一定在原字符串的索引范围内是合法的。这个数学性质保证了我们无需对L进行额外的边界检查。3.3 代码实现示例Python下面给出一个清晰且高效的Python实现包含了输入处理和核心逻辑def min_swaps_to_group_ones(s: str) - int: # 1. 收集所有1的位置索引 positions [i for i, ch in enumerate(s) if ch 1] m len(positions) if m 1: # 0个或1个1无需交换 return 0 # 2. 构建b数组b[i] positions[i] - i b [positions[i] - i for i in range(m)] # 3. 找到b数组的中位数作为最优左端点L b.sort() median_b b[m // 2] L median_b # 4. 计算总代价 total_cost 0 for i in range(m): target_pos L i # 第i个1应该去往的目标位置 total_cost abs(positions[i] - target_pos) return total_cost # 示例用法 if __name__ __main__: # 假设从标准输入读取字符串例如 101001 # input_str input().strip() input_str 101001 result min_swaps_to_group_ones(input_str) print(f最少需要相邻交换次数: {result})这段代码的时间复杂度是O(m log m)主要来自对b数组的排序。其中m是字符串中‘1’的个数。空间复杂度是O(m)。对于长度很大的字符串如果‘1’的个数非常稀疏这个算法依然高效。4. 贪心算法的思维训练与常见误区解出这道题不仅仅是学会了一个模板更重要的是训练了一种化归的思维。面对一个复杂问题我们通过定义合适的数学模型pos数组目标序列代价公式然后利用已知的数学结论中位数最小化绝对值和来解决问题。这是贪心算法中非常高级的一种应用其正确性依赖于严谨的数学证明而非直观上的“显然”。在实际做题或面试中围绕这道题常见的误区和考察点有以下几个误区一误用“最近匹配”贪心。有人可能会想到这样的策略从左到右扫描遇到一个‘1’就把它往左移动直到碰到另一个‘1’或边界。或者总是交换当前最左边‘0’和最右边‘1’之类的局部操作。这些策略都是错误的无法保证得到最小交换次数。例如字符串“1001”错误策略可能先交换中间两个字符变成“1100”代价为1但正确的最优解其实也是1交换第一个‘1’和它右边的‘0’或者交换最后一个‘1’和它左边的‘0’。虽然这个例子结果相同但对于更复杂的串如“1010001”错误策略就会得到次优解。这道题的正解要求我们必须从全局出发找到那个最优的“聚集中心”L。误区二忽略索引转换的细节。在推导b[i] pos[i] - i时这个i是pos数组的序号而不是字符串的索引。如果这里搞混比如写成pos[i] - pos[0]之类的整个计算就会出错。务必理解i在这里代表的是“第几个1”它的目的是将最终目标位置序列L, L1, ...与pos序列对齐。误区三中位数计算处理不当。如前所述当m为偶数时中位数不唯一但取b[m//2]是简单有效的选择。有些初学者可能会尝试计算(b[m//2 - 1] b[m//2]) // 2但这样得到的L可能不是整数或者即使取整了也不一定比直接取其中一个更好。实际上对于绝对值最小化问题闭区间内的任意点都是最优解所以直接取数组中的一个元素b[m//2]是最稳妥的因为它一定是整数并且是b数组中的一个实际值保证了L是整数。考察点对“相邻交换”代价的深刻理解。这道题巧妙地将“相邻交换次数”等价为“移动格子的距离之和”。这是解决很多字符串相邻交换问题的基础。例如另一道经典题“使字符串平衡的最少交换次数”交换任意两个字符不一定是相邻其代价计算方式就完全不同。理解这种等价关系是灵活运用贪心思想的关键。5. 算法扩展与变式思考掌握了“An Easy Problem”的核心解法我们可以看看它的几种变式这有助于深化对贪心策略的理解。变式一移动‘0’而非‘1’。如果题目改为每次交换相邻字符求使所有‘0’连续的最少交换次数。解法完全一样吗是的完全对称。我们可以选择移动‘0’到连续位置也可以等价地认为移动‘1’因为字符串只有‘0’和‘1’。实际上使所有‘0’连续等价于使所有‘1’连续。所以算法无需改变计算‘1’的位置即可。变式二推广到多种字符。如果字符串不止‘0’和‘1’而是有多个字符比如‘a’, ‘b’, ‘c’要求使所有‘a’连续最少需要多少次相邻交换解法依然相同。我们只关心‘a’的位置将其视为“1”其他字符视为“0”问题就规约到了原问题。这是因为我们只移动‘a’而其他字符的相对顺序在交换过程中会被打乱但这不影响我们的目标——只要‘a’连续就行。代价计算依然只依赖于‘a’的初始位置和目标位置。变式三求最终连续段的可能位置。题目可能不仅要求最小交换次数还要求输出所有可能的最优连续段起始位置L。根据之前的分析当m为奇数时中位数唯一所以L唯一。当m为偶数时最优的L可以是b[m//2 - 1]到b[m//2]之间的任意整数。但L必须是整数且要保证连续段[L, Lm-1]在字符串范围内。所以我们需要找出这个区间内所有合法的整数L。计算方法是设low b[m//2 - 1],high b[m//2]则所有满足max(0, low) L min(n-m, high)的整数L都是最优解。这里n-m是为了保证Lm-1 n-1。变式四数据范围极大时的优化。如果字符串长度n高达10^9但‘1’的个数m只有10^5我们无法存储整个字符串。这时输入可能会给出‘1’的位置列表。我们的算法本身只依赖于pos数组所以完全可以处理。时间复杂度依然是O(m log m)。如果m也非常大比如10^7排序可能成为瓶颈。这时我们可以用线性时间选择算法如快速选择来找到中位数将时间复杂度降至O(m)。但一般情况下基于比较的排序已经足够高效且代码简洁。通过这道“简单问题”我们深入实践了贪心算法中“数学建模结论应用”的高阶玩法。它提醒我们很多看似需要复杂模拟或动态规划的问题经过巧妙的转化可以变成一个有经典结论的数学问题。核心在于训练自己将问题抽象化的能力以及熟悉一些基本的优化模型如中位数最小化绝对值和。下次再遇到类似“通过相邻交换使相同元素聚集”的问题你就能一眼看穿其本质快速给出优雅而高效的解法了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门