ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蓝桥杯算法题解:最小乘积(基本型)的贪心策略与排序不等式应用

蓝桥杯算法题解:最小乘积(基本型)的贪心策略与排序不等式应用 1. 问题引入从“最小乘积”到算法竞赛的思维训练最近在整理蓝桥杯的历年真题翻到了ALGO-53这道“最小乘积(基本型)”。题目名字听起来挺数学的但实际做下来发现它更像是一个精巧的思维体操考察的是对问题本质的洞察和基础算法的灵活运用而不是复杂的数学推导。很多刚接触算法竞赛的同学一看到“乘积”、“最小”这些词容易下意识地往动态规划或者贪心算法的复杂模型上套结果往往把简单问题复杂化代码写了一大堆最后还超时或者出错。这道题就是一个很好的反面教材——它用最朴素的逻辑教会我们如何化繁为简。这道题的核心场景是这样的给你两组数每组数的个数相同。你可以任意调整每组数内部的顺序。调整之后将两组数相同位置上的数一一对应相乘然后把所有的乘积加起来得到一个总和。我们的目标就是通过调整顺序让这个总和最小。举个例子A组是[1, 3, -5]B组是[-2, 4, 1]。如果我们把A排成[-5, 1, 3]B排成[4, -2, 1]那么对应乘积和为 (-54) (1-2) (3*1) -20 -2 3 -19。但这是最小的吗我们稍后验证。为什么这道题值得拿出来单独讲因为在各类编程比赛和面试中这种“通过调整顺序优化某个目标值”的问题非常常见。它剥离了复杂的数据结构和算法外壳直指一个最核心的编程思想如何利用已知条件找到问题的最优结构。理解这道题不仅能帮你轻松拿下蓝桥杯的这一分更能为你解决更复杂的调度问题、资源分配问题打下坚实的思维基础。接下来我们就一层层剥开它的外壳。2. 核心猜想与证明为什么“大配小”能实现最小和面对这个问题我们的第一直觉可能是穷举所有排列组合。对于长度为n的数组排列有n!种两组数就是(n!)^2种组合当n稍微大一点比如10计算量就是天文数字必然超时。所以我们必须寻找规律。一个非常自然的猜想是要让乘积和最小应该让一组中最大的数去乘以另一组中最小的数。更一般地说将一组数按升序排列另一组数按降序排列然后对应位置相乘相加得到的就是最小和。这个猜想通常被称为“排序不等式”的逆应用。我们来严谨地证明一下这个猜想。假设我们有两个正序序列A序列满足 a1 ≤ a2 ≤ ... ≤ anB序列满足 b1 ≤ b2 ≤ ... ≤ bn。根据排序不等式正序和最大配最大是最大的a1b1 a2b2 ... an*bn ≥ 乱序和 ≥ 逆序和最大配最小。而我们的目标是求最小和那么逆序和即a正序配b逆序就应该是候选。但题目并没有说数组元素都是正数。如果包含负数呢我们考虑更一般的情况。设S a1b1 a2b2 ... anbn。如果我们交换其中一对组合(i, j)看看和的变化。 交换前贡献aibi ajbj 交换后贡献aibj ajbi 变化量 Δ (aibj ajbi) - (aibi ajbj) ai(bj - bi) aj*(bi - bj) (ai - aj)*(bj - bi)。我们的目标是让S最小。如果一次交换能使S变小即Δ 0那么我们就应该进行这次交换。 Δ (ai - aj)*(bj - bi) 0。 这个不等式成立的条件是(ai - aj) 和 (bj - bi) 异号。情况1ai aj 且 bj bi。即A中较小的数对应B中较大的数A中较大的数对应B中较小的数。这听起来已经有点像“逆序”配对了。情况2ai aj 且 bj bi。这和情况1是对称的。这告诉我们任何“正序”配对即A中较大的数对应B中较大的数都不是最优的因为我们可以通过交换使其变得更小。通过不断地进行这种使Δ0的交换最终系统会稳定在一种状态对于任意的i, j都有(ai - aj)*(bj - bi) 0。这意味着(ai - aj)和(bj - bi)总是同号。一种满足这个条件的稳定状态就是A完全升序B完全降序或反之。此时对于任意ij我们有 ai ≤ aj 且 bi ≥ bj。那么(ai - aj) ≤ 0, (bj - bi) ≤ 0乘积大于等于0满足稳定条件。此时的总和S就是局部极小值也是全局最小值。因此无论数组元素是正、负还是零最优策略都是将一组数组升序排列另一组数组降序排列然后对应位置相乘并求和。这个结论是普适的也是我们解题的黄金法则。注意这里有一个非常关键的思维陷阱。有同学会想是不是应该把两组数都按升序排然后从头到尾乘这是求最大和的方法正序和最大。而我们要求最小和所以必须是一升一降。在编码时千万要分清目标排序的方向直接决定了结果的正确性。3. 解题步骤详解与C/C代码实现理解了原理实现就变得异常简单。整个解题过程可以分解为几个清晰的步骤我们用C来描述因为这是蓝桥杯的主流语言之一其sort函数和向量容器用起来非常方便。C语言的实现思路完全一致只是输入输出和排序函数需要调整。3.1 输入处理与数据结构选择首先我们需要读入数据。题目通常的格式是先给一个整数T表示测试用例的个数。对于每个用例先读入整数n数组长度然后读入两个长度为n的数组A和B。在C中我们通常使用vectorint来存储这些数组因为它动态大小使用方便。当然用原生数组int a[1000]也可以但要提前声明足够大的固定尺寸。#include iostream #include vector #include algorithm // 用于sort函数 using namespace std; int main() { int T; cin T; while (T--) { int n; cin n; vectorint a(n), b(n); for (int i 0; i n; i) cin a[i]; for (int i 0; i n; i) cin b[i]; // ... 后续处理 } return 0; }3.2 核心排序操作根据第2部分的结论我们对数组A进行升序排序对数组B进行降序排序。// 对a进行升序排序 sort(a.begin(), a.end()); // 对b进行降序排序。sort默认是升序降序需要传入比较函数greaterint() sort(b.begin(), b.end(), greaterint());这里有一个非常重要的细节greaterint()是一个函数对象仿函数它定义了“大于”的比较关系从而实现降序。也可以使用Lambda表达式sort(b.begin(), b.end(), [](int x, int y){return x y;});。对于C语言你需要自己实现一个cmp函数传给qsort例如int cmp_desc(const void* a, const void* b) { return *(int*)b - *(int*)a; }。3.3 计算最小乘积和并输出排序完成后最小乘积和就是对应位置元素的乘积之和。long long min_sum 0; // 使用long long防止乘积求和时溢出 for (int i 0; i n; i) { min_sum (long long)a[i] * b[i]; // 注意类型转换确保乘法在long long范围内进行 } cout min_sum endl;为什么用long long这是算法题中极其常见的坑点。题目虽未明确给出数据范围但n可能达到1000每个数的绝对值也可能很大比如10^4。那么单个乘积最大可能是10^8求和后最大可能是10^11这已经超过了32位int约2*10^9的表示范围。使用long long通常是64位是安全的编程习惯。在C语言中对应的是long long类型输出用%lld。3.4 完整代码整合与测试将以上部分整合就得到了完整的AC代码。我们再用之前的例子测试一下A[1,3,-5], B[-2,4,1]。排序后A升序 - [-5, 1, 3]B降序 - [4, 1, -2] (注意降序是4, 1, -2)乘积和 (-5)4 11 3*(-2) -20 1 - 6 -25。我们之前随意配对的-19比-25大验证了我们的算法确实找到了更小的和。你可以尝试其他配对会发现-25就是最小值。#include iostream #include vector #include algorithm using namespace std; int main() { int T; cin T; while (T--) { int n; cin n; vectorint a(n), b(n); for (int i 0; i n; i) cin a[i]; for (int i 0; i n; i) cin b[i]; sort(a.begin(), a.end()); // A升序 sort(b.begin(), b.end(), greaterint()); // B降序 long long ans 0; for (int i 0; i n; i) { ans (long long)a[i] * b[i]; } cout ans endl; } return 0; }对于C语言版本代码会稍长一些主要区别在于需要使用qsort和scanf/printf。#include stdio.h #include stdlib.h // 升序比较函数 int cmp_asc(const void* a, const void* b) { return *(int*)a - *(int*)b; } // 降序比较函数 int cmp_desc(const void* a, const void* b) { return *(int*)b - *(int*)a; } int main() { int T; scanf(%d, T); while (T--) { int n; scanf(%d, n); int* a (int*)malloc(n * sizeof(int)); int* b (int*)malloc(n * sizeof(int)); for (int i 0; i n; i) scanf(%d, a[i]); for (int i 0; i n; i) scanf(%d, b[i]); qsort(a, n, sizeof(int), cmp_asc); qsort(b, n, sizeof(int), cmp_desc); long long ans 0; for (int i 0; i n; i) { ans (long long)a[i] * b[i]; } printf(%lld\n, ans); free(a); free(b); } return 0; }4. 算法分析、常见错误与思维延伸4.1 时间与空间复杂度分析这道题的算法效率非常高。时间复杂度主要消耗在排序操作上。C的sort和C的qsort平均时间复杂度是O(n log n)。输入输出是O(n)。对于每个测试用例总复杂度是O(n log n)对于n1000甚至10000都完全不在话下。空间复杂度除了存储两个数组的O(n)空间排序过程通常需要额外的O(log n)栈空间对于内省排序整体是O(n)级别非常低。这对比穷举法的O((n!)^2)是指数级的提升充分体现了算法设计的价值。4.2 实战中极易出现的错误与调试技巧即便思路清晰实现时也可能踩坑下面是我在练习和教学中遇到的高频错误排序方向弄反这是最致命的错误直接导致结果错误。时刻记住最小和 一升序 一降序。可以在代码旁加上清晰的注释或者用一个小样例如12和34心算验证。整数溢出如前所述使用int存储最终结果可能导致溢出得到错误甚至负数的结果。务必使用long long。在C中即使a[i]和b[i]是int在相乘前将其强制转换为long long是一种好习惯(long long)a[i] * b[i]。这样乘法会在64位环境下进行。输入格式处理错误蓝桥杯的题目输入有时很紧凑多个测试用例连着。务必确保你的while(T--)循环正确读取了每个用例的n和后续的2*n个数。在本地调试时可以打印出读入的数组内容确保与题目样例一致。C语言qsort比较函数返回值错误cmp函数应返回int规则是若认为第一个参数应排在第二个参数之前则返回负数若认为应排之后则返回正数相等返回0。对于降序cmp_descreturn *(int*)b - *(int*)a;意味着当ba时返回正数使大的b排在前面正确。调试技巧当提交后答案错误(WA)时不要慌张。首先用题目给的样例测试。如果过了再构造一些边界数据比如n1的情况所有数都是负数的情况所有数都是0的情况正负混合的情况。自己手算预期结果与程序输出对比很容易定位是逻辑错误还是溢出错误。4.3 从“基本型”到“变种问题”的思维延伸ALGO-53标注了“基本型”意味着还有“升级版”。理解这个基本模型是解决更复杂问题的基础。这里分享几个可能的变种和延伸思考变种1求最大乘积和。这就是排序不等式的直接应用了两组数都按升序或都按降序排列对应位置相乘求和即可得到最大值。变种2数组长度不同。如果两组数长度分别为m和n (m n)你需要从长数组中选择m个数进行配对使得乘积和最小。这就不再是简单的排序了可能需要结合贪心选择长数组中哪些数来配对甚至动态规划。但核心思想依然是“大配小”的贪心原则通常需要对两个数组都排序然后用短数组的最小值依次匹配长数组的最大值或反之取决于求最大还是最小。变种3每个数可以使用多次这完全变成了另一个问题可能涉及完全背包等动态规划模型。思维延伸为什么贪心算法在这里有效因为这个问题满足“贪心选择性质”和“最优子结构性质”。我们每一步都做出当前看来最优的选择用最大的配最小的并且这个选择不会影响后续子问题的最优解。识别出问题具备这种性质是应用贪心算法的关键。这道“最小乘积(基本型)”就像一颗完美的种子它生长出的解题思维——观察规律、数学证明、简化实现、警惕边界——是应对算法竞赛中大量“贪心”、“排序”类题目的通用法宝。下次再遇到类似“通过调整顺序优化某个指标”的问题时不妨先想想是否存在一种确定的排序策略可以让结果达到最优这往往就是解题的突破口。
返回列表