二分答案与贪心匹配:从COCI竞赛题解析算法实战与C++实现
2026/7/18 6:53:06 网站建设 项目流程

1. 项目概述:从一道COCI竞赛题看C++算法实战

最近在带学生刷信奥(信息学奥林匹克)题目时,遇到了P7305这道题,它源自COCI(克罗地亚信息学奥林匹克竞赛)2018/2019赛季的第一轮。这道题本身是一个经典的“二分答案”结合“贪心匹配”的问题,但它的价值远不止于AC(通过测试)。在我看来,它像是一个微型的“算法实验室”,能集中训练我们几个核心能力:对问题模型的抽象、对二分查找边界条件的精准把控、以及用C++标准库高效实现匹配逻辑的熟练度。很多初学者在接触二分答案时,总感觉思路懂了,代码一写就错,问题往往出在“区间开闭”和“检查函数”的设计细节上。这道题恰好提供了一个绝佳的练兵场。今天,我就以这道题为引子,不仅拆解它的标准解法,更想分享我在多年刷题和教学中总结出的、关于这类“最大值最小化”或“最小值最大化”问题的通用思考框架和C++实现技巧,希望能帮你把这道题的营养“吃干榨净”。

2. 问题核心与数学模型抽象

2.1 题目重述与关键信息提取

题目“Cipele”翻译过来是“鞋子”。简单来说,你有N只左脚鞋和M只右脚鞋,每只鞋都有一个尺寸。鞋子只能配对穿着,但允许左右脚鞋的尺寸存在一定差异。我们的目标是,为尽可能多的脚配对鞋子,并且使得所有配对中,左右脚尺寸差的最大值最小化。输出这个最小的最大尺寸差。

首先,我们需要把这段生活化的描述翻译成计算机能处理的数学模型。这里有三个关键约束:

  1. 一一配对:一只左脚鞋最多配一只右脚鞋,反之亦然。
  2. 差异容忍:配对的两只鞋尺寸差不能超过一个我们设定的阈值D。
  3. 目标双重性:首要目标是最大化配对数量(尽可能多的人有鞋穿),在满足最大配对数的前提下,次要目标是让所有配对中的最大尺寸差D尽可能小。

这立刻让我们联想到经典算法问题中的“二分图匹配”或“贪心匹配”。但仔细看,鞋子分左右脚,且我们关心的是最大差值的最小值,这是典型的“最小值最大化”问题,二分答案(Binary Search on Answer)是标准入口。

2.2 为什么选择二分答案?

“最小值最大化”或“最大值最小化”问题,其解空间(即可能的答案)通常是单调有序的。在这道题里,答案就是那个“最小的最大尺寸差”D。我们思考:如果允许的尺寸差D很大(比如无限大),那么所有鞋都能胡乱配上对,最大配对数很容易达到。如果D很小(比如为0),那么只有尺寸完全相同的左右鞋才能配对,配对数可能很少。这里存在一个单调性:随着允许的尺寸差D增大,我们能够成功配对的鞋子对数(最大匹配数)是单调不减的。我们的目标,就是找到最小的那个D,使得在该D下,能够配对的最大鞋子对数达到“可能的最大值”(即min(N, M))。

二分答案的精髓就在于,我们不去直接求解这个最优的D,而是假设一个D,然后设计一个检查函数check(D),来判断“当允许的最大尺寸差为D时,能否实现最大配对(即配对数达到 min(N, M))”。由于单调性的存在,如果某个D可行,那么所有比它大的D都可行;如果某个D不可行,那么所有比它小的D都不可行。这完美符合二分查找的应用条件。

注意:这里“最大配对数”是指在该D限制下,通过某种匹配策略能得到的最多对数。我们的check(D)函数需要判断这个“最大对数”是否等于理论最大值min(N, M)。在实际编码中,我们通常在check函数里实现一个贪心算法,力求配出尽可能多的对,然后看数量是否达标。

3. 算法思路详解与贪心策略证明

3.1 二分答案的框架搭建

