前言
在刷题时,我们经常会遇到“如何让某个数值最大化”的问题,比如让得分尽可能高、让成本尽可能低。这类问题看似复杂,但其实只要找到合适的策略,往往能简化得出解法。今天我们就以一道“数组配对最大化得分”的题目为例,聊聊怎么用贪心算法高效地解决它。
问题描述
假设你有一个整数数组 aaa,初始得分为 0。你可以从中挑选两个数配对,计算它们的乘积并累加到得分中。注意,两个数的差值不能超过 kkk,而且已经用过的数字不能再用。目标是尽可能让总得分最大化。
举个例子:
- 输入 n=6,k=2,a=[1,1,4,5,1,4]n = 6, k = 2, a = [1, 1, 4, 5, 1, 4]n=6,k=2,a=[1,1,4,5,1,4],输出是 212121。怎么来的呢?配对 4×5=204 \times 5 = 204×5=20,再配对 1×1=11 \times 1 = 11×1=1,得分累加。
- 如果 n=5,k=0,a=[2,2,2,2,2]n = 5, k = 0, a = [2, 2, 2, 2, 2]n=5,k=0,a=[2,2,2,2,2],输出就是 888。因为差值必须是 0,只能配对 2×2=42 \times 2 = 42×2=4,总共两对。
解题思路:从复杂到简单
拿到这个问题后,第一反应可能是:“我是不是得把所有可能的配对都试一遍?”
乍一看,这种暴力解法确实可行,但问题是,数组稍微长一点,计算量就会爆炸。比如一个 100 个数的数组,所有可能的配对要计算上千次,效率太低了。所以,我们需要一个高效的策略——这就是贪心算法。
1. 为什么要排序?
排序是很多贪心问题的基础操作。通过排序,我们可以:
- 快速找到差值符合 kkk 条件的数对,比如两个相邻的数差值最小。
- 从大到小配对优先级高的数,让得分尽量高。
具体来说,先把数组按升序排好后,每次从后往前取最大的数字,然后找一个和它差值不超过 kkk 的最优配对。
2. 贪心策略:优先选择大数
贪心算法的核心思想是“每次都做当前看起来最好的选择”。在这里,“最好的选择”就是让乘积尽可能大。所以我们要优先配对数组里最大的数。比如:
- 对于数组 [1,1,4,5,1,4][1, 1, 4, 5, 1, 4][1,1,4,5,1,4],如果你先配对 1×11 \times 11×1,浪费了两个“潜力股”,结果肯定不如先配 4×54 \times 54×5。
排序后,我们总是先选最大的数,再从剩下的数里找一个满足条件的配对,这样能确保得分最大。
3. 如何记录用过的数字?
为了避免重复使用数字,我们可以用一个布尔数组 used 来记录每个数字是否已经被配对过。每次配对成功后,就把这两个数字标记为“已用”,下次跳过它们。
代码实现
有了思路,接下来就是实现了。这段代码基于排序和双指针:
import java.util.Arrays;
public class Main {
public static int solution(int n, int k, int[] a) {
// Step 1: 排序数组
Arrays.sort(a);
// Step 2: 标记哪些数字已经被用过
boolean[] used = new boolean[n];
int score = 0;
// Step 3: 从大到小配对
for (int i = n - 1; i > 0; i--) {
if (used[i]) continue; // 如果当前数字已被配对,跳过
for (int j = i - 1; j >= 0; j--) {
if (!used[j] && Math.abs(a[i] - a[j]) <= k) {
// 配对成功,累加得分
score += a[i] * a[j];
used[i] = true;
used[j] = true;
break;
}
}
}
return score;
}
public static void main(String[] args) {
System.out.println(solution(6, 2, new int[]{1, 1, 4, 5, 1, 4})); // 输出 21
System.out.println(solution(4, 1, new int[]{3, 3, 4, 4})); // 输出 25
System.out.println(solution(5, 0, new int[]{2, 2, 2, 2, 2})); // 输出 8
}
}
思考与总结
1. 贪心算法的特点
贪心算法特别适合这种问题结构简单的场景,比如资源分配、区间覆盖等。它的核心思想是:
- 每一步都做当前最优选择:这里的最优就是“优先选大数”。
- 不追求全局考虑:只用关注当前数字的最佳配对。
但贪心算法也有局限性,不是所有问题都能通过局部最优来推导出全局最优。当问题有更复杂的约束时,可能需要动态规划或回溯法。
2. 关于时间复杂度
这段代码的效率主要取决于排序操作(O(nlogn)O(n \log n)O(nlogn))。配对过程用双指针实现,每个数字最多遍历一次,复杂度是 O(n)O(n)O(n)。整体复杂度是 O(nlogn)O(n \log n)O(nlogn),即使对于较大的数组也非常高效。
3. 代码优化的方向
如果希望进一步优化,可以尝试用更高效的数据结构(比如堆)来管理未配对的数字,从而减少寻找配对数字的耗时。但对于题目当前的规模,现有方法已经足够了。
写在最后
这道题虽然逻辑简单,但很能体现贪心算法的精髓。通过排序、逐步配对,我们把问题分解成一个个独立的小问题解决。这种方法不仅在算法题中常见,在实际工作中也非常实用,比如如何最大化资源利用率、最小化开销等。
解决问题的关键是理清思路,抓住核心。希望通过这篇文章,你对贪心算法的应用有了更直观的认识,也能在未来遇到类似问题时轻松应对!