确定了使用二分答案后,我们需要明确三个要素:

  1. 搜索范围(Left, Right):尺寸差D最小可能是0(当所有配对尺寸相同时),最大可能是什么?最坏情况是,左脚最大尺寸和右脚最小尺寸的差,或者左脚最小尺寸和右脚最大尺寸的差。更稳妥且简单的做法是,将左右脚所有鞋的尺寸放在一起排序,用最大值减最小值作为上界。但一个更安全、无需计算的上界是1e9(根据题目数据范围),或者直接用1e9+7。下界L = 0
  2. 循环条件与更新规则:这是二分查找最容易出错的地方。我强烈推荐使用“左闭右开”区间[L, R)的写法,其中R初始化为一个绝对不可行的较大值(或理论最大值+1)。这样,循环条件为while (L < R),更新规则为:
    • 如果check(mid)为真(当前D可行),说明答案可能更小或就是当前值,我们将搜索范围向右收敛:R = mid
    • 如果check(mid)为假(当前D不可行),说明答案必须更大,我们将搜索范围向左收敛:L = mid + 1。 循环结束后,LR的值就是我们要找的最小可行的D。这种写法能有效避免死循环和边界错误。
  3. 检查函数check(long long D):这是算法的核心,决定了二分答案的效率和质量。我们需要判断:在最大尺寸差不超过D的前提下,能否完成min(N, M)对配对。

3.2 贪心匹配策略的设计与实现

如何高效实现check函数?一个直观的想法是二分图最大匹配(如匈牙利算法),但时间复杂度为 O(N*M),在数据量较大时(题目中N, M可达1e5)会超时。我们必须寻找更高效的贪心策略。

观察发现,由于我们只关心尺寸差,并且鞋子是分左右的两组有序序列,一个经典的贪心策略是:将左右脚的鞋分别按尺寸排序,然后使用两个指针进行匹配

具体贪心策略如下:

  1. 将左脚鞋尺寸数组left和右脚鞋尺寸数组right分别按升序排序。
  2. 初始化两个指针i = 0(指向左脚),j = 0(指向右脚),以及计数器pairs = 0
  3. i < Nj < M的循环内:
    • 计算当前指针所指鞋子的尺寸差diff = abs(left[i] - right[j])
    • 如果diff <= D,说明当前这对鞋子可以配对。那么我们就配对它们,pairs++,然后i++,j++,同时移动两个指针去考虑下一只左脚和下一只右脚。
    • 如果diff > D,说明当前这对不能配对。那么我们应该移动指向尺寸较小的那只鞋的指针。因为数组已排序,移动较小尺寸的指针,才有可能让下一只鞋的尺寸更接近另一只,从而满足diff <= D
      • left[i] < right[j],则i++(尝试用下一只更大的左脚鞋来匹配当前这只右脚鞋)。
      • 否则,j++(尝试用下一只更大的右脚鞋来匹配当前这只左脚鞋)。
  4. 循环结束后,pairs就是在最大尺寸差D的限制下,通过此贪心策略能配出的最多对数。如果pairs == min(N, M),则check函数返回true,否则返回false

为什么这个贪心策略是正确的?我们需要证明,按照这个策略得到的配对数,就是全局最优解(即在该D下能配出的最大对数)。这可以通过“交换论证”或“贪心选择性质”来理解。核心思想是:对于已排序的序列,当前可匹配的鞋子中,如果左脚鞋left[i]和右脚鞋right[j]满足abs(left[i]-right[j]) <= D,那么将它们配对不会使结果变差。因为如果不配对left[i],它只能去匹配right[j]之后的更大的鞋子,这可能会“浪费”一个更接近的匹配机会,或者导致right[j]无法被任何左脚鞋匹配(因为left[i]是当前能匹配right[j]的最小左脚鞋)。这种“尽量匹配当前可匹配的对”的策略,保证了不会错过任何可能的配对,从而得到最大匹配数。许多竞赛题解都默认了这个结论,但理解其背后的直觉对于应对变种题至关重要。

实操心得:在编写check函数时,diff的计算可能会溢出。题目中尺寸值范围没说,但通常这种问题尺寸值用int存储足够。然而,在二分查找中,D的范围可能很大,left[i] - right[j]也可能很大,直接相减取绝对值可能溢出int。一个稳健的做法是在读入数据和计算时使用long long类型。这是信奥题目中非常常见的坑点。

4. 完整C++代码实现与逐行解析

理解了算法,我们来看代码实现。我会提供一份清晰、健壮且带有详细注释的代码。

#include <iostream> #include <vector> #include <algorithm> #include <cmath> // 用于 abs,但C++11中<cstdlib>或<algorithm>中的std::abs更好,这里用<cmath>亦可 using namespace std; typedef long long ll; // 习惯使用long long避免溢出 int n, m; vector<ll> leftShoes, rightShoes; // 检查函数:当允许的最大尺寸差为maxDiff时,能否配出min(n, m)对鞋子 bool check(ll maxDiff) { int i = 0, j = 0; // i指向左脚鞋,j指向右脚鞋 int pairs = 0; int targetPairs = min(n, m); // 理论最大配对数 while (i < n && j < m) { // 计算当前两只鞋的尺寸差,注意使用ll类型 ll diff = leftShoes[i] > rightShoes[j] ? (leftShoes[i] - rightShoes[j]) : (rightShoes[j] - leftShoes[i]); if (diff <= maxDiff) { // 可以配对 pairs++; i++; j++; // 如果已经达到目标对数,可以提前结束(小优化) if (pairs == targetPairs) return true; } else { // 不能配对,移动指向较小尺寸鞋子的指针 if (leftShoes[i] < rightShoes[j]) { i++; } else { j++; } } } // 循环结束,检查是否达到了目标配对数 return pairs == targetPairs; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); // 关闭同步,加速输入输出,对于大量数据很关键 cin >> n >> m; leftShoes.resize(n); rightShoes.resize(m); for (int i = 0; i < n; ++i) cin >> leftShoes[i]; for (int i = 0; i < m; ++i) cin >> rightShoes[i]; // 关键步骤1:分别对左右脚鞋的尺寸排序 sort(leftShoes.begin(), leftShoes.end()); sort(rightShoes.begin(), rightShoes.end()); // 关键步骤2:二分答案 ll left = 0; // 答案下界,最小可能差 ll right = 1e10; // 答案上界,设一个足够大的数(例如1e10),确保不可行 // 注意:这里使用左闭右开区间 [left, right) while (left < right) { ll mid = left + (right - left) / 2; // 防止溢出的标准写法 if (check(mid)) { // mid可行,答案可能在mid或更小,收缩右边界 right = mid; } else { // mid不可行,答案必须更大,收缩左边界 left = mid + 1; } } // 循环结束时,left == right,即为所求的最小可行D cout << left << endl; return 0; }

代码关键点解析:

  1. 输入加速ios::sync_with_stdio(false);cin.tie(nullptr);是C++竞赛中处理大量输入输出的标准操作,能显著提升效率。
  2. 排序:在二分查找之前必须对leftShoesrightShoes排序,这是贪心匹配正确性的前提。
  3. 二分查找实现
    • while (left < right)是左闭右开区间的典型循环条件。
    • ll mid = left + (right - left) / 2;是计算中点的安全方法,避免(left+right)/2可能导致的溢出。
    • 根据check(mid)的结果更新边界。当check(mid)为真时,说明mid是一个可行的解,但我们要找的是最小的可行解,所以答案区间变为[left, mid],即right = mid。当为假时,说明mid不可行,答案区间变为[mid+1, right),即left = mid + 1
  4. check函数细节
    • 使用targetPairs记录理论最大配对数min(n, m),逻辑清晰。
    • 在找到一对匹配后,立即判断pairs == targetPairs,如果成立则直接返回true,这是一个有效的提前终止优化。
    • 计算diff时,使用条件运算符手动计算绝对值,避免调用std::abs可能产生的类型转换问题(虽然这里用ll没问题),这也是一种更底层的控制。

5. 常见错误与调试技巧实录

即便思路清晰,实现这类题目时仍会踩坑。下面是我总结的几个常见错误点和调试方法。

5.1 二分查找的边界与死循环

这是最大的坑。常见的错误写法有:

  • while (left <= right)配合left = mid + 1right = mid - 1:这种闭区间写法对于标准二分查找可行,但对于“寻找第一个满足条件的值”这种问题,结束时leftright的关系需要仔细处理,容易出错。我推荐始终使用上面代码中的“左闭右开”写法,它对于“寻找第一个可行解”的问题非常直观和稳定。
  • 更新边界时混淆:一定要牢记逻辑:如果mid可行,那么答案可能是mid或更小,所以应该让right = mid(缩小右边界);如果mid不可行,答案一定比mid大,所以left = mid + 1。写反了会导致答案错误。
  • 初始上界right设置过小:如果right初始值小于真实答案,那么二分查找永远找不到解。保险起见,可以设一个非常大的数,如1e10LLONG_MAX(需包含<climits>)。

调试技巧:在二分查找的循环内打印left,right,midcheck(mid)的值。观察区间是如何收缩的,最终是否收敛到正确答案。对于小数据,可以手动模拟。

5.2 贪心匹配逻辑错误

  • 忘记排序:这是致命错误。贪心匹配策略严重依赖于左右脚数组都是有序的。如果忘记排序,匹配结果将是错误的。
  • 指针移动逻辑错误:在不能匹配时,必须移动尺寸较小的鞋子的指针。如果错误地总是移动左脚指针或右脚指针,在某些数据下会得到错误的配对数量。例如,左脚鞋尺寸为[1, 10],右脚鞋尺寸为[2, 3],D=1。正确策略应配对(1,2)和(10,? 无法配对),得到1对。如果错误移动大尺寸的指针,可能错过(1,2)的配对。
  • 数据类型溢出:尺寸值和尺寸差使用int可能溢出。统一使用long long是竞赛中的好习惯。

5.3 输入输出与性能

  • 未使用输入输出加速:当N, M达到1e5级别,输入数据量较大时,关闭cinstdio的同步能大幅提升速度。
  • check函数中频繁调用abs:对于内置算术类型,abs函数很快,但确保传入正确的类型。使用手动计算差值也是一种清晰的做法。

5.4 问题排查速查表

问题现象可能原因检查点与解决方法
输出答案比预期大二分查找边界更新错误;check函数逻辑错误导致某些D下本应可行却返回不可行。1. 检查二分循环更新条件(right=mid/left=mid+1逻辑)。
2. 用一组小数据测试check函数,手动模拟匹配过程。
输出答案比预期小check函数过于“宽松”,在某些D下本不可行却返回可行。重点检查贪心匹配策略的正确性,特别是尺寸差>D时指针的移动逻辑。
程序超时 (TLE)二分查找或check函数复杂度太高。1. 确保check函数是O(N+M)的贪心,而不是O(N*M)的暴力或匈牙利算法。
2. 确认输入输出已加速。
3. 检查二分查找的终止条件,避免死循环。
答案错误 (WA)多种可能。1.首先用极小数据测试:例如N=1,M=1,各种尺寸情况。
2.验证排序:在check函数前打印排序后的数组。
3.验证二分:在循环内打印left,mid,right,check(mid),看收敛过程。
4.检查数据范围:使用long long
运行时错误 (RE)数组越界。检查check函数中while循环的边界条件i < n && j < m,以及数组访问leftShoes[i],rightShoes[j]

6. 举一反三:算法模式与变种思考

解决P7305,掌握的不只是一道题,而是一类问题的解法。我们来做个延伸思考。

6.1 算法模式总结

这道题展示了“二分答案 + 贪心验证”的经典模式。其应用场景非常广泛,通常具有以下特征:

  • 问题要求最大化或最小化某个单一数值指标(如最大距离、最小时间、最大差值)。
  • 这个指标难以直接计算,但如果我们固定这个指标的值,判断是否可行则相对容易。
  • 固定指标值后的可行性判断(check函数)具有单调性:如果值X可行,那么所有比X更“宽松”的值(对于最小化问题是更大值,对于最大化问题是更小值)都可行。

同类信奥/竞赛题目包括:

  • 安排房间:有N个学生和M间房,每个房间容量固定,问如何安排使得学生间最大距离最小化。
  • 砍树:有N棵树,每棵高度不同,每次能砍掉固定高度,求最短时间使所有树高度一致。
  • 分发礼物:有N个礼物和M个孩子,每个孩子对礼物有偏好,求在满足最多孩子的情况下,最小化某个不满意度。

6.2 变种与挑战

你可以尝试修改题目条件,来加深理解:

  1. 如果鞋子不分左右脚(即任何两只鞋都能配对,只要尺寸差<=D),问题就变成了在数组中找最多对数。这可以用简单的贪心解决:排序后,相邻配对。
  2. 如果目标是“最大化这个最小尺寸差”(即配对成功的鞋子,其尺寸差至少为D,求最大的D)。这就变成了“最大值最小化”的对称问题。check函数需要判断是否存在至少K对鞋子,其尺寸差大于等于D。贪心策略可能需要调整。
  3. 如果每只鞋可以配对多次(无限制)?这通常不符合现实,但如果是其他资源分配问题,可能演变为网络流问题。

6.3 对C++学习的启示

通过这道题,我们巩固了:

  • STL的使用vector,sort。这是C++竞赛的基石。
  • 二分查找的精准实现:理解并熟练运用一种正确的、不易出错的二分模板至关重要。
  • 贪心算法的证明与实现:不仅要知道怎么做,还要大概知道为什么这样做是对的。
  • 复杂度分析:排序O(N log N + M log M),二分查找O(log K),每次检查O(N+M),总复杂度在可接受范围内。
  • 防御性编程:使用long long防溢出,注意输入输出效率。

最后,刷题的意义不在于AC的数量,而在于通过每一道经典题目,打通一类问题的任督二脉。P7305这道题,就像一把钥匙,帮你打开了“二分答案”和“有序序列贪心匹配”这两扇门。下次遇到类似“最小化最大值”的题目,不妨先想想:能不能二分?二分的check函数能不能用一个高效的贪心或判定算法来实现?多这样思考,你的算法能力自然会稳步提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